什么是未定义的参考 / 未解决的外部符号错误?常见原因是什么?如何解决 / 预防它们?
随意编辑 / 添加自己的。
编译 C ++ 程序分2.2步(分给 Keith Thompson 供参考)指定,分几个步骤进行 :
翻译的语法规则中的优先级由以下阶段指定[请参见脚注] 。
- 必要时,以实现定义的方式将物理源文件字符映射到基本源字符集(为行尾指示符引入换行符)。 [SNIP]
- 紧跟换行符的每个反斜杠字符(\)实例都将被删除,从而将物理源代码行拼接起来以形成逻辑源代码行。 [SNIP]
- 源文件被分解为预处理令牌(2.5)和空白字符序列(包括注释)。 [SNIP]
- 执行预处理指令,扩展宏调用,并执行_Pragma 一元运算符表达式。 [SNIP]
- 字符文字或字符串文字中的每个源字符集成员,以及字符文字或非原始字符串文字中的每个转义序列和通用字符名称都将转换为执行字符集的相应成员; [SNIP]
- 相邻的字符串文字标记是串联在一起的。
- 分隔标记的空格字符不再重要。每个预处理令牌都将转换为令牌。 (2.7)。对生成的令牌进行语法和语义分析,并将其作为翻译单元进行翻译。 [SNIP]
- 翻译的翻译单元和实例化单元的组合如下: [SNIP]
- 所有外部实体引用均已解决。库组件被链接以满足对当前翻译中未定义的实体的外部引用。所有此类转换器输出都收集到一个程序映像中,该映像包含在其执行环境中执行所需的信息。 (强调我的)
[脚注]尽管实际上不同的阶段可能会折叠在一起,但实现方式必须表现得好像这些单独的阶段一样。
指定的错误发生在编译的最后阶段,通常称为链接。从根本上讲,这意味着您将一堆实现文件编译为目标文件或库,现在想让它们一起工作。
假设您在a.cpp
定义了符号a
。现在, b.cpp
声明了该符号并使用了它。在链接之前,它只是假定该符号已在某处定义,但它并不关心在何处。链接阶段负责查找符号并将其正确链接到b.cpp
(实际上是使用它的对象或库)。
如果您使用的是 Microsoft Visual Studio,则会看到项目会生成.lib
文件。它们包含一个导出符号表和一个导入符号表。将根据链接的库解析导入的符号,并为使用该.lib
的库(如果有)提供导出的符号。
对于其他编译器 / 平台也存在类似的机制。
常见错误消息是Microsoft Visual Studio error LNK2001
, error LNK1120
, error LNK2019
和GCC 的 undefined reference to
symbolName 的 undefined reference to
。
代码:
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
常见原因包括:
virtual
析构函数需要实现。 声明纯析构函数仍然需要您对其进行定义(与常规函数不同):
struct X
{
virtual ~X() = 0;
};
struct Y : X
{
~Y() {}
};
int main()
{
Y y;
}
//X::~X(){} //uncomment this line for successful definition
发生这种情况是因为在隐式销毁对象时调用了基类析构函数,因此需要定义。
virtual
方法必须实现或定义为纯方法。 这类似于没有定义的非virtual
方法,其另外的原因是纯声明会生成虚拟 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;
};
virtual
班级成员即使没有明确使用,也需要定义一些成员:
struct A
{
~A();
};
以下将产生错误:
A a; //destructor undefined
在类定义本身中,实现可以是内联的:
struct A
{
~A() {}
};
或外面:
A::~A() {}
如果实现在类定义之外,但在标头中,则必须将方法标记为inline
以防止出现多个定义。
如果使用,则需要定义所有使用的成员方法。
struct A
{
void foo();
};
void foo() {}
int main()
{
A a;
a.foo();
}
定义应为
void A::foo() {}
static
数据成员必须在类外部以单个翻译单元定义 : struct X
{
static int x;
};
int main()
{
int x = X::x;
}
//int X::x; //uncomment this line to define X::x
可以为类定义内的整数或枚举类型的static
const
数据成员提供初始化程序;但是,对该成员的 odr-use 仍然需要如上所述的名称空间范围定义。 C ++ 11 允许在类内部对所有static const
数据成员进行初始化。
通常,每个翻译单元都会生成一个目标文件,其中包含该翻译单元中定义的符号的定义。要使用这些符号,您必须链接这些对象文件。
在gcc 下,您将指定所有要在命令行中链接在一起的目标文件,或者将实现文件编译在一起。
g++ -o test objectFile1.o objectFile2.o -lLibraryName
这里的libraryName
只是库的裸名,没有特定于平台的添加。因此,例如在 Linux 上,库文件通常称为libfoo.so
但您只写-lfoo
。在 Windows 上,该文件可能称为foo.lib
,但您将使用相同的参数。您可能必须使用-L‹directory›
添加可以在其中找到这些文件的-L‹directory›
。确保不要在-l
或-L
之后写空格。
对于XCode :添加用户标题搜索路径 -> 添加库搜索路径 -> 将实际库引用拖放到项目文件夹中。
在MSVS 下 ,添加到项目中的文件会自动将其目标文件链接在一起,并且会生成一个lib
文件(通常使用)。要在单独的项目中使用这些符号,您需要在项目设置中包含lib
文件。这是在项目属性的 “链接器” 部分的 “ Input -> Additional Dependencies
。 ( lib
文件的路径应在Linker -> General -> Additional Library Directories
)当使用lib
文件附带的第三方库时,这样做通常会导致错误。
您还可能忘记将文件添加到编译中,在这种情况下将不会生成目标文件。在gcc 中,您可以将文件添加到命令行中。在MSVS 中,将文件添加到项目将使其自动编译(尽管可以手动将文件从构建中单独排除)。
在 Windows 编程中,您没有链接必要库的迹象表明,未解析符号的名称以__imp_
。在文档中查找该函数的名称,并应说明您需要使用哪个库。例如,MSDN 将信息放在每个函数底部 “方框” 中的框中。
典型的变量声明是
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
请注意,您实现的功能与声明的功能完全匹配。例如,您的 cv 限定词可能不匹配:
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)
不匹配的其他示例包括
来自编译器的错误消息通常会为您提供已声明但从未定义的变量或函数的完整声明。将其与您提供的定义进行比较。 确保每个细节都匹配。
如果库相互依赖,则链接库的顺序确实很重要。在一般情况下,如果库A
依赖于图书馆B
,然后libA
前必须出现libB
在连接标志。
例如:
// B.h
#ifndef B_H
#define B_H
struct B {
B(int);
int x;
};
#endif
// B.cpp
#include "B.h"
B::B(int xx) : x(xx) {}
// A.h
#include "B.h"
struct A {
A(int x);
B b;
};
// A.cpp
#include "A.h"
A::A(int x) : b(x) {}
// main.cpp
#include "A.h"
int main() {
A a(5);
return 0;
};
创建库:
$ g++ -c A.cpp
$ g++ -c B.cpp
$ ar rvs libA.a A.o
ar: creating libA.a
a - A.o
$ ar rvs libB.a B.o
ar: creating libB.a
a - B.o
编译:
$ g++ main.cpp -L. -lB -lA
./libA.a(A.o): In function `A::A(int)':
A.cpp:(.text+0x1c): undefined reference to `B::B(int)'
collect2: error: ld returned 1 exit status
$ g++ main.cpp -L. -lA -lB
$ ./a.out
因此,要再次重复,顺序确实很重要!
什么是 “未定义的引用 / 未解析的外部符号”
我将尝试解释什么是 “未定义的引用 / 未解析的外部符号”。
注意:我使用 g ++ 和 Linux,所有示例均适用
例如我们有一些代码
// src1.cpp
void print();
static int local_var_name; // 'static' makes variable not visible for other modules
int global_var_name = 123;
int main()
{
print();
return 0;
}
和
// src2.cpp
extern "C" int printf (const char*, ...);
extern int global_var_name;
//extern int local_var_name;
void print ()
{
// printf("%d%d\n", global_var_name, local_var_name);
printf("%d\n", global_var_name);
}
制作目标文件
$ g++ -c src1.cpp -o src1.o
$ g++ -c src2.cpp -o src2.o
在汇编阶段之后,我们有了一个目标文件,其中包含要导出的任何符号。看符号
$ readelf --symbols src1.o
Num: Value Size Type Bind Vis Ndx Name
5: 0000000000000000 4 OBJECT LOCAL DEFAULT 4 _ZL14local_var_name # [1]
9: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 global_var_name # [2]
我拒绝了输出中的某些行,因为它们无关紧要
因此,我们看到跟随符号导出。
[1] - this is our static (local) variable (important - Bind has a type "LOCAL")
[2] - this is our global variable
src2.cpp 不导出任何内容,我们也没有看到它的符号
链接我们的目标文件
$ g++ src1.o src2.o -o prog
并运行它
$ ./prog
123
链接器看到导出的符号并将其链接。现在,我们尝试像这样在 src2.cpp 中取消注释行
// src2.cpp
extern "C" int printf (const char*, ...);
extern int global_var_name;
extern int local_var_name;
void print ()
{
printf("%d%d\n", global_var_name, local_var_name);
}
并重建一个目标文件
$ g++ -c src2.cpp -o src2.o
OK(没有错误),因为我们仅构建目标文件,所以尚未完成链接。尝试连结
$ g++ src1.o src2.o -o prog
src2.o: In function `print()':
src2.cpp:(.text+0x6): undefined reference to `local_var_name'
collect2: error: ld returned 1 exit status
这是因为我们的 local_var_name 是静态的,即其他模块不可见。现在更深入。获取翻译阶段输出
$ g++ -S src1.cpp -o src1.s
// src1.s
look src1.s
.file "src1.cpp"
.local _ZL14local_var_name
.comm _ZL14local_var_name,4,4
.globl global_var_name
.data
.align 4
.type global_var_name, @object
.size global_var_name, 4
global_var_name:
.long 123
.text
.globl main
.type main, @function
main:
; assembler code, not interesting for us
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 4.8.2-19ubuntu1) 4.8.2"
.section .note.GNU-stack,"",@progbits
因此,我们已经看到 local_var_name 没有标签,这就是链接器找不到它的原因。但是我们是黑客:),我们可以修复它。在文本编辑器中打开 src1.s 并进行更改
.local _ZL14local_var_name
.comm _ZL14local_var_name,4,4
至
.globl local_var_name
.data
.align 4
.type local_var_name, @object
.size local_var_name, 4
local_var_name:
.long 456789
即你应该像下面
.file "src1.cpp"
.globl local_var_name
.data
.align 4
.type local_var_name, @object
.size local_var_name, 4
local_var_name:
.long 456789
.globl global_var_name
.align 4
.type global_var_name, @object
.size global_var_name, 4
global_var_name:
.long 123
.text
.globl main
.type main, @function
main:
; ...
我们已经更改了 local_var_name 的可见性并将其值设置为 456789。尝试从中构建目标文件
$ g++ -c src1.s -o src2.o
好的,请参见 readelf 输出(符号)
$ readelf --symbols src1.o
8: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 local_var_name
现在 local_var_name 具有 Bind GLOBAL(原为 LOCAL)
链接
$ g++ src1.o src2.o -o prog
并运行它
$ ./prog
123456789
好的,我们破解它:)
因此,结果是 - 当链接器在目标文件中找不到全局符号时,发生 “未定义的引用 / 未解决的外部符号错误”。
函数(或变量) void foo()
是在 C 程序中定义的,而您尝试在 C ++ 程序中使用它:
void foo();
int main()
{
foo();
}
C ++ 链接器期望名称被修饰,因此必须将函数声明为:
extern "C" void foo();
int main()
{
foo();
}
同样,不是在 C 程序中定义,而是在 C ++ 中使用 C 链接定义了函数(或变量) void foo()
:
extern "C" void foo();
并且您尝试在具有 C ++ 链接的 C ++ 程序中使用它。
如果整个库包含在头文件中(并已编译为 C 代码);包含将需要如下;
extern "C" {
#include "cheader.h"
}
如果其他所有方法均失败,请重新编译。
我最近能够通过重新编译有问题的文件来摆脱 Visual Studio 2012 中无法解决的外部错误。当我重建时,错误消失了。
当两个(或多个)库具有循环依赖性时,通常会发生这种情况。库 A 尝试使用 B.lib 中的符号,而库 B 尝试使用 A.lib 中的符号。都不存在。当您尝试编译 A 时,链接步骤将失败,因为它找不到 B.lib。将生成 A.lib,但不会生成 dll。然后,您编译 B,它将成功并生成 B.lib。现在可以重新编译 A,因为现在可以找到 B.lib。
MSVS 要求您使用__declspec(dllexport)
和__declspec(dllimport)
指定要导出和导入的符号。
通常通过使用宏来获得此双重功能:
#ifdef THIS_MODULE
#define DLLIMPEXP __declspec(dllexport)
#else
#define DLLIMPEXP __declspec(dllimport)
#endif
只能在导出函数的模块中定义宏THIS_MODULE
。这样,声明:
DLLIMPEXP void foo();
扩展到
__declspec(dllexport) void foo();
并告诉编译器导出函数,因为当前模块包含其定义。当将声明包含在其他模块中时,它将扩展为
__declspec(dllimport) void foo();
并告诉编译器该定义在您链接的库之一中(另请参见1) )。
您可以相似地导入 / 导出类:
class DLLIMPEXP X
{
};
这是每个 VC ++ 程序员一次又一次看到的最令人困惑的错误消息之一。首先让我们弄清楚。
答:什么是符号?简而言之,符号就是名称。它可以是变量名称,函数名称,类名称,typedef 名称或除属于 C ++ 语言的那些名称和符号以外的任何名称。它是由用户定义或由依赖项库引入的(另一个用户定义的)。
B. 什么是外部?在 VC ++ 中,每个源文件(.cpp,.c 等)都被视为翻译单元,编译器一次编译一个单元,并为当前翻译单元生成一个目标文件(.obj)。 (请注意,此源文件包含的每个头文件都将经过预处理,并将被视为此翻译单元的一部分)。翻译单元中的所有内容均视为内部文件,其他所有内容均视为外部文件。在 C ++ 中,您可以使用诸如extern
, __declspec (dllimport)
等关键字来引用外部符号。
C. 什么是 “解决”?解决是一个链接时间项。在链接时,链接器尝试为无法在内部找到其定义的目标文件中的每个符号查找外部定义。此搜索过程的范围包括:
此搜索过程称为解析。
D. 最后,为什么未解析的外部符号?如果链接器找不到内部没有定义的符号的外部定义,则会报告未解决的外部符号错误。
E.LNK2019 的可能原因 :未解决的外部符号错误。我们已经知道此错误是由于链接器未能找到外部符号的定义,可能的原因可以归类为:
例如,如果我们在 a.cpp 中定义了一个名为 foo 的函数:
int foo()
{
return 0;
}
在 b.cpp 中,我们要调用函数 foo,因此我们添加了
void foo();
声明函数 foo(),并在另一个函数体中调用它,例如bar()
:
void bar()
{
foo();
}
现在,当您构建此代码时,您将收到 LNK2019 错误,抱怨 foo 是未解决的符号。在这种情况下,我们知道 foo()在 a.cpp 中有其定义,但与我们正在调用的定义不同(不同的返回值)。这就是定义存在的情况。
如果我们要调用库中的某些函数,但导入库未添加到Project | Properties | Configuration Properties | Linker | Input | Additional Dependency
设置的其他依赖项列表(设置为: Project | Properties | Configuration Properties | Linker | Input | Additional Dependency
)中。现在,链接器将报告 LNK2019,因为当前搜索范围中不存在该定义。