编译原理小知识

为什么我不能乱链接

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
2
#include "a.h"
...
  • test.c
1
2
#include "a.h"
#include "b.h"

​ 那么两次include导致a.h两次代码复制到test.c,将由头文件保护的存在而不被编译两次。头文件保护和跨translation unit的链接没有关系,它只是防止一个translation unit内部,不因为多重间接include导致重定义。

​ 不仅是namespace不行,直接裸函数定义在 .hpp 中,之后又被多重引用 + 链接到同一个可执行文件中,也会导致问题,比如我试了试:

  • name.hpp 内容如下
1
2
3
4
5
6
7
8
#ifndef __NAME_HPP
#define __NAME_HPP

int sub(int a, int b) {
return a - b;
}

#endif //__NAME_HPP
  • 此后在test.cc , main.cc两个文件都 includename.hpp,进行编译,输出结果如下:
1
2
3
4
5
>>> g++ ./main.cc ./test.cc -o main.exe

~\ccMq7p5P.o:test.cc:(.text+0x0): multiple definition of `sub(int, int)'
~\cces7mzx.o:main.cc:(.text+0x0): first defined here
collect2.exe: error: ld returned 1 exit status

​ 也直接报错了。


IV. class可以在hpp内定义

​ class可以在 hpp 内定义函数,同样都是被定义.cc主函数.ccinclude,为什么类函数可以过编译?

​ 因为类函数有特殊性:声明 和 定义放在一起时,函数自动内联(inline)。inline函数在多重定义存在时,只会处理inline声明位置的定义。

​ 所以,另一种解决方案是:在namespace下的函数前面加上 inline。但是我个人非常不建议这么做。