前言
之前虽然学过C++,但是都是零零散散看的,很多特性,尤其是C++ 11的特性我都不知道。因此最近在看C++ primer,算是系统地过一遍C++吧。在差不多看完C++ primer之后,对C++有了更深的理解,我才发现C++真是一门很有魅力的语言。虽然控制底层的东西很繁琐,但是也给了程序员一个接触底层的机会。
在写书上的代码时,我又遇到了一个刚入门时反复遇到的问题:头文件(.h)和源文件(.cpp)以及变量或函数的多重定义问题。因此借此机会,好好地缕一缕头文件和源文件的关系。
案例
头文件就是后缀名为.h
的文件,源文件就是后缀名为.cpp
的文件。在编译过程中编译器是怎么处理头文件和源文件的呢?我举个例子。假设项目结构如下所示:
project
----| main.cpp
----| test.cpp
----| test1.h
一个main.cpp
,一个test.cpp
,和一个头文件test1.h
。按道理来说头文件和对应的源文件应该使用同一个名字,不过其实两个文件的名字是否相同对于编译器来说是一样的,所以我就故意给这两个文件起了不同的名字。
test1.h
#ifndef HEADER_AND_SOURCE_TEST1_H
#define HEADER_AND_SOURCE_TEST1_H
#include <iostream>
void hhh() {
std::cout << "hhhhh" << std::endl;
}
#endif //HEADER_AND_SOURCE_TEST1_H
test.cpp
#include "test1.h"
main.cpp
#include <iostream>
#include "test1.h"
int main() {
hhh();
return 0;
}
相当于在头文件中定义了一个函数hhh()
,然后在两个源文件都#include
这个头文件。此时运行的话,会出现hhh()
多重定义的错误。
这就是我经常在使用多个头文件和源文件时会遇到的一个多重定义的问题。每次遇到都很苦恼,因为实际的文件中代码非常的多,都不知道这个问题到底错在哪。接下来就来仔细分析问题出在哪了。
分析
- 每个源文件(.cpp)都会被编译(这里的编译指的是整一个生成可执行文件的过程,包括预处理、编译、汇编、链接),最终会生成一个
.obj
文件,然后所有的.obj
文件链接起来你的可执行程序就算生成了。所以main.cpp
会变成main.o
,test.cpp
会变成test.o
。 - 每个头文件(.h)是不能被编译的。
#include
叫做编译预处理指令,可以简单理解成,在test.cpp
中的#include "test1.h"
指令把test1.h
中的代码在编译前添加到了test.cpp
的头部。可以简单理解为将头文件的文本插入到源文件中。
根据上面两个特点,可以重新分析一下案例。实际上,我们在test1.h
中定义了一个hhh()
,然后分别插入到test.cpp
和main.cpp
中。所以在将main.o
和test.o
链接生成可执行代码时,链接器发现他们各自都定义了hhh()
,这就起冲突了。这就是为什么会出现多重定义。
解决方法
其实找到了问题的根源之后,解决问题就很简单了。核心思想就是只保留函数的一个定义。
- 使用
inline
。实测有效,具体原因我不太明白。
与 inline 失败的函数有关的真相是, 编译器会为 inline 函数做上标记, 链接器根据这个标记, 从目标文件中找到这个函数的多处实现, 不负任何责任地选取其中一个作为正身, 并抛弃其它的实现, 无论这些实现是否相同 (链接器也无从判断是否相同).
转载自:http://blog.bitfoc.us/p/379
- 只在头文件
test1.h
中保留函数hhh()
的声明,而在源文件test.cpp
或者main.cpp
中定义函数hhh()
。这样可以保证在链接时,只有一个目标文件中拥有函数hhh()
的定义,因此就不会出现多重定义的错误。
总结
最好在头文件中只放函数的声明,而在源文件中才进行函数的定义。上面的分析已经给了一个明确的原因。如果头文件中包含函数定义,而这个头文件又被多个源文件包含,那么这个函数定义就会被插入到各个源文件中。因为每个源文件都会被编译成一个目标文件(.obj),也就意味着多个目标文件都有同一个函数的定义,因此在最终对所有目标文件进行链接生成可执行文件时,就会出现函数多重定义的问题。
所以可以理解为一个头文件会有一个对应的源文件来对头文件中的声明进行定义,因此头文件的名字一般与实现定义的源文件的名字是相同的,这样才能显示出两者的对应关系。