一个同事在检查另外一个同事的代码时,突然发现一个 inline 函数在不同的 cpp 文件里面都有定义,而且,在另外一个cpp里面也引用了这个inline函数。示意代码如下:
// a.h
|
// a.cpp |
// b.h
|
// b.cpp |
// c.cpp |
一开始觉得很奇怪,怎么定义了2个一摸一样的inline函数呢 ? 而且在VC下面编译的时候居然没有报错,更加神奇的,是在链接的时候也没有报错。。。
于是乎,查C++标准,查各种文档,不断地试验,终于得知了为什么能够编译和链接:
1、当 inline 函数被声明为 extern 或者在定义该inline函数的翻译单元(TU)之外也有对该函数的引用的时候,该函数就会被作为一个特别的函数来生成代码(类似于一般的函数),而不是一段嵌入的代码(最简单的inline函数那种)。这就解释了为什么能够编译成功。
2、在编译成功后要进行链接啊,比如上面的例子,c.cpp里面的foo引用要跟a.cpp或者b.cpp里面的foo定义链接啊,按照上面1的情况,会有2个foo的定义,这会导致“定义歧义”的错误,但是为什么这里没有报错呢 ?大家估计可以猜得到,肯定是链接器只链接了其中一个版本,比如a.obj或者b.obj里面得版本。这个是什么机制呢 ?
3、答案就在前面第1点里面,当被作为一个特别的函数生成目标代码的时候,这个“特别的函数”的特别地方就是它的目标代码被标志为“延迟链接”,同时被被标志为在重定位的时候“pick any”,这就意味着我们所定义的foo函数是延迟链接,最后从2个foo定义里面取一个进行重定位,生成可执行文件。这就解释了为什么能够链接成功而不报错误。
4、既然是“pick any”,如果没有明确的链接顺序指定给链接器(linker),则很容易产生预期结果和实际结果不一致的情况(linker充当一下葫芦官),比如,上面的例子,你用 cl /out:c.exe a.cpp c.cpp b.cpp得到的 c.exe 和 cl /out:c2.exe b.cpp c.cpp a.cpp 得到的 c2.exe 是不一样的,可以试着修改一下 b.cpp 里面的 foo 函数,就知道修改文件的次序也会导致结果的不同。
5、这个例子是一个典型的“code of bad smell ”,这些代码导致修改了foo函数的修改不能如实地按照预期在最终的可执行文件里面得到体现,导致代码和结果的不一致,如果栽在这样的问题当中,估计无论是谁都会很郁闷。