为什么要将声明和定义分离

OpenFOAM 中的类基本都遵循类的声明和定义分开在不同文件的规则。具体来说,一般是类的声明放在 “xxx.H”,类的成员函数的具体定义 “xxx.C”,如果有内联函数(inline),则还有 “xxxI.H”,并且,”xxx.H” 文件的最后会有 #include "xxxI.H"。这么做不仅是一种代码规范,真正的目的应该是为了防止重复定义的问题。本篇博文用一个简单的例子来说明这个问题。

为了防止混淆,先说明一下概念:

  • 函数声明:即只声明函数的返回类型,函数名以及参数列表,没有函数体,不具体定义函数的功能,比如
1
int max(int a, int b);
  • 函数定义:包含函数体,具体定义函数的功能。比如
1
2
3
4
5
6
7
int max(int a, int b)
{

if (a<b)
return b;
else
return a;
}

下面用一个简单的例子来说明。

测试一:非内联函数声明和定义必须分开的原因是防止重复定义问题

正常测试:

以下的测试代码共包括5个源文件,inlinetest.Hinlinetest.cppinlinederived.Hinlinederived.cppmain.cpp ,分别定义如下:

  • inlinetest.H

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #ifndef _INLINETEST_H
    #define _INLINETEST_H

    #include <iostream>
    using namespace std;

    class Itest //基类
    {
    public:
    Itest(int a, int b); // 构造函数
    int a_, b_; //数据成员
    inline int a(); // 内联成员函数
    int b(); //非内联成员函数
    };
    inline int Itest::a() //内联函数的定义
    {
    cout << "a=" << a_ << endl;
    return a_;
    }
    #endif
  • inlinetest.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include "inlinetest.H"

    Itest::Itest(int a, int b):a_(a),b_(b)
    {
    }
    int Itest::b()//非内联函数的定义
    {
    cout << "b=" << b_ << endl;
    return b_;
    }
  • inlinederived.H

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #ifndef _INLINEDERIVED_H
    #define _INLINEDERIVED_H

    #include <iostream>
    #include "inlinetest.H"
    using namespace std;
    class Iderive: public Itest //派生类
    {
    public:
    int c_;
    int c();//非内联成员函数
    Iderive(int a, int b, int c);
    };
    #endif
  • inlinederived.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include "inlinederived.H"

    Iderive::Iderive(int a, int b, int c):Itest(a,b),c_(c)
    {}
    int Iderive::c()
    {
    cout << "c=" << c_ << endl;
    return c_;
    }
  • main.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #include <iostream>
    #include "inlinetest.H"
    #include "inlinederived.H"

    int main(int argc, char *argv[])
    {

    Itest obj1(2,3); //基类对象
    Iderive obj2(1,2,3); // 派生类对象

    obj1.a();
    obj1.b();

    obj2.a();
    obj2.b();
    obj2.c();

    return 0;
    }

以上五个源文件是可以成功编译链接成可执行文件的,运行结果与预期一致:

1
2
3
4
5
a=2
b=3
a=1
b=2
c=3

异常测试:

下面来修改,如果将基类代码改一下,将非内联函数 b 的定义也放到头文件里,即:

  • inlinetest.H

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    #ifndef _INLINETEST_H
    #define _INLINETEST_H

    #include <iostream>
    using namespace std;

    class Itest //基类
    {
    public:
    Itest(int a, int b); // 构造函数
    int a_, b_; //数据成员
    inline int a(); // 内联成员函数
    int b(); //非内联成员函数
    };
    inline int Itest::a() //内联函数的定义
    {
    cout << "a=" << a_ << endl;
    return a_;
    }

    int Itest::b()//非内联函数的定义
    {
    cout << "b=" << b_ << endl;
    return b_;
    }
    #endif
  • inlinetest.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include "inlinetest.H"

    Itest::Itest(int a, int b):a_(a),b_(b)
    {
    }
    //int Itest::b()//非内联函数的定义
    //{
    // cout << "b=" << b_ << endl;
    // return b_;
    //}

结果是无法通过编译:

1
2
3
4
5
6
7
8
9
10
g++ -c inlinetest.cpp
g++ -c inlinederived.cpp
g++ -o main main.o inlinetest.o inlinederived.o
inlinetest.o:inlinetest.cpp:(.text+0x0): multiple definition of `Itest::b()'
main.o:main.cpp:(.text+0x0): first defined here
inlinederived.o:inlinederived.cpp:(.text+0x0): multiple definition of `Itest::b()'
main.o:main.cpp:(.text+0x0): first defined here
c:/mingw/bin/../lib/gcc/mingw32/4.8.1/../../../../mingw32/bin/ld.exe: main.o: bad reloc address 0x0 in section `.ctors'
collect2.exe: error: ld returned 1 exit status
make: *** [main] Error 1

可见,编译过程没有出错,但是链接时出错了,报错说 Itest::b() 函数(即基类中的非内联成员函数)被重复定义。原因在于,main.cpp 里有 #include "inlinetest.H" ,这句包含了对Itest::b() 函数的定义;此外,main.cpp 里还有 #include "inlinederived.H",而由于 inlinederived.H 里,也包含了#include "inlinetest.H"。所以,相当于main.cpp 中也对 Itest::b() 函数的定义了两次,于是连接过程就报错了。

在上面可以正常编译的情况里,即将Itest::b() 函数放在 inlinetest.cpp 里,就不会有这个问题,因为这时头文件里只有函数的声明,而函数的声明是可以重复的。

有人可能会问,这里的 main.cpp 里完全可以去掉 #include "inlinetest.H",这样也可以解决重复定义问题。对这里的简单例子,是没问题,但是,如果是像 OpenFOAM 这样大的项目,源文件之间的关系非常复杂,那就只能通过将声明和定义分离来解决这个问题了。

注意:这里的内联成员函数 a ,声明和定义都在头文件 inlinetest.H 里,但是却不会报重复定义的错误。

测试二:内联函数的声明和定义需要在同一个文件里,否则无法通过编译。

将上面提到的派生类代码修改一下,将内联成员函数的定义放到 inlinetest.cpp 里,即

  • inlinetest.H

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #ifndef _INLINETEST_H
    #define _INLINETEST_H

    #include <iostream>
    using namespace std;

    class Itest //基类
    {
    public:
    Itest(int a, int b); // 构造函数
    int a_, b_; //数据成员
    inline int a(); // 内联成员函数
    int b(); //非内联成员函数
    };
    //inline int Itest::a() //内联函数的定义
    //{
    //cout << "a=" << a_ << endl;
    //return a_;
    //}
    #endif
  • inlinetest.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #include "inlinetest.H"

    Itest::Itest(int a, int b):a_(a),b_(b)
    {
    }
    inline int Itest::a() //内联函数的定义
    {
    cout << "a=" << a_ << endl;
    return a_;
    }

    int Itest::b()//非内联函数的定义
    {
    cout << "b=" << b_ << endl;
    return b_;
    }

这时无法通过编译,编译器报错如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
g++ -c main.cpp
In file included from main.cpp:2:0:
inlinetest.H:13:13: warning: inline function 'int Itest::a()' used but never defined [enabled by default]
inline int a();
^
g++ -c inlinetest.cpp
g++ -c inlinederived.cpp
g++ -o main main.o inlinetest.o inlinederived.o
main.o:main.cpp:(.text+0x5c): undefined reference to `Itest::a()'
main.o:main.cpp:(.text+0x70): undefined reference to `Itest::a()'
c:/mingw/bin/../lib/gcc/mingw32/4.8.1/../../../../mingw32/bin/ld.exe: main.o: bad reloc address 0x0 in section `.ctors'
collect2.exe: error: ld returned 1 exit status
make: *** [main] Error 1

首先是编译过程有一个警告说 inlinetest.H 里的函数 int Itest::a() 没有定义,然后链接的时候报了 undefined reference to 'Itest::a()' 的错误,说明如果内联函数的声明和定义分属不同源文件,编译器是无法找到函数的定义的。

测试三 :内联函数的定义放到一个单独的源文件,并在头文件里 include 这个源文件

这个测试我做的比较简单,即将 inlinetest.H 拆开,内联函数 a 的定义放在单独的一个 inlinetestI.H 里:

  • inlinetest.H

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #ifndef _INLINETEST_H
    #define _INLINETEST_H

    #include <iostream>
    using namespace std;

    class Itest //基类
    {
    public:
    Itest(int a, int b); // 构造函数
    int a_, b_; //数据成员
    inline int a(); // 内联成员函数
    int b(); //非内联成员函数
    };
    #include "inlinetestI.H"
    #endif
  • inlinetestI.H

    1
    2
    3
    4
    5
    inline int Itest::a() //内联函数的定义
    {
    cout << "a=" << a_ << endl;
    return a_;
    }

编译链接成功,运行结果跟正常预期一致。

经过上述简单的测试,可以得到以下结论:

  1. 使用C++模板编程时,对于非内联成员函数,函数声明和定义要分开,目的在于防止重复定义的问题。
  2. 内联函数的声明和定义需要在同一个文件里,否则无法通过编译。
  3. OpenFOAM 里将内联成员函数提取出来放到一个单独的文件里( *I.H ),应该只是一种使用惯例。不是 C++ 语法的要求,将 *I.H 里的内容拷贝出来放到 *.H 后面,然后删除 #include "*I.H" 效果应该是一样的。