首页 > C++ 阅读:12,693

C++如何进行多文件编程?(汇总版)

在 C++ 多文件编程中,一个完整的 C++ 项目可以包含 2 类文件,即 .h 文件和 .cpp 文件。通常情况下,.h 文件称为 C++ 头文件,.cpp 文件称为 C++ 源文件。

通过 《用g++命令执行C++多文件项目》一节的学习我们知道,同属一个 C++ 项目中的所有代码文件是分别进行编译的,只需要在编译成目标文件后再与其它目标文件做一次链接即可。例如,在 a.cpp 源文件中定义有一个全局函数 a(),而在文件 b.cpp 中需要调用这个函数。即便如此,处于编译阶段的 a.cpp 和 b.cpp 并不需要知道对方的存在,它们各自是独立编译的是,只要最后将编译得到的目标文件进行链接,整个程序就可以运行。

那么,整个过程是如何实现的呢?从写程序的角度来理解,当文件 b.cpp 中需要调用 a() 函数时,只需要先声明一下该函数即可。这是因为,编译器在编译 b.cpp 时会生成一个符号表,类似 a() 这样看不到定义的符号就会被存放在这个表中。在链接阶段,编译器就会在别的目标文件中去寻找这个符号的定义,一旦找到了,程序也就可以 顺利地生成了(反之则出现链接错误)。

注意,这里提到了两个概念,即“声明”和“定义”。所谓定义,指的是就是将某个符号完整的描述清楚,它是变量还是函数,变量类型以及变量值是多少,函数的参数有哪些以及返回值是什么等等;而“声明”的作用仅是告诉编译器该符号的存在,至于该符号的具体的含义,只有等链接的时候才能知道。

也就是说,定义的时候需要遵循 C++ 语法规则完整地描绘一个符号,而声明的时候只需要给出该符号的原型即可。值得一提的是在 C++ 项目中,一个符号允许被声明多次,但只能被定义一次。理由很简单,如果一个符号出现多种定义,编译器该采用哪一个呢?

基于声明和定义的不同,才有了 C++ 多文件编程的出现。试想如果有一个很常用的函数 f(),其会被程序中的很多 .cpp 文件调用,那么我们只需要在一个文件中定义此函数,然后在需要调用的这些文件中声明这个函数就可以了。

那么问题来了,一个函数还好对付,声明起来也就一句话,如果有好几百个函数(比如是一大堆的数学函数),该怎么办呢?一种简单的方法就是将它们的声明全部放在一个文件中,当需要时直接从文件中拷贝。这种方式固然可行,但还是太麻烦,而且还显得很笨拙,于是头文件便可以发挥它的作用了。

所谓的头文件,其实它的内容跟 .cpp 文件中的内容是一样的,都是 C++ 的源代码,唯一的区别在于头文件不用被编译。我们把所有的函数声明全部放进一个头文件中,当某一个 .cpp 源文件需要时,可以通过 #include 宏命令直接将头文件中的所有内容引入到 .cpp 文件中。这样,当 .cpp 文件被编译之前(也就是预处理阶段),使用 #include 引入的 .h 文件就会替换成该文件中的所有声明。

以《用g++命令执行C++多文件项目》一节中的 C++ 项目为例,拥有 student.h、student.cpp 和 main.cpp 这 3 个文件,其中 student.cpp 和 main.cpp 文件中用 #include 引入了 student.h 文件。在此基础上,文章中用 g++ 命令分别对 student.cpp 和 main.cpp 进行了预处理操作,并分别生成了 student.i 和 main.i 文件。

如下展示了 main.i 文件中的内容:
class Student {
public:
    const char *name;
    int age;
    float score;
    void say();
};
int main() {
    Student *pStu = new Student;
    pStu->name = "小明";
    pStu->age = 15;
    pStu->score = 92.5f;
    pStu->say();
    delete pStu;
    return 0;
}
显然和之前的 main.cpp 文件相比,抹去了用 #include 引入 student.h 文件,而是将 student.h 文件中所有的内容都拷贝了过来。

#include 是一个来自 C 语言的宏命令,作用于程序执行的预处理阶段,其功能是将它后面所写文件中的内容,完完整整、一字不差地拷贝到当前文件中。

C++头文件内应该写什么

通过上面的讲解读者应该知道,.h 头文件的作用就是被其它的 .cpp 包含进去,其本身并不参与编译,但实际上它们的内容会在多个 .cpp 文件中得到编译。

通过“符号的定义只能有一次”的规则,我们可以很容易地得出,头文件中应该只放变量和函数的声明,而不能放它们的定义。因为一个头文件的内容实际上是会被引入到多个不同的 .cpp 文件中的,并且它们都会被编译。换句话说,如果在头文件中放了定义,就等同于在多个 .cpp 文件中出现对同一个符号(变量或函数)的定义,纵然这些定义的内容相同,编译器也不认可这种做法(报“重定义”错误)。

所以读者一定要谨记,.h 头文件中只能存放变量或者函数的声明,而不要放定义。例如:
extern int a;
void f();
这些都是声明。反之:
int a;
void f() {}
这些都是定义,如果存放在 .h 文件中,一旦该文件被 2 个以上的 .cpp 文件引入,编译器就会立马报错。

凡事都有例外,以上 3 种情况也属于定义的范畴,但它们应该放在 .h 文件中:

1) 头文件中可以定义 const 对象

要知道,全局的 const 对象默认是没有 extern 声明的,所以它只在当前文件中有效。把这样的对象写进头文件中,即使它被包含到其他多个 .cpp 文件中,这个对象也都只在包含它的那个文件中有效,对其他文件来说是不可见的,所以便不会导致多重定义。

与此同时,由于这些 .cpp 文件中的 const 对象都是从一个头文件中包含进去的,也就保证了这些 .cpp 文件中的 const 对象的值是相同的,可谓一举两得。

同理,static 对象的定义也可以放进头文件。

2) 头文件中可以定义内联函数

内联函数(用 inline 修饰的函数)是需要编译器在编译阶段根据其定义将它内联展开的(类似宏展开),而并非像普通函数那样先声明再链接。这就意味着,编译器必须在编译时就找到内联函数的完整定义。

显然,把内联函数的定义放进一个头文件中是非常明智的做法。

有关 C++ 内联函数,读者可回顾《C++ inline内联函数详解》一节。

3) 头文件中可以定义类

因为在程序中创建一个类的对象时,编译器只有在这个类的定义完全可见的情况下,才能知道这个类的对象应该如何布局,所以,关于类的定义的要求,跟内联函数是基本一样的,即把类的定义放进头文件,在使用到这个类的.cpp文件中去包含这个头文件。

值得一提的是,类的内部通常包含成员变量和成员函数,成员变量是要等到具体的对象被创建时才会被定义(分配空间),但成员函数却是需要在一开始就被定义的,这也就是类的实现。通常的做法是将类的定义放在头文件中,而把成员函数的实现代码放在一个 .cpp 文件中。

还有另一种办法,就是直接成员函数的实现代码写到类定义的内部。在 C++ 的类中,如果成员函数直接定义在类体的内部,则编译器会将其视为内联函数。所以把函数成员的定义写进类体内,一起放进头文件中,也是合法的。

注意,如果把成员函数的定义写在定义类的头文件中,而没有写进类内部,这是不合法的。这种情况下,此成员函数不是内联函数,一旦头文件被两个或两个以上的 .cpp 文件包含,就可能会出现重定义的错误。

有效避免头文件被重复引入

在 C++ 多文件编程中,如果 .h 头文件中只包含声明语句的话,即便被同一个 .cpp 文件引入多次也没有问题,因为声明语句是可以重复的,且重复次数不受限制。然而,刚刚讨论到的 3 种特殊情况也是头文件很常用的一个用处。如果一个头文件中出现了上面 3 种情况中的任何一种,且被同一个 .cpp 文件引入多次,就会发生重定义错误。

在 C++ 多文件编程中,为了有效避免“因多次引入头文件发生重定义”的问题,C++ 提供了 3 种处理机制,其中最常用的一种方式就是借助条件编译 #ifndef/#define/#endif,初学者一定要学会至少一种方式。

有关 C++ 中防止头文件被重复引入的 3 种方式,读者可回顾《C++防止头文件被重复引入的3种方法》一节。

关注微信公众号「站长严长生」,在手机上阅读所有教程,随时随地都能学习。本公众号由C语言中文网站长运营,每日更新,坚持原创,敢说真话,凡事有态度。

魏雪原二维码
微信扫描二维码关注公众号