理解链接器重复符号错误的起源

27

我有一个之前编译过的c++程序,但在修改了Jamfiles后,该程序不再编译,并且ld发出了一个duplicate symbol error。即使我还原到原始Jamfiles,运行bjam clean,手动删除对象文件,并从clang切换到gcc 4.2.1 on MacOs 10.6.7,问题仍然存在。

简化程序描述如下:有main.cpp和四个文件a.h,cppb.h,cpp,它们被编译成一个静态库,该库链接到main.omain.cppb.cpp都依赖于包含有冲突符号off.h的文件,但是两个中间文件依赖于不同的off.h版本,而a.ha.cpp则没有任何依赖关系。

在你询问之前,我确保所有文件都包含了多重定义守卫(#ifndef, #define, #endif),并且虽然我发现有一个文件缺少了这些守卫,但它没有引用off.h。更重要的是,b.h没有包含任何引用off.h的内容,只有实现b.cppoff.h进行了引用。这让我感到困惑。

更加令我困惑的是,我能够从b.cpp中删除对off.h的引用,并且如预期一样成功重新编译。然而,当我将引用添加回去时,它也成功编译,并在清除对象文件后继续编译。由于符号不应该冲突,我已经避免了符号重复,也已经清除了任何之前/不完全的构建,因此我仍然不知道为什么它无法编译。

我已经成功编译了我的程序,因此我怀疑我是否能够再现它以测试任何建议。然而,我对于这种情况的发生感到好奇,如果将来我遇到这种行为,除了我已经做过的事情之外,我还可以做什么来修复它呢?


@Neil,没有这样的情况。请注意:我能够在多次尝试编译时重现此问题,直到我找到了一个“解决方案”。由于问题不再出现,我预计答案更具有理论性而非具体性。然而,我正在努力更好地理解当两个独立编译单元中引用同一符号时可能导致冲突的原因,并且我可以采取哪些额外步骤来自行诊断和缓解问题。 - rcollyer
1
我会给这个问题一个90%的概率,你实际上并没有清理所有的目标文件(也许是libtool或其他工具将它们隐藏在了一个隐藏目录中?)。不过,如果你无法重现这个问题,那么就无法找到问题的根源。 - bdonlan
@bdonlan,这是个有趣的想法,如果libtool执行了那种缓存操作,那将会非常令人恼火。可以假定通过bjam cleanrm *.o *.a删除了所有对象和库文件。如果没有被删除,那么在重新编译时它们应该已经被覆盖了。此外,默认情况下,当您更改编译器时,bjam会在不同的目录中进行编译,因此由libtool或其他工具进行的任何缓存都会使这种分离变得毫无意义。此类缓存会让我觉得很难创建可靠的构建,这是相当令人反感的。 - rcollyer
1
@bdonlan,关于你提到的不可简化性问题,作为一名前测试员,这些类型的问题对测试员和开发人员来说都是最令人沮丧的,它们经常因此被拒绝。然而,我认为这些问题有其价值:尽管采取了预防措施,我怎么会陷入这种情况?一旦陷入这种情况,该怎么办才能摆脱它?虽然缺乏具体的解决方案,但这些问题揭示了构建过程/语言中可能存在的潜在陷阱,这对于初学者/中级程序员可能并不清晰。 - rcollyer
耸肩 我没怎么用过bjam,所以不能具体说明。你可能想考虑使用版本控制工具来进行清理-例如,git clean -dfx(警告:这将会清除git未跟踪的任何文件,不询问,无例外) - bdonlan
1
@bdonlan,bjam似乎使用rm -f或操作系统的等效命令,而我正在使用Mercurial作为我的版本控制系统,它具有清除功能。 - rcollyer
1个回答

57

这通常是在头文件中定义对象而不仅仅是声明它的结果。请考虑以下示例:

h.h :

#ifndef H_H_
#define H_H_
int i;
#endif

a.cpp :

#include "h.h"

b.cpp :

->

b.cpp

#include "h.h"
int main() {}

这将产生一个重复的符号i。解决方法是在头文件中声明对象:extern int i;,并在源代码文件中确切地定义它:int i;


3
这个符号是在头文件中声明为内联函数的,但缺少关键字 inline,这可能会导致与这里所提到的链接错误有关。但是,我对你的解决方案的问题是为什么 a.o 目标文件中的 int i 会以这种方式被暴露出来,以至于会与 b.o 中的变量发生冲突?据我理解,这种情况不应该发生。我漏掉了什么? - rcollyer
5
  1. 文件作用域声明的名称具有外部链接。
  2. 一个对象可以在多个翻译单元中声明,但必须恰好定义一次。
- Robᵩ
等等,这个例子暗示着你不能将同一个头文件导入到两个不同的文件中。但是我们在使用stdio时经常这样做。我错过了什么吗? - sudo
4
@9000 - 如果一个头文件没有像 int i; 或者 int i = 17; 这样的语句,只有声明而没有定义,那么你可以在两个文件中都使用 #include 引入它。比如 extern int i; 就是可以的。 - Robᵩ
1
@rcollyer:我认为,在版本9之前,gcc假定a.ob.o中的i是相同的。这在后来的版本中发生了改变。 - Uwe Kleine-König
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接