编译原理小知识
为什么我不能乱链接
I. 缘从何起?
机队的小伙伴问了我一个问题,说是在一个.hpp
文件中,定义了一个namespace,.hpp
也有头文件保护,在namespace下也定义(声明 / 定义都在这个文件里)了几个函数(非类函数),.hpp
文件中同时还有一个类的定义。那么自然这个文件会被至少两个.cc
文件给include:
- 类定义文件 / main函数所在的可执行文件
结果编译的时候报了redefined的错误,说是namespace下面的函数重复定义了。小伙伴很疑惑。我也很疑惑,开始我觉得是不应该把定义写在.hpp
中,但想到类函数好像可以这么做啊?作为一个没有学过编译原理的自动化学生(请问自动化学生都在学些啥啊?),感到疑惑,于是查了点资料。
II. 编译单元
首先,一个名词:translation unit
A translation unit is the basic unit of compilation in C++. It consists of the contents of a single source file, plus the contents of any header files directly or indirectly included by it, minus those lines that were ignored using conditional preprocessing statements.
也即,一个translation unit包含源文件,直接或者间接include的头文件以及排除通过头文件保护排除的文件。
而编译形成最终的可执行文件需要通过:
- 编译+汇编(分别编译,汇编形成各自的机器指令文件)
- 链接:多重符号,跨文件符号问题
而你现在的情况是:
- main.cc 文件(主函数,将会产生一个translation unit)
- xxx.cc 文件(类定义,将会产生一个translation unit)
最终在链接阶段会将两个translation unit内容合并(链接过程工作的通俗说法)。但是很不巧,你的namespace下的函数同时进入了两个translation unit中。
也就是在两个translation unit中,各被编译一次。如果两个translation unit分属不同的可执行文件(不被链接到一块儿去),那还好说。但是现在他们被链接到一起去了,也就形成了重定义,两个translation unit中各有一份定义。
也就是说,最好的解决方案是:只在hpp中声明函数,在cc中定义函数,这样不会错。
III. 头文件保护?
头文件保护作用与单一的translation unit中。也即,比如:
b.h
中
1 |
|
test.c
中
1 |
那么两次include导致a.h
两次代码复制到test.c
,将由头文件保护的存在而不被编译两次。头文件保护和跨translation unit的链接没有关系,它只是防止一个translation unit内部,不因为多重间接include导致重定义。
不仅是namespace不行,直接裸函数定义在 .hpp
中,之后又被多重引用 + 链接到同一个可执行文件中,也会导致问题,比如我试了试:
name.hpp
内容如下
1 |
|
- 此后在
test.cc
,main.cc
两个文件都 includename.hpp
,进行编译,输出结果如下:
1 | >> g++ ./main.cc ./test.cc -o main.exe |
也直接报错了。
IV. class可以在hpp内定义
class可以在 hpp 内定义函数,同样都是被定义.cc
, 主函数.cc
include,为什么类函数可以过编译?
因为类函数有特殊性:声明 和 定义放在一起时,函数自动内联(inline)。inline函数在多重定义存在时,只会处理inline声明位置的定义。
所以,另一种解决方案是:在namespace下的函数前面加上 inline。但是我个人非常不建议这么做。