什么是未定义的引用/未解决的外部符号错误?什么是常见原因以及如何修复/预防它们?
this
指针,也无法访问类成员。当非静态成员函数缺少其限定名时,完成编译并且仅在链接期间失败是非常罕见的。
如 2.2 (credits to Keith Thompson for the reference) 所指定,编译 C++ 程序需要几个步骤:
翻译语法规则之间的优先级由以下阶段指定[见脚注]。如有必要,物理源文件字符以实现定义的方式映射到基本源字符集(为行尾指示符引入换行符)。 [SNIP] 每个反斜杠字符 (\) 的实例后面紧跟一个换行符被删除,拼接物理源代码行以形成逻辑源代码行。 [SNIP] 源文件被分解为预处理标记(2.5)和空白字符序列(包括注释)。 [SNIP] 执行预处理指令,扩展宏调用,并执行 _Pragma 一元运算符表达式。 [SNIP] 字符文字或字符串文字中的每个源字符集成员,以及字符文字或非原始字符串文字中的每个转义序列和通用字符名称,都将转换为执行的相应成员字符集; [SNIP] 连接相邻的字符串文字标记。分隔标记的空白字符不再重要。每个预处理令牌都被转换为一个令牌。 (2.7)。生成的标记在句法和语义上进行分析,并作为翻译单元进行翻译。 [SNIP] 翻译后的翻译单元和实例化单元组合如下: [SNIP] 解析所有外部实体引用。链接库组件以满足对当前翻译中未定义的实体的外部引用。所有此类翻译器输出都被收集到程序映像中,该程序映像包含在其执行环境中执行所需的信息。 (强调我的)[脚注]实现必须表现得好像这些单独的阶段发生了一样,尽管在实践中不同的阶段可能被折叠在一起。
指定的错误发生在编译的最后阶段,通常称为链接。这基本上意味着您将一堆实现文件编译成目标文件或库,现在您想让它们一起工作。
假设您在 a.cpp
中定义了符号 a
。现在,b.cpp
声明该符号并使用它。在链接之前,它只是假设该符号是在 somewhere 定义的,但它并不关心在哪里。链接阶段负责查找符号并将其正确链接到 b.cpp
(实际上,链接到使用它的对象或库)。
如果您使用的是 Microsoft Visual Studio,您会看到项目生成 .lib
文件。其中包含导出符号表和导入符号表。导入的符号将根据您链接的库进行解析,并为使用该 .lib
(如果有)的库提供导出的符号。
其他编译器/平台也存在类似的机制。
Microsoft Visual Studio 的常见错误消息是 error LNK2001
、error LNK1120
、error LNK2019
和 GCC 的 undefined reference to
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 定义不一致
const 变量声明/定义中缺少“extern”(仅限 C++)
未为多文件项目配置 Visual Studio Code
班级成员:
纯虚拟析构函数需要一个实现。
声明一个纯析构函数仍然需要您定义它(与常规函数不同):
struct X
{
virtual ~X() = 0;
};
struct Y : X
{
~Y() {}
};
int main()
{
Y y;
}
//X::~X(){} //uncomment this line for successful definition
发生这种情况是因为当对象被隐式销毁时会调用基类析构函数,因此需要定义。
虚拟方法必须实现或定义为纯方法。
这类似于没有定义的非 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;
};
非虚拟班级成员
一些成员即使没有显式使用也需要定义:
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() {}
静态数据成员必须在单个翻译单元中的类之外定义:
struct X
{
static int x;
};
int main()
{
int x = X::x;
}
//int X::x; //uncomment this line to define X::x
可以为类定义中的整数或枚举类型的 static
const
数据成员提供初始化程序;但是,该成员的 odr 使用仍需要如上所述的命名空间范围定义。 C++11 允许在类内对所有 static const
数据成员进行初始化。
未能链接到适当的库/目标文件或编译实现文件
通常,每个翻译单元将生成一个目标文件,其中包含在该翻译单元中定义的符号的定义。要使用这些符号,您必须链接到这些目标文件。
在 gcc 下,您将指定要在命令行中链接在一起的所有目标文件,或者一起编译实现文件。
g++ -o test objectFile1.o objectFile2.o -lLibraryName
-l...
必须位于任何 .o
/.c
/.cpp
文件的右侧。
这里的 libraryName
只是库的简单名称,没有特定于平台的添加。例如,Linux 库文件通常称为 libfoo.so
,但您只需要编写 -lfoo
。在 Windows 上,同一文件可能称为 foo.lib
,但您将使用相同的参数。您可能必须使用 -L‹directory›
添加可以找到这些文件的目录。确保在 -l
或 -L
之后不要写空格。
对于 XCode:添加用户标题搜索路径 -> 添加库搜索路径 -> 将实际库引用拖放到项目文件夹中。
在 MSVS 下,添加到项目中的文件会自动将它们的目标文件链接在一起,并且会生成一个 lib
文件(常用)。要在单独的项目中使用符号,您需要在项目设置中包含 lib
文件。这是在 Input -> Additional Dependencies
中项目属性的链接器部分完成的。 (lib
文件的路径应添加到 Linker -> General -> Additional Library Directories
中)当使用随 lib
文件提供的第三方库时,不这样做通常会导致错误。
您也可能忘记将文件添加到编译中,在这种情况下将不会生成目标文件。在 gcc 中,您可以将文件添加到命令行。在 MSVS 中,将文件添加到项目将使其自动编译(尽管可以手动将文件从构建中单独排除)。
在 Windows 编程中,未链接必要库的标志是未解析符号的名称以 __imp_
开头。在文档中查找函数的名称,它应该说明您需要使用哪个库。例如,MSDN 将信息放在每个函数底部的一个框中,称为“库”部分。
gcc main.c
而不是 gcc main.c other.c
的常见错误(初学者经常在他们的项目变得如此之大以构建 .o 文件之前经常这样做),那就太好了。
已声明但未定义变量或函数。
一个典型的变量声明是
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)
其他不匹配的例子包括
在一个命名空间中声明的函数/变量,在另一个命名空间中定义。
声明为类成员的函数/变量,定义为全局(反之亦然)。
函数返回类型、参数编号和类型以及调用约定并不完全一致。
来自编译器的错误消息通常会为您提供已声明但从未定义的变量或函数的完整声明。将其与您提供的定义进行仔细比较。确保每个细节都匹配。
#includes
而不是 added 与源目录匹配的 cpp 文件也属于缺少定义的类别。
指定相互依赖的链接库的顺序是错误的。
如果库相互依赖,则链接库的顺序很重要。通常,如果库 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
好的(没有错误),因为我们只构建目标文件,链接还没有完成。尝试链接
$ 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
好的,我们破解它:)
因此,当链接器在目标文件中找不到全局符号时,会发生“未定义的引用/未解决的外部符号错误”。
符号在 C 程序中定义并在 C++ 代码中使用。
函数(或变量)void foo()
在 C 程序中定义,您尝试在 C++ 程序中使用它:
void foo();
int main()
{
foo();
}
C++ 链接器期望名称被破坏,因此您必须将函数声明为:
extern "C" void foo();
int main()
{
foo();
}
等效地,函数(或变量)void foo()
不是在 C 程序中定义,而是在 C++ 中定义但具有 C 链接:
extern "C" void foo();
并且您尝试在具有 C++ 链接的 C++ 程序中使用它。
如果整个库包含在头文件中(并且被编译为 C 代码);包含将需要如下所示;
extern "C" {
#include "cheader.h"
}
#ifdef __cplusplus [\n] extern"C" { [\n] #endif
和 #ifdef __cplusplus [\n] } [\n] #endif
包围所有导出的声明来保护头文件([\n]
是真正的回车,但我不能写这个正确地在评论中)。
extern "C" { #include <myCppHeader.h> }
中,也会发生这种情况。
如果一切都失败了,请重新编译。
我最近能够通过重新编译有问题的文件来摆脱 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
的定义移动到头文件或使用它的翻译单元可见的某个位置。
专门化的模板可以在实现文件中实现,并且实现不必是可见的,但必须事先声明专门化。
有关进一步的解释和另一种可能的解决方案(显式实例化),请参见 this question and answer。
这是每个 VC++ 程序员一次又一次看到的最令人困惑的错误消息之一。让我们先把事情弄清楚。
A. 什么是象征?简而言之,符号就是名称。它可以是变量名、函数名、类名、typedef 名,或者除了那些属于 C++ 语言的名称和符号之外的任何东西。它是用户定义或由依赖库(另一个用户定义)引入的。
B.什么是外部的? 在VC++中,每个源文件(.cpp,.c等)都被认为是一个翻译单元,编译器一次编译一个单元,生成一个目标文件(.obj)当前翻译单元。 (请注意,此源文件包含的每个头文件都将被预处理并被视为此翻译单元的一部分)翻译单元内的所有内容都被视为内部内容,其他所有内容都被视为外部内容。在 C++ 中,您可以使用 extern
、__declspec (dllimport)
等关键字来引用外部符号。
C. 什么是“决心”? Resolve 是一个链接时间术语。在链接时,链接器尝试为目标文件中无法在内部找到其定义的每个符号查找外部定义。此搜索过程的范围包括:
编译时生成的所有目标文件
显式或隐式指定为此构建应用程序的附加依赖项的所有库 (.lib)。
这个搜索过程称为resolve。
D. 最后,为什么是 Unresolved External Symbol?如果链接器找不到内部没有定义的符号的外部定义,则会报告未解析的外部符号错误。
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
)中。现在链接器将报告 LNK2019,因为当前搜索范围内不存在该定义。
跨模块/dll(特定于编译器)错误地导入/导出方法/类。
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
{
};
visibility
和 Windows 的 .def
文件,因为它们也会影响符号名称和存在。
.def
文件了。随意添加答案或编辑此答案。
对 WinMain@16
或类似'unusual' main()
入口点引用的未定义引用(尤其是对于 visual-studio)。
您可能错过了使用实际 IDE 选择正确的项目类型。 IDE 可能希望将例如 Windows 应用程序项目绑定到此类入口点函数(如上面缺少的参考中所指定),而不是常用的 int main(int argc, char** argv);
签名。
如果您的 IDE 支持普通控制台项目,您可能希望选择此项目类型,而不是 Windows 应用程序项目。
以下是从现实世界问题中更详细地处理的 case1 和 case2。
此外,如果您使用的是 3rd 方库,请确保您拥有正确的 32/64 位二进制文件
Microsoft 提供 #pragma
以在链接时引用正确的库;
#pragma comment(lib, "libname.lib")
除了包含库目录的库路径外,这应该是库的全名。
需要为新的工具集版本更新 Visual Studio NuGet 包
我只是在尝试将 libpng 与 Visual Studio 2013 链接时遇到了这个问题。问题是包文件只有 Visual Studio 2010 和 2012 的库。
正确的解决方案是希望开发人员发布一个更新的包然后升级,但它通过入侵 VS2013 的额外设置对我有用,指向 VS2012 库文件。
我通过找到 packagename\build\native\packagename.targets
并在该文件中复制所有 v110
部分来编辑包(在解决方案目录内的 packages
文件夹中)。我在仅条件字段中将 v110
更改为 v120
,非常小心地将文件名路径全部保留为 v110
。这只是允许 Visual Studio 2013 链接到 2012 年的库,在这种情况下,它起作用了。
假设您有一个用 c++ 编写的大项目,其中包含一千个 .cpp 文件和一千个 .h 文件。假设该项目还依赖于十个静态库。假设我们在 Windows 上,我们在 Visual Studio 20xx 中构建我们的项目。当您按 Ctrl + F7 Visual Studio 开始编译整个解决方案时(假设我们在解决方案中只有一个项目)
编译是什么意思?
Visual Studio 搜索文件 .vcxproj 并开始编译每个扩展名为 .cpp 的文件。编译顺序是未定义的。所以你不能假设文件 main.cpp 是先编译的
如果 .cpp 文件依赖于其他 .h 文件以查找可能在文件 .cpp 中定义或未定义的符号
如果存在一个编译器在其中找不到一个符号的 .cpp 文件,则编译器时错误会引发消息 Symbol x could not be found
为每个扩展名为 .cpp 的文件生成一个目标文件 .o,Visual Studio 还将输出写入名为 ProjectName.Cpp.Clean.txt 的文件中,该文件包含必须由链接器处理的所有目标文件。
编译的第二步由 Linker 完成。Linker 应该合并所有目标文件并最终构建输出(可能是可执行文件或库)
链接项目的步骤
解析所有目标文件并找到仅在标头中声明的定义(例如:前面的答案中提到的类的一种方法的代码,或事件初始化一个类内部成员的静态变量)
如果在目标文件中找不到一个符号,他也会在 Additional Libraries 中进行搜索。为了将新库添加到项目配置属性 -> VC++ 目录 -> 库目录,您在此处指定了用于搜索库和配置属性的附加文件夹 ->链接器 -> 用于指定库名称的输入。 - 如果链接器找不到您在一个 .cpp 中写入的符号,他会引发链接器时间错误,这听起来可能像错误 LNK2001:未解析的外部符号“void __cdecl foo(void)”(?foo@@YAXXZ)
观察
一旦链接器找到一个符号,他就不会在其他库中搜索它 链接库的顺序确实很重要。如果链接器在一个静态库中找到一个外部符号,他会将符号包含在项目的输出中。但是,如果库是共享的(动态),他不会在输出中包含代码(符号),但运行时崩溃可能发生
如何解决此类错误
编译器时间错误:
确保您编写的 c++ 项目语法正确。
链接器时间错误
定义您在头文件中声明的所有符号
使用 #pragma once 允许编译器不包含一个头文件,如果它已经包含在当前编译的 .cpp 中
确保您的外部库不包含可能与您在头文件中定义的其他符号发生冲突的符号
当您使用模板时,请确保在头文件中包含每个模板函数的定义,以允许编译器为任何实例化生成适当的代码。
使用链接器帮助诊断错误
大多数现代链接器都包含一个详细的选项,可以在不同程度上打印出来。
链接调用(命令行),
关于链接阶段包含哪些库的数据,
图书馆的位置,
使用的搜索路径。
对于 gcc 和 clang;您通常会将 -v -Wl,--verbose
或 -v -Wl,-v
添加到命令行。更多详情可在这找到;
Linux ld 手册页。
LLVM 链接器页面。
“GCC 简介”第 9 章。
对于 MSVC,将 /VERBOSE
(特别是 /VERBOSE:LIB
)添加到链接命令行。
/VERBOSE 链接器选项上的 MSDN 页面。
编译器/IDE 中的错误
我最近遇到了这个问题,结果是 it was a bug in Visual Studio Express 2013。我不得不从项目中删除一个源文件并重新添加它来克服这个错误。
如果您认为这可能是编译器/IDE 中的错误,请尝试以下步骤:
清理项目(某些 IDE 可以选择执行此操作,您也可以通过删除目标文件手动执行此操作)
尝试开始一个新项目,从原始项目中复制所有源代码。
链接的 .lib 文件与 .dll 相关联
我遇到过同样的问题。假设我有项目 MyProject 和 TestProject。我已经有效地将 MyProject 的 lib 文件链接到了 TestProject。但是,这个 lib 文件是在构建 MyProject 的 DLL 时生成的。此外,我没有包含 MyProject 中所有方法的源代码,而只包含对 DLL 入口点的访问。
为了解决这个问题,我将 MyProject 构建为 LIB,并将 TestProject 链接到这个 .lib 文件(我将生成的 .lib 文件复制粘贴到 TestProject 文件夹中)。然后我可以再次将 MyProject 构建为 DLL。它正在编译,因为与 TestProject 链接的库确实包含 MyProject 中类中所有方法的代码。
由于人们似乎在谈到链接器错误时被引导到这个问题,所以我将在此处添加此问题。
GCC 5.2.0 出现链接器错误的一个可能原因是现在默认选择了新的 libstdc++ 库 ABI。
如果您收到有关对涉及 std::__cxx11 命名空间或标记 [abi:cxx11] 中的类型的符号的未定义引用的链接器错误,则可能表明您正在尝试将使用不同值编译的目标文件链接在一起 _GLIBCXX_USE_CXX11_ABI宏。当链接到使用旧版本 GCC 编译的第三方库时,通常会发生这种情况。如果第三方库不能用新的 ABI 重建,那么你需要用旧的 ABI 重新编译你的代码。
因此,如果在 5.1.0 之后切换到 GCC 时突然出现链接器错误,这将是一件需要检查的事情。
您的链接在引用它们的目标文件之前使用库
您正在尝试使用 GCC 工具链编译和链接您的程序。
您的链接指定了所有必要的库和库搜索路径
如果 libfoo 依赖于 libbar,那么您的链接正确地将 libfoo 放在 libbar 之前。
您的链接因未定义对某些错误的引用而失败。
但是所有未定义的东西都在你有#included的头文件中声明,实际上是在你链接的库中定义的。
示例在 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
eg1.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
eg2.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
以及涉及 pkg-config
的示例 2 的变体:
$ 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-config
变体:
$ gcc -o eg2 eg2.o $(pkg-config --libs zlib)
$ ./eg2
1.2.8
说明
从这里开始,阅读是可选的。
默认情况下,由 GCC 生成的链接命令在您的发行版上按命令行顺序从左到右使用链接中的文件。当它发现一个文件引用了某个东西并且不包含它的定义时,它将在更右边的文件中搜索一个定义。如果它最终找到定义,则解析该引用。如果最后有任何引用仍未解决,则链接失败:链接器不会向后搜索。
首先,示例 1,带有静态库 my_lib.a
静态库是目标文件的索引存档。当链接器在链接序列中找到 -lmy_lib
并确定 this 引用静态库 ./libmy_lib.a
时,它想知道您的程序是否需要 libmy_lib.a
中的任何目标文件。
libmy_lib.a
中只有目标文件,即my_lib.o
,而my_lib.o
中只定义了一个东西,即函数hw
。
当且仅当链接器已经知道您的程序在它已添加到程序的一个或多个目标文件中引用 hw
并且没有任何目标文件时,链接器才会决定您的程序需要 my_lib.o
它已添加包含 hw
的定义。
如果是这样,那么链接器将从库中提取 my_lib.o
的副本并将其添加到您的程序中。然后,您的程序包含 hw
的定义,因此它对 hw
的引用已解决。
当您尝试像这样链接程序时:
$ gcc -o eg1 -L. -lmy_lib eg1.o
当它看到 -lmy_lib
时,链接器还没有添加 eg1.o
到程序中。因为那时,它还没有看到 eg1.o
。您的程序尚未对 hw
进行任何引用:它根本没有进行任何引用,因为它所做的所有引用都在 eg1.o
中。
因此链接器不会将 my_lib.o
添加到程序中,并且对 libmy_lib.a
没有进一步的用途。
接下来,它找到eg1.o
,并将其添加到程序中。链接序列中的目标文件总是添加到程序中。现在,程序引用了 hw
,并且不包含 hw
的定义;但是链接序列中没有任何东西可以提供缺失的定义。对 hw
的引用最终未解决,并且链接失败。
二、示例 2,使用共享库 libz
共享库不是目标文件或类似文件的存档。它更像是一个程序,它没有 main
函数,而是公开了它定义的多个其他符号,以便其他程序可以在运行时使用它们。
如今,许多 Linux 发行版都配置了他们的 GCC 工具链,以便其语言驱动程序(gcc
、g++
、gfortran
等)指示系统链接器(ld
)在根据需要 em>基础。您拥有其中一个发行版。
这意味着当链接器在链接序列中找到 -lz
并发现它引用了共享库(例如)/usr/lib/x86_64-linux-gnu/libz.so
时,它想知道它添加到您的程序中的任何引用是否不是尚未定义具有由 libz
导出的定义
如果这是真的,那么链接器将不会从 libz
中复制任何块并将它们添加到您的程序中;相反,它只会修改您的程序代码,以便:-
在运行时,系统程序加载器将在加载程序副本时将 libz 的副本加载到与您的程序相同的进程中,以运行它。
在运行时,只要您的程序引用 libz 中定义的内容,该引用就会使用同一进程中的 libz 副本导出的定义。
您的程序只想引用一个具有由 libz
导出的定义的东西,即函数 zlibVersion
,它在 eg2.c
中只被引用一次。如果链接器将该引用添加到您的程序,然后找到 libz
导出的定义,则该引用已解析
但是当您尝试像这样链接程序时:
gcc -o eg2 -lz eg2.o
事件的顺序与示例 1 相同。当链接器找到 -lz
时,程序中没有 no 对任何内容的引用:它们都在 {2 },目前还没有看到。所以链接器决定它对 libz
没有用处。当它到达 eg2.o
时,将其添加到程序中,然后对 zlibVersion
有未定义的引用,链接序列完成;该引用未解决,并且链接失败。
最后,示例 2 的 pkg-config
变体现在有了明显的解释。外壳扩展后:
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 系统通过不同的规则链接静态库和共享库是很正常的。链接序列中的静态库是在示例 1 中解释的按需链接的,但共享库是无条件链接的。
这种行为在链接时是经济的,因为链接器不必考虑程序是否需要共享库:如果它是共享库,则链接它。大多数链接中的大多数库都是共享库。但也有缺点:-
这在运行时是不经济的,因为即使不需要共享库,它也会导致与程序一起加载共享库。
静态库和共享库的不同链接规则可能会让不熟练的程序员感到困惑,他们可能不知道链接中的 -lfoo 将解析为 /some/where/libfoo.a 还是 /some/where/libfoo.so,并且无论如何可能不理解共享库和静态库之间的区别。
这种权衡导致了今天的分裂局面。一些发行版更改了共享库的 GCC 链接规则,以便按需原则适用于所有库。一些发行版坚持使用旧方式。
为什么即使我同时编译和链接,我仍然会遇到这个问题?
如果我这样做:
$ gcc -o eg1 -I. -L. -lmy_lib eg1.c
当然 gcc 必须先编译 eg1.c
,然后将生成的目标文件与 libmy_lib.a
链接。那么它在进行链接时如何不知道需要目标文件呢?
因为使用单个命令编译和链接不会改变链接顺序的顺序。
当您运行上述命令时,gcc
确定您需要编译 + 链接。所以在幕后,它会生成一个编译命令并运行它,然后生成一个链接命令并运行它,就像 you 运行了这两个命令一样:
$ gcc -I. -c -o eg1.o eg1.c
$ gcc -o eg1 -L. -lmy_lib eg1.o
所以链接会失败,就像你 do 运行这两个命令一样。您在失败中注意到的唯一区别是 gcc 在编译 + 链接情况下生成了一个临时目标文件,因为您没有告诉它使用 eg1.o
。我们看:
/tmp/ccQk1tvs.o: In function `main'
代替:
eg1.o: In function `main':
也可以看看
The order in which interdependent linked libraries are specified is wrong
将相互依赖的库以错误的顺序放置只是一种方式,您可以通过这种方式获取需要在链接中稍后出现的事物定义的文件,而不是提供定义的文件。将库放在引用它们的目标文件之前是犯同样错误的另一种方法。
不支持链接器脚本的 GNU ld 包装器
一些 .so 文件实际上是 GNU ld linker scripts,例如 libtbb.so 文件是具有以下内容的 ASCII 文本文件:
INPUT (libtbb.so.2)
一些更复杂的构建可能不支持这一点。例如,如果您在编译器选项中包含 -v,您可以看到 mainwin gcc wrapper mwdip 在要链接的库的详细输出列表中丢弃链接描述文件命令文件。一个简单的解决方法是替换链接描述文件输入命令文件使用文件的副本(或符号链接),例如
cp libtbb.so.2 libtbb.so
或者您可以将 -l 参数替换为 .so 的完整路径,例如,代替 -ltbb
执行 /home/foo/tbb-4.3/linux/lib/intel64/gcc4.4/libtbb.so.2
与模板成为朋友...
给定带有友元运算符(或函数)的模板类型的代码片段;
template <typename T>
class Foo {
friend std::ostream& operator<< (std::ostream& os, const Foo<T>& a);
};
operator<<
被声明为非模板函数。对于与 Foo
一起使用的每种类型 T
,都需要有一个非模板化的 operator<<
。例如,如果声明了类型 Foo<int>
,则必须有如下操作符实现;
std::ostream& operator<< (std::ostream& os, const Foo<int>& a) {/*...*/}
由于它没有实现,链接器找不到它并导致错误。
要更正此问题,您可以在 Foo
类型之前声明一个模板运算符,然后将适当的实例化声明为友元。语法有点尴尬,但看起来如下;
// forward declare the Foo
template <typename>
class Foo;
// forward declare the operator <<
template <typename T>
std::ostream& operator<<(std::ostream&, const Foo<T>&);
template <typename T>
class Foo {
friend std::ostream& operator<< <>(std::ostream& os, const Foo<T>& a);
// note the required <> ^^^^
// ...
};
template <typename T>
std::ostream& operator<<(std::ostream&, const Foo<T>&)
{
// ... implement the operator
}
上面的代码将操作符的友谊限制在对应的Foo
实例化上,即operator<< <int>
实例化仅限于访问Foo<int>
实例化的私有成员。
替代方案包括;
允许友谊扩展到模板的所有实例化,如下所示; template
或者, operator<< 的实现可以在类定义中内联完成; template
注意,当运算符(或函数)的声明只出现在类中时,该名称不可用于“正常”查找,只能用于参数相关查找,从cppreference开始;
在类或类模板 X 中的友元声明中首先声明的名称成为 X 的最内层封闭命名空间的成员,但不能用于查找(除了考虑 X 的参数相关查找),除非命名空间范围内的匹配声明是假如...
在 cppreference 和 C++ FAQ 有关于模板朋友的进一步阅读。
Code listing showing the techniques above。
作为失败代码示例的旁注; g++ 对此发出警告如下
警告:朋友声明 'std::ostream& operator<<(...)' 声明了一个非模板函数 [-Wnon-template-friend] 注意:(如果这不是您想要的,请确保函数模板已经已声明并在此处的函数名称后添加 <>)
当您的包含路径不同时
当头文件及其关联的共享库(.lib 文件)不同步时,可能会发生链接器错误。让我解释。
链接器如何工作?链接器通过比较函数声明(在头文件中声明)与其定义(在共享库中)匹配它们的签名。如果链接器找不到完全匹配的函数定义,您可能会收到链接器错误。
即使声明和定义似乎匹配,是否仍然可能出现链接器错误?是的!它们在源代码中可能看起来相同,但这实际上取决于编译器看到的内容。基本上你可能会遇到这样的情况:
// header1.h
typedef int Number;
void foo(Number);
// header2.h
typedef float Number;
void foo(Number); // this only looks the same lexically
请注意,即使两个函数声明在源代码中看起来相同,但根据编译器的不同,它们确实不同。
你可能会问,一个人是怎么落到这样的境地的?当然包括路径!如果在编译共享库时,包含路径指向 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(...);
}
繁荣!你得到一个链接器错误,你不知道它为什么会失败。原因是公共库使用相同包含 common_math.h
的不同版本(我在示例中通过包含不同的路径使其很明显,但它可能并不总是那么明显。可能包含路径在编译器设置)。
请注意,在此示例中,链接器会告诉您它找不到 draw()
,而实际上您知道它显然是由库导出的。你可能会花几个小时挠头,想知道出了什么问题。问题是,链接器看到不同的签名,因为参数类型略有不同。在示例中,就编译器而言,vec3
在两个项目中都是不同的类型。这可能是因为它们来自两个略有不同的包含文件(可能包含文件来自两个不同版本的库)。
调试链接器
DUMPBIN 是您的朋友,如果您使用的是 Visual Studio。我确信其他编译器也有其他类似的工具。
过程是这样的:
请注意链接器错误中给出的奇怪的错位名称。 (例如,draw@graphics@XYZ)。将库中导出的符号转储到文本文件中。搜索导出的感兴趣的符号,并注意重命名的名称不同。注意为什么损坏的名称最终会有所不同。您将能够看到参数类型是不同的,即使它们在源代码中看起来相同。他们不同的原因。在上面给出的示例中,它们是不同的,因为包含文件不同。
[1] 项目是指一组源文件,它们链接在一起以生成库或可执行文件。
编辑 1:重写第一部分以便于理解。请在下面发表评论,让我知道是否需要修复其他问题。谢谢!
UNICODE 定义不一致
构建 Windows UNICODE 构建时将 TCHAR
等定义为 wchar_t
等。当不构建时将 UNICODE
定义为构建时将 TCHAR
定义为 char
等。这些 UNICODE
和 _UNICODE
定义影响所有的"T
" string types; LPTSTR
、LPCTSTR
和他们的麋鹿。
构建一个定义了 UNICODE
的库并尝试在未定义 UNICODE
的项目中链接它会导致链接器错误,因为 TCHAR
的定义会不匹配; char
与 wchar_t
。
错误通常包括具有 char
或 wchar_t
派生类型的函数值,这些也可能包括 std::basic_string<>
等。浏览代码中受影响的函数时,通常会引用 TCHAR
或 std::basic_string<TCHAR>
等。这是代码最初用于 UNICODE 和多字节字符 (或“窄”)构建。
要更正此问题,请使用一致的 UNICODE
(和 _UNICODE
)定义构建所有必需的库和项目。
这可以通过任何一个来完成; #define UNICODE #define _UNICODE 或者在项目设置中;项目属性 > 常规 > 项目默认值 > 字符集或在命令行上; /DUNICODE /D_UNICODE
替代方法也适用,如果不打算使用 UNICODE,请确保未设置定义,和/或在项目中使用多字符设置并一致应用。
不要忘记在“发布”和“调试”版本之间保持一致。
清理和重建
构建的“清理”可以去除之前构建、失败构建、不完整构建和其他构建系统相关构建问题留下的“死木”。
通常,IDE 或构建将包含某种形式的“清理”功能,但这可能没有正确配置(例如在手动 makefile 中)或可能失败(例如中间或生成的二进制文件是只读的)。
一旦“清理”完成,验证“清理”是否成功并且所有生成的中间文件(例如自动生成文件)都已成功删除。
这个过程可以看作是最后的手段,但通常是很好的第一步;特别是如果最近添加了与错误相关的代码(本地或来自源存储库)。
const 变量声明/定义中缺少“extern”(仅限 C++)
对于来自 C 语言的人来说,C++ 中的全局变量const
具有内部(或静态)链接可能会令人惊讶。在 C 中情况并非如此,因为所有全局变量都是隐式的 extern
(即当缺少 static
关键字时)。
例子:
// file1.cpp
const int test = 5; // in C++ same as "static const int test = 5"
int test2 = 5;
// file2.cpp
extern const int test;
extern int test2;
void foo()
{
int x = test; // linker error in C++ , no error in C
int y = test2; // no problem
}
正确的是使用头文件并将其包含在 file2.cpp 和 file1.cpp
extern const int test;
extern int test2;
或者,可以使用显式 extern
在 file1.cpp 中声明 const
变量
尽管这是一个很老的问题,有多个已接受的答案,但我想分享如何解决一个晦涩的“未定义引用”错误。
不同版本的库
我使用别名来引用 std::filesystem::path
: filesystem is in the standard library since C++17 but my program needs to also compile in C++14 所以我决定使用变量别名:
#if (defined _GLIBCXX_EXPERIMENTAL_FILESYSTEM) //is the included filesystem library experimental? (C++14 and newer: <experimental/filesystem>)
using path_t = std::experimental::filesystem::path;
#elif (defined _GLIBCXX_FILESYSTEM) //not experimental (C++17 and newer: <filesystem>)
using path_t = std::filesystem::path;
#endif
假设我有三个文件:main.cpp、file.h、file.cpp:
file.h #include 的
file.cpp,file.h的执行,#include的“file.h”
main.cpp #include 的
请注意 main.cpp 和 file.h 中使用的不同库。由于
g++ -g -std=c++17 -c main.cpp
->将 main.cpp 编译为 main.o
$ g++ -g -std=c++17 -c file.cpp
->将 file.cpp 和 file.h 编译为 file.o
$ g++ -g -std=c++17 -o executable main.o file.o -lstdc++fs
->链接 main.o 和 file.o
这样 任何包含在 file.o 中并在 main.o 中使用的 required path_t
函数都会给出“未定义的引用”错误,因为 main.o 引用 std::filesystem::path
但 file.o 引用 std::experimental::filesystem::path
。
解析度
为了解决这个问题,我只需将 file.h 中的
链接共享库时,请确保未隐藏使用的符号。
gcc 的默认行为是所有符号都是可见的。但是,当使用选项 -fvisibility=hidden
构建翻译单元时,只有标有 __attribute__ ((visibility ("default")))
的函数/符号在生成的共享对象中是外部的。
您可以通过调用来检查您正在寻找的符号是否是外部的:
# -D shows (global) dynamic symbols that can be used from the outside of XXX.so
nm -D XXX.so | grep MY_SYMBOL
隐藏/局部符号由 nm
以小写符号类型显示,例如 t
而不是 `T 代码段:
nm XXX.so
00000000000005a7 t HIDDEN_SYMBOL
00000000000005f8 T VISIBLE_SYMBOL
您还可以将 nm
与选项 -C
一起使用来解开名称(如果使用了 C++)。
与 Windows-dll 类似,可以使用定义标记公共函数,例如 DLL_PUBLIC
定义为:
#define DLL_PUBLIC __attribute__ ((visibility ("default")))
DLL_PUBLIC int my_public_function(){
...
}
大致对应于 Windows 的/MSVC 版本:
#ifdef BUILDING_DLL
#define DLL_PUBLIC __declspec(dllexport)
#else
#define DLL_PUBLIC __declspec(dllimport)
#endif
更多 information about visibility 可以在 gcc wiki 上找到。
当使用 -fvisibility=hidden
编译翻译单元时,生成的符号仍然具有外部链接(以 nm
显示的大写符号类型)并且如果目标文件成为静态库的一部分,则可以毫无问题地用于外部链接。仅当目标文件链接到共享库时,链接才成为本地链接。
要查找目标文件中的哪些符号是隐藏的,请运行:
>>> objdump -t XXXX.o | grep hidden
0000000000000000 g F .text 000000000000000b .hidden HIDDEN_SYMBOL1
000000000000000b g F .text 000000000000000b .hidden HIDDEN_SYMBOL2
函数或类方法在源文件中使用 inline 说明符定义。
一个例子:-
主文件
#include "gum.h"
#include "foo.h"
int main()
{
gum();
foo f;
f.bar();
return 0;
}
富.h (1)
#pragma once
struct foo {
void bar() const;
};
口香糖.h (1)
#pragma once
extern void gum();
foo.cpp (1)
#include "foo.h"
#include <iostream>
inline /* <- wrong! */ void foo::bar() const {
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
口香糖.cpp (1)
#include "gum.h"
#include <iostream>
inline /* <- wrong! */ void gum()
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
如果您在其定义中指定 gum
(类似地,foo::bar
)为 inline
,则编译器将内联 gum
(如果它选择),通过:-
没有发出任何独特的口香糖定义,因此
不发出链接器可以引用口香糖定义的任何符号,而是
用已编译的 gum 主体的内联副本替换所有对 gum 的调用。
因此,如果您在源文件 gum.cpp
中内联定义 gum
,它会被编译为目标文件 gum.o
,其中对 gum
的所有调用都是内联的,并且没有定义链接器可以引用的符号到 gum
。当您将 gum.o
与另一个目标文件(例如 main.o
引用外部符号 gum
)一起链接到程序中时,链接器无法解析这些引用。所以链接失败:
编译:
g++ -c main.cpp foo.cpp gum.cpp
关联:
$ g++ -o prog main.o foo.o gum.o
main.o: In function `main':
main.cpp:(.text+0x18): undefined reference to `gum()'
main.cpp:(.text+0x24): undefined reference to `foo::bar() const'
collect2: error: ld returned 1 exit status
如果编译器可以在每个可能调用 gum
的源文件中看到它的定义,那么您只能将 gum
定义为 inline
。这意味着它的内联定义需要存在于一个 header 文件中,您在编译的每个可能调用 gum
的源文件中include 该文件。做以下两件事之一:
要么不内联定义
从源文件定义中删除 inline
说明符:
foo.cpp (2)
#include "foo.h"
#include <iostream>
void foo::bar() const {
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
口香糖.cpp (2)
#include "gum.h"
#include <iostream>
void gum()
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
重建:
$ g++ -c main.cpp foo.cpp gum.cpp
imk@imk-Inspiron-7559:~/develop/so/scrap1$ g++ -o prog main.o foo.o gum.o
imk@imk-Inspiron-7559:~/develop/so/scrap1$ ./prog
void gum()
void foo::bar() const
成功。
或正确内联
头文件中的内联定义:
富.h (2)
#pragma once
#include <iostream>
struct foo {
void bar() const { // In-class definition is implicitly inline
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
};
// Alternatively...
#if 0
struct foo {
void bar() const;
};
inline void foo::bar() const {
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
#endif
口香糖.h (2)
#pragma once
#include <iostream>
inline void gum() {
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
现在我们不需要 foo.cpp
或 gum.cpp
:
$ g++ -c main.cpp
$ g++ -o prog main.o
$ ./prog
void gum()
void foo::bar() const
inline
唯一保证的效果是它使定义文件成为静态的。 (特别是,inline
不保证实际内联任何内容 ;-)。)
不定期副业成功案例分享
unresolved symbol
编译器错误的另一个可能原因。函数最初在标头中定义为inline
,但我将其更改为声明并在源文件中单独定义。在我从声明和定义中删除inline
关键字之前,此操作失败并出现unresolved symbol
编译器错误。