什么是未定义的参考/未解析的外部符号错误?常见原因是什么?如何解决/预防?
当前回答
编译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系统上也可以
其他回答
班级成员:
纯虚拟析构函数需要实现。
声明析构函数pure仍然需要定义它(与常规函数不同):
struct X
{
virtual ~X() = 0;
};
struct Y : X
{
~Y() {}
};
int main()
{
Y y;
}
//X::~X(){} //uncomment this line for successful definition
这是因为在隐式销毁对象时调用基类析构函数,因此需要定义。
虚拟方法必须实现或定义为纯方法。
这类似于没有定义的非虚拟方法,增加了如下推理:纯声明会生成一个虚拟vtable,您可能会在不使用函数的情况下得到链接器错误:
struct X
{
virtual void foo();
};
struct Y : X
{
void foo() {}
};
int main()
{
Y y; //linker error although there was no call to X::foo
}
要使其工作,请将X::foo()声明为纯:
struct X
{
virtual void foo() = 0;
};
非虚拟类成员
即使未明确使用,也需要定义某些成员:
struct A
{
~A();
};
以下内容将产生错误:
A a; //destructor undefined
实现可以在类定义本身中内联:
struct A
{
~A() {}
};
或外部:
A::~A() {}
如果实现在类定义之外,但在头中,则必须将方法标记为内联,以防止多重定义。
如果使用,则需要定义所有使用的成员方法。
一个常见的错误是忘记限定名称:
struct A
{
void foo();
};
void foo() {}
int main()
{
A a;
a.foo();
}
定义应为
void A::foo() {}
静态数据成员必须在类外部的单个转换单元中定义:
struct X
{
static int x;
};
int main()
{
int x = X::x;
}
//int X::x; //uncomment this line to define X::x
可以为类定义中的整型或枚举类型的静态常量数据成员提供初始值设定项;然而,odr使用这个成员仍然需要如上所述的命名空间范围定义。C++11允许在类内初始化所有静态常量数据成员。
UNICODE定义不一致
Windows UNICODE构建时,TCHAR等被定义为wchar_t等。如果不使用UNICODE进行构建,则TCHAR被定义为char等。这些UNICODE和_UNICODE定义会影响所有“t”字符串类型;LPTSTR、LPCTSTR及其麋鹿。
构建一个定义了UNICODE的库,并试图将其链接到未定义UNICODE项目中,将导致链接器错误,因为TCHAR的定义将不匹配;char与wchar_t。
错误通常包括一个带有char或wchar_t派生类型的值的函数,这些类型也可能包括std::basic_string<>等。浏览代码中受影响的函数时,通常会引用TCHAR或std::basic_string<TCHAR>等。这是一个信号,表明代码最初用于UNICODE和多字节字符(或“窄”)构建。
要更正此问题,请使用UNICODE(和_UNICODE)的一致定义构建所有必需的库和项目。
这可以通过以下两种方式实现:;#定义UNICODE#定义UNICODE或在项目设置中;项目财产>常规>项目默认值>字符集或在命令行上;/DUNICODE/D-UNICODE
如果不打算使用UNICODE,请确保未设置定义,和/或在项目中使用了多字符设置,并始终应用。
不要忘记在“发布”和“调试”版本之间保持一致。
当我们在程序中引用了对象名(类、函数、变量等),并且链接器试图在所有链接的对象文件和库中搜索它时无法找到它的定义时,就会出现“Undefined Reference”错误。
因此,当链接器找不到链接对象的定义时,它会发出“未定义引用”错误。从定义中可以清楚地看出,这种错误发生在链接过程的后期。导致“未定义引用”错误的原因多种多样。
一些可能的原因(更频繁):
#1) 没有为对象提供定义
这是导致“未定义引用”错误的最简单原因。程序员只是忘记了定义对象。
考虑以下C++程序。这里我们只指定了函数的原型,然后在主函数中使用了它。
#include <iostream>
int func1();
int main()
{
func1();
}
输出:
main.cpp:(.text+0x5): undefined reference to 'func1()'
collect2: error ld returned 1 exit status
因此,当我们编译此程序时,会发出链接器错误,该错误表示“未定义对‘func1()’的引用”。
为了消除这个错误,我们通过提供函数func1的定义来如下更正程序。现在程序给出了适当的输出。
#include <iostream>
using namespace std;
int func1();
int main()
{
func1();
}
int func1(){
cout<<"hello, world!!";
}
输出:
hello, world!!
#2) 使用的对象定义错误(签名不匹配)
“未定义引用”错误的另一个原因是我们指定了错误的定义。我们在程序中使用任何对象,其定义都不同。
考虑以下C++程序。这里我们调用了func1()。它的原型是int func1()。但其定义与原型不符。如我们所见,函数的定义包含函数的参数。
因此,当编译程序时,由于原型和函数调用匹配,编译是成功的。但是,当链接器试图将函数调用与其定义链接时,它会发现问题并将错误作为“未定义引用”发出。
#include <iostream>
using namespace std;
int func1();
int main()
{
func1();
}
int func1(int n){
cout<<"hello, world!!";
}
输出:
main.cpp:(.text+0x5): undefined reference to 'func1()'
collect2: error ld returned 1 exit status
因此,为了防止这种错误,我们只需交叉检查程序中所有对象的定义和用法是否匹配。
#3) 对象文件未正确链接
此问题还可能导致“未定义引用”错误。在这里,我们可能有多个源文件,我们可以独立编译它们。这样做时,对象链接不正确,导致“未定义引用”。
考虑以下两个C++程序。在第一个文件中,我们使用了第二个文件中定义的“print()”函数。当我们分别编译这些文件时,第一个文件为打印函数提供“未定义引用”,而第二个文件为主函数提供“不定义引用”。
int print();
int main()
{
print();
}
输出:
main.cpp:(.text+0x5): undefined reference to 'print()'
collect2: error ld returned 1 exit status
int print() {
return 42;
}
输出:
(.text+0x20): undefined reference to 'main'
collect2: error ld returned 1 exit status
解决此错误的方法是同时编译两个文件(例如,使用g++)。
除了已经讨论过的原因之外,“未定义的引用”也可能因为以下原因而发生。
#4) 错误的项目类型
当我们在visual studio等C++IDE中指定错误的项目类型,并尝试做项目不期望的事情时,我们就会得到“未定义的引用”。
#5) 没有库
如果程序员没有正确指定库路径,或者完全忘记指定它,那么我们将为程序从库中使用的所有引用获得一个“未定义引用”。
#6) 未编译从属文件
程序员必须确保我们事先编译项目的所有依赖项,以便在编译项目时,编译器找到所有依赖项并成功编译。如果缺少任何依赖项,那么编译器将给出“未定义的引用”。
除了上面讨论的原因之外,“未定义引用”错误还可能在许多其他情况下发生。但底线是程序员搞错了,为了防止这种错误,应该纠正这些错误。
链接在引用库的对象文件之前使用库
您正在尝试编译程序并将其与GCC工具链链接。链接指定了所有必要的库和库搜索路径如果libfoo依赖于libbar,那么链接会正确地将libfoo放在libbar之前。链接失败,对某些错误的引用未定义。但是所有未定义的东西都在你的头文件中声明#并且实际上是在您链接的库中定义的。
示例在C中。它们也可以是C++
一个涉及您自己构建的静态库的最小示例
my_lib.c文件
#include "my_lib.h"
#include <stdio.h>
void hw(void)
{
puts("Hello World");
}
my_lib.h
#ifndef MY_LIB_H
#define MT_LIB_H
extern void hw(void);
#endif
例如1.c
#include <my_lib.h>
int main()
{
hw();
return 0;
}
构建静态库:
$ gcc -c -o my_lib.o my_lib.c
$ ar rcs libmy_lib.a my_lib.o
编译程序:
$ gcc -I. -c -o eg1.o eg1.c
尝试将其与libmy_lib.a链接,但失败:
$ gcc -o eg1 -L. -lmy_lib eg1.o
eg1.o: In function `main':
eg1.c:(.text+0x5): undefined reference to `hw'
collect2: error: ld returned 1 exit status
如果您在一个步骤中编译和链接,则会得到相同的结果,例如:
$ gcc -o eg1 -I. -L. -lmy_lib eg1.c
/tmp/ccQk1tvs.o: In function `main':
eg1.c:(.text+0x5): undefined reference to `hw'
collect2: error: ld returned 1 exit status
一个涉及共享系统库的最小示例,即压缩库libz
例如2.c
#include <zlib.h>
#include <stdio.h>
int main()
{
printf("%s\n",zlibVersion());
return 0;
}
编译程序:
$ gcc -c -o eg2.o eg2.c
尝试将程序与libz链接并失败:
$ gcc -o eg2 -lz eg2.o
eg2.o: In function `main':
eg2.c:(.text+0x5): undefined reference to `zlibVersion'
collect2: error: ld returned 1 exit status
如果您一次编译并链接:
$ gcc -o eg2 -I. -lz eg2.c
/tmp/ccxCiGn7.o: In function `main':
eg2.c:(.text+0x5): undefined reference to `zlibVersion'
collect2: error: ld returned 1 exit status
示例2的一个变体涉及pkg配置:
$ gcc -o eg2 $(pkg-config --libs zlib) eg2.o
eg2.o: In function `main':
eg2.c:(.text+0x5): undefined reference to `zlibVersion'
你做错了什么?
在要链接的对象文件和库序列中程序,您将库放在引用的对象文件之前他们您需要将库放在引用对他们来说。
正确链接示例1:
$ gcc -o eg1 eg1.o -L. -lmy_lib
成功:
$ ./eg1
Hello World
正确链接示例2:
$ gcc -o eg2 eg2.o -lz
成功:
$ ./eg2
1.2.8
正确链接示例2 pkg配置变体:
$ gcc -o eg2 eg2.o $(pkg-config --libs zlib)
$ ./eg2
1.2.8
解释
从这里开始,阅读是可选的。
默认情况下,GCC在您的发行版上生成的链接命令,在中从左到右使用链接中的文件命令行序列。当它发现一个文件引用了某个并且不包含其定义,to将搜索定义在更右边的文件中。如果它最终找到了定义解析引用。如果任何引用在结束时仍未解决,链接失败:链接器没有向后搜索。
首先,示例1,使用静态库my_lib.a
静态库是对象文件的索引存档。当链接器在链接序列中找到-lmy-lib,并指出这是指到静态库/libmy_lib.a,它想知道您的程序是否需要libmy_lib.a中的任何对象文件。
libmy_lib.a中只有一个对象文件,即my_lib.o,并且只定义了一个对象在my_lib.o中,即函数hw。
链接器将决定您的程序需要my_lib.o,如果并且仅当它已经知道您的程序在一个或多个对象文件中引用hw添加到程序中,并且没有添加任何对象文件包含hw的定义。
如果这是真的,那么链接器将从库中提取my_lib.o的副本,并将其添加到您的程序中。然后,您的程序包含hw的定义,因此其对hw的引用被解析。
当您尝试链接程序时,如:
$ gcc -o eg1 -L. -lmy_lib eg1.o
链接器在看到时没有将eg1.o添加到程序中-lmy_库。因为在这一点上,它还没有看到eg1.o。您的程序尚未引用hw:it根本没有做任何引用,因为它做的所有引用在eg1.o中。
因此,链接器不会将my_lib.o添加到程序中,并且没有其他内容用于libmy_lib.a。
接下来,它找到eg1.o,并将其添加到程序中。中的对象文件链接序列总是添加到程序中。现在,该程序使对hw的引用,并且不包含hw的定义;但是链接序列中没有任何内容可以提供缺失释义对hw的引用最终无法解析,链接失败。
第二,示例2,使用共享库libz
共享库不是对象文件或类似文件的存档更像是一个没有主功能的程序而是公开它定义的多个其他符号程序可以在运行时使用它们。
今天,许多Linux发行版配置其GCC工具链,以便其语言驱动程序(GCC、g++、gfortran等)指示系统链接器(ld)根据需要链接共享库。你有一个这样的发行版。
这意味着当链接器在链接序列中找到-lz,并发现这是指到共享库(例如)/usr/lib/x86_64-linux-gnu/libz,它想知道它添加到程序中的任何尚未定义的引用是否具有libz导出的定义
如果这是真的,那么链接器将不会从libz和将它们添加到您的程序中;相反,它只会修改程序的代码从而:-
在运行时,系统程序加载器会将libz的副本加载到无论何时加载程序的副本,都可以执行与程序相同的过程。在运行时,每当程序引用libz,该引用使用中libz副本导出的定义相同的过程。
您的程序只想引用一个由libz导出的定义,即函数zlibVersion,在eg2.c中仅引用一次。如果链接器将该引用添加到程序中,然后找到定义由libz导出,引用被解析
但当您尝试链接程序时,如:
gcc -o eg2 -lz eg2.o
事件的顺序是错误的,其方式与示例1相同。在链接器找到-lz时,没有对任何内容的引用在节目中:他们都在eg2.o中,这还没有被看到。所以链接器决定它不适用于libz。当它达到eg2.o时,将其添加到程序中,然后对zlibVersion有未定义的引用,链接序列完成;该引用未解析,链接失败。
最后,示例2的pkg配置变体现在有了一个显而易见的解释。壳体膨胀后:
gcc -o eg2 $(pkg-config --libs zlib) eg2.o
变为:
gcc -o eg2 -lz eg2.o
这再次只是示例2。
我可以重复示例1中的问题,但不能重复示例2中的问题
联动装置:
gcc -o eg2 -lz eg2.o
对你来说很好!
(或者:在Fedora 23上,这种链接很好,但在Ubuntu 16.04上失败了)
这是因为链接工作的发行版是不配置其GCC工具链以根据需要链接共享库。
过去,类unix系统链接静态和共享是很正常的不同的规则。链接序列中的静态库已链接但是共享库是无条件链接的。
这种行为在链接时是经济的,因为链接器不必考虑程序是否需要共享库:如果是共享库,大多数链接中的大多数库都是共享库。但也有缺点:-
这在运行时是不经济的,因为它会导致共享库与程序一起加载,即使不需要它们。静态库和共享库的不同链接规则可能会令人困惑对于不熟练的程序员,他们可能不知道-lfo是否在他们的联系中将解析为/some/where/libfoo.a或/some/wwhere/libfoo.so,并且可能不理解共享库和静态库之间的区别无论如何
这种权衡导致了今天的分裂局面。一些发行版有更改了共享库的GCC链接规则,以便根据需要这一原则适用于所有图书馆。一些发行版沿用了旧版本方法
为什么即使同时编译和链接,我仍然会遇到这个问题?
如果我这样做:
$ gcc -o eg1 -I. -L. -lmy_lib eg1.c
当然,gcc必须首先编译eg1.c,然后链接生成的带有libmy_lib.a的对象文件,那么它怎么可能不知道该对象文件在进行链接时是否需要?
因为使用单个命令编译和链接不会更改连杆顺序。
当您运行上面的命令时,gcc发现您需要编译+链接。因此,在幕后,它生成一个编译命令,并运行然后生成一个链接命令并运行它,就像您运行了两个命令:
$ gcc -I. -c -o eg1.o eg1.c
$ gcc -o eg1 -L. -lmy_lib eg1.o
因此,如果运行这两个命令,链接就会失败。这个您在失败中注意到的唯一区别是gcc生成了编译+链接情况下的临时对象文件,因为您没有告诉它使用eg1.o.我们看到:
/tmp/ccQk1tvs.o: In function `main'
而不是:
eg1.o: In function `main':
另请参见
指定相互依赖的链接库的顺序错误
将相互依赖的库按错误的顺序排列只是一种方法在其中,您可以获取需要对即将到来的事物进行定义的文件在链接中晚于提供定义的文件。将库放在引用它们的对象文件是犯同样错误的另一种方式。
已声明但未定义变量或函数。
典型的变量声明是
extern int x;
由于这只是一个声明,因此需要一个单独的定义。相应的定义如下:
int x;
例如,以下内容将生成错误:
extern int x;
int main()
{
x = 0;
}
//int x; // uncomment this line for successful definition
类似的注释适用于函数。声明函数而不定义它会导致错误:
void foo(); // declaration only
int main()
{
foo();
}
//void foo() {} //uncomment this line for successful definition
请注意,您实现的函数与您声明的函数完全匹配。例如,您可能有不匹配的简历限定符:
void foo(int& x);
int main()
{
int x;
foo(x);
}
void foo(const int& x) {} //different function, doesn't provide a definition
//for void foo(int& x)
不匹配的其他示例包括
函数/变量在一个命名空间中声明,在另一个命名空间定义。函数/变量声明为类成员,定义为全局(反之亦然)。函数返回类型、参数编号和类型以及调用约定并不完全一致。
来自编译器的错误消息通常会给出已声明但从未定义的变量或函数的完整声明。将其与您提供的定义进行比较。确保每个细节都匹配。