什么是未定义的参考/未解析的外部符号错误?常见原因是什么?如何解决/预防?


当前回答

我正在构建一个共享/动态库。它在Linux和*BSD上运行,但在Mac OS X上,完全相同的编译和链接命令会产生未解决的引用错误。有什么好处?

Mac OS X在内部与Linux和*BSD非常不同。对象/可执行文件格式为

在Linux和*BSD上,当构建共享库时,默认情况下允许未解析的引用。期望它们在加载时能够满足主可执行文件和/或其他共享库的要求。如果在加载时无法解析这些符号,则共享库将无法加载。

在Mac OS X上,构建动态库时,默认情况下不允许未解析的引用。如果希望在加载时解析引用,则需要显式启用未解析的引用。这是使用未定义的dynamic_lookup链接器标志完成的。

在构建可加载插件时,允许未解析的引用非常有用。

其他回答

如果所有其他操作都失败,请重新编译。

最近,我只需重新编译有问题的文件,就可以消除Visual Studio 2012中未解决的外部错误。当我重新构建时,错误消失了。

当两个(或多个)库具有循环依赖关系时,通常会发生这种情况。库A尝试使用B.lib中的符号,库B尝试使用A.lib中的字符。两者都不存在。当您尝试编译A时,链接步骤将失败,因为它找不到B.lib。将生成A.lib,但不会生成dll。然后编译B,这将成功并生成B.lib。重新编译A现在可以工作了,因为现在找到了B.lib。

模板实现不可见。

非专用模板的定义必须对使用它们的所有翻译单位可见。这意味着不能分离模板的定义到实现文件。如果必须分离实现,通常的解决方法是在头的末尾包含一个impl文件声明模板。常见的情况是:

template<class T>
struct X
{
    void foo();
};

int main()
{
    X<int> x;
    x.foo();
}

//differentImplementationFile.cpp
template<class T>
void X<T>::foo()
{
}

要解决这个问题,必须将X::foo的定义移动到头文件或使用它的翻译单元可见的某个位置。

专用化模板可以在实现文件中实现,并且实现不必是可见的,但是必须事先声明专用化。

有关进一步的解释和另一种可能的解决方案(显式实例化),请参阅此问题和答案。

编译C++程序分几个步骤进行,如2.2所述(Keith Thompson作为参考):

翻译语法规则之间的优先顺序由以下阶段规定[参见脚注]。物理源文件字符以实现定义的方式映射到基本源字符集(为行尾指示符引入新的行尾字符)如果必需的[剪]将删除紧跟着换行符的反斜杠字符(\)的每个实例,将物理源行拼接到形成逻辑源线。[剪]源文件被分解为预处理标记(2.5)和空白字符序列(包括注释)。[剪]执行预处理指令,展开宏调用,并执行_Pragma一元运算符表达式。[剪]字符文本或字符串文本中的每个源字符集成员,以及每个转义序列和通用字符名在字符文本或非原始字符串文本中执行字符集的对应成员;[剪]连接相邻的字符串文字标记。分隔标记的空白字符不再有效。每个预处理令牌都转换为一个令牌。(2.7)对生成的令牌进行语法和语义分析翻译为翻译单元。[剪]翻译的翻译单元和实例化单元组合如下:[SNIP]解析所有外部实体引用。链接库组件以满足对未在中定义的实体的外部引用当前翻译。所有这样的转换器输出都被收集到包含执行所需信息的程序映像执行环境。(强调矿井)[脚注]尽管在实践中不同的阶段可能会被合并在一起,但实现必须表现得好像这些单独的阶段发生了一样。

指定的错误发生在编译的最后阶段,通常称为链接。这基本上意味着你把一堆实现文件编译成了对象文件或库,现在你想让它们一起工作。

假设您在.cpp中定义了符号a。现在,b.cpp声明了该符号并使用了它。在链接之前,它只是假设该符号是在某个地方定义的,但它并不在乎在哪里。链接阶段负责查找符号并将其正确链接到b.cpp(实际上,链接到使用它的对象或库)。

如果您使用的是Microsoft Visual Studio,您将看到项目生成.lib文件。其中包含导出符号表和导入符号表。导入的符号将根据链接的库进行解析,导出的符号将提供给使用该.lib的库(如果有)。

其他编译器/平台也存在类似的机制。

常见的错误消息包括错误LNK2001、错误LNK1120、错误LNK2019(适用于Microsoft Visual Studio)和未定义的对GCC symbolName的引用。

代码:

struct X
{
   virtual void foo();
};
struct Y : X
{
   void foo() {}
};
struct A
{
   virtual ~A() = 0;
};
struct B: A
{
   virtual ~B(){}
};
extern int x;
void foo();
int main()
{
   x = 0;
   foo();
   Y y;
   B b;
}

将使用GCC生成以下错误:

/home/AbiSfw/ccvvuHoX.o: In function `main':
prog.cpp:(.text+0x10): undefined reference to `x'
prog.cpp:(.text+0x19): undefined reference to `foo()'
prog.cpp:(.text+0x2d): undefined reference to `A::~A()'
/home/AbiSfw/ccvvuHoX.o: In function `B::~B()':
prog.cpp:(.text._ZN1BD1Ev[B::~B()]+0xb): undefined reference to `A::~A()'
/home/AbiSfw/ccvvuHoX.o: In function `B::~B()':
prog.cpp:(.text._ZN1BD0Ev[B::~B()]+0x12): undefined reference to `A::~A()'
/home/AbiSfw/ccvvuHoX.o:(.rodata._ZTI1Y[typeinfo for Y]+0x8): undefined reference to `typeinfo for X'
/home/AbiSfw/ccvvuHoX.o:(.rodata._ZTI1B[typeinfo for B]+0x8): undefined reference to `typeinfo for A'
collect2: ld returned 1 exit status

以及Microsoft Visual Studio中的类似错误:

1>test2.obj : error LNK2001: unresolved external symbol "void __cdecl foo(void)" (?foo@@YAXXZ)
1>test2.obj : error LNK2001: unresolved external symbol "int x" (?x@@3HA)
1>test2.obj : error LNK2001: unresolved external symbol "public: virtual __thiscall A::~A(void)" (??1A@@UAE@XZ)
1>test2.obj : error LNK2001: unresolved external symbol "public: virtual void __thiscall X::foo(void)" (?foo@X@@UAEXXZ)
1>...\test2.exe : fatal error LNK1120: 4 unresolved externals

常见原因包括:

未能链接到适当的库/对象文件或编译实现文件已声明和未定义的变量或函数。类类型成员的常见问题模板实现不可见。符号是在C程序中定义的,并在C++代码中使用。跨模块.dll错误地导入/导出方法/类。(特定于MSVS)循环库依赖关系对的未定义引用`WinMain@16'相互依赖的库顺序多个同名源文件使用#pragma(Microsoft Visual Studio)时键入错误或不包含.lib扩展名模板好友问题UNICODE定义不一致常量变量声明/定义中缺少“extern”(仅限C++)未为多文件项目配置Visual Studio代码在Mac OS X上构建dylib时出错,但在其他Unix-y系统上也可以

当包含路径不同时

当头文件及其关联的共享库(.lib文件)不同步时,可能会发生链接器错误。让我解释一下。

链接器是如何工作的?链接器通过比较函数声明(在头中声明)和函数定义(在共享库中)的签名来匹配它们。如果链接器找不到完全匹配的函数定义,则可能会出现链接器错误。

即使声明和定义似乎匹配,仍然可能出现链接器错误吗?对它们在源代码中看起来可能相同,但这实际上取决于编译器所看到的内容。基本上,你可能会遇到这样的情况:

// header1.h
typedef int Number;
void foo(Number);

// header2.h
typedef float Number;
void foo(Number); // this only looks the same lexically

注意,即使两个函数声明在源代码中看起来相同,但根据编译器的不同,它们确实不同。

你可能会问,一个人在这样的情况下是如何结束的?当然包括路径!如果在编译共享库时,include路径指向header1.h,而您最终在自己的程序中使用了header2.h,那么您将无法理解发生了什么(双关语)。

下面将解释这在现实世界中如何发生的一个示例。

通过示例进一步阐述

我有两个项目:graphics.lib和main.exe。两个项目都依赖common_math.h。假设库导出以下函数:

// graphics.lib    
#include "common_math.h" 
   
void draw(vec3 p) { ... } // vec3 comes from common_math.h

然后,您继续将库包含在您自己的项目中。

// main.exe
#include "other/common_math.h"
#include "graphics.h"

int main() {
    draw(...);
}

繁荣你得到了一个链接器错误,你不知道它为什么会失败。原因是公共库使用相同includecommon_math.h的不同版本(我在本例中通过包含不同的路径来说明这一点,但可能并不总是那么明显。可能编译器设置中的包含路径不同)。

注意,在这个例子中,链接器会告诉你它找不到draw(),而实际上你知道它显然是由库导出的。你可以花几个小时挠头,想知道出了什么问题。问题是,链接器看到的签名不同,因为参数类型略有不同。在本例中,就编译器而言,vec3在两个项目中都是不同的类型。这可能是因为它们来自两个稍微不同的包含文件(可能包含文件来自库的两个不同版本)。

调试链接器

如果您正在使用Visual Studio,DUMPBIN是您的朋友。我相信其他编译器也有类似的工具。

过程如下:

注意链接器错误中给出的奇怪的损坏名称。(例如。draw@graphics@XYZ)。将库中导出的符号转储到文本文件中。搜索导出的感兴趣符号,并注意损坏的名称不同。请注意,为什么被弄乱的名字最终会不同。您将能够看到参数类型不同,即使它们在源代码中看起来相同。它们不同的原因。在上面给出的示例中,它们是不同的,因为包含文件不同。

[1] 我所说的项目是指一组链接在一起以生成库或可执行文件的源文件。

编辑1:改写第一节,使其更容易理解。请在下面评论,让我知道是否需要修复其他问题。谢谢

使用链接器帮助诊断错误

大多数现代链接器都有一个冗长的选项,可以在不同程度上打印出来;

链接调用(命令行),关于链接阶段中包括哪些库的数据,库的位置,使用的搜索路径。

用于gcc和clang;通常会在命令行中添加-v-Wl、--verbose或-v-Wl和-v。更多详情请点击此处;

Linux ld手册页。LLVM链接器页。“GCC简介”第9章。

对于MSVC,/VERBOSE(特别是/VERBOSE:LIB)将添加到链接命令行。

/VERBOSE链接器选项上的MSDN页面。