什么是未定义的参考/未解决的外部符号错误?常见原因是什么?如何解决/预防它们?
随意编辑/添加自己的。
什么是未定义的参考/未解决的外部符号错误?常见原因是什么?如何解决/预防它们?
随意编辑/添加自己的。
Answers:
编译C ++程序分2.2 步(分给Keith Thompson供参考)指定,分几个步骤进行:
翻译的语法规则中的优先级由以下阶段指定[请参见脚注]。
- 如有必要,物理源文件字符将以实现定义的方式映射到基本源字符集(为行尾指示符引入换行符)。[SNIP]
- 紧跟换行符的每个反斜杠字符(\)的实例都将被删除,从而将物理源代码行拼接成逻辑源代码行。[SNIP]
- 源文件被分解为预处理令牌(2.5)和空白字符序列(包括注释)。[SNIP]
- 执行预处理指令,扩展宏调用,并执行_Pragma一元运算符表达式。[SNIP]
- 字符文字或字符串文字中的每个源字符集成员,以及字符文字或非原始字符串文字中的每个转义序列和通用字符名称都将转换为执行字符集的相应成员;[SNIP]
- 相邻的字符串文字标记是串联在一起的。
- 分隔标记的空格字符不再重要。每个预处理令牌都将转换为令牌。(2.7)。对生成的令牌进行语法和语义分析,并将其作为翻译单元进行翻译。[SNIP]
- 翻译的翻译单元和实例化单元的组合如下:[SNIP]
- 所有外部实体引用均已解决。库组件被链接以满足对当前翻译中未定义的实体的外部引用。所有此类转换器输出都收集到一个程序映像中,该映像包含在其执行环境中执行所需的信息。(强调我的)
[脚注]尽管实际上不同的阶段可能会折叠在一起,但实现方式必须表现得好像这些单独的阶段一样。
指定的错误发生在编译的最后阶段,通常称为链接。从根本上讲,这意味着您将一堆实现文件编译为目标文件或库,现在想让它们一起工作。
假设你定义的符号a
在a.cpp
。现在,b.cpp
声明该符号并使用它。在链接之前,它只是假定该符号已在某处定义,但它并不关心在何处。链接阶段负责查找符号并将其正确链接到b.cpp
(实际上,实际上是使用该符号的对象或库)。
如果您使用的是Microsoft Visual Studio,则会看到项目生成.lib
文件。它们包含一个导出符号表和一个导入符号表。将根据链接的库解析导入的符号,并为使用该库的库提供导出的符号.lib
(如果有)。
对于其他编译器/平台也存在类似的机制。
常见的错误信息error LNK2001
,error LNK1120
,error LNK2019
对于微软的Visual Studio和undefined reference to
符号名称为GCC。
编码:
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()
为pure:
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
或后写空格-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
未添加到源目录的头文件中的文件匹配的cpp文件也属于缺少定义的类别。
如果库相互依赖,则链接库的顺序确实很重要。通常,如果library A
取决于library 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程序void foo()
中定义,而是在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。
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
文件了。随时添加答案或编辑答案。
这是每个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
项目设置的其他依赖项列表(从中设置)。现在,链接器将报告LNK2019,因为当前搜索范围中不存在该定义。
非专业模板的定义必须对使用它们的所有翻译单位可见。这意味着您无法将模板的定义与实现文件分开。如果必须分开实现,通常的解决方法是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
移至头文件或使用该文件的翻译单元可见的位置。
专用模板可以在实现文件中实现,并且实现不必是可见的,但是必须事先声明专用化。
有关进一步的说明和其他可能的解决方案(显式实例化),请参阅此问题和答案。
未定义的引用WinMain@16
或类似的“异常” main()
入口点引用(尤其是对于视觉工作室)。
您可能错过了使用实际IDE选择正确的项目类型的机会。IDE可能希望将Windows应用程序项目绑定到此类入口点功能(如上面缺少的参考文献中所述),而不是通常使用的int main(int argc, char** argv);
签名。
如果您的IDE支持普通控制台项目,则可能要选择此项目类型,而不是Windows应用程序项目。
需要为新的工具集版本更新Visual Studio NuGet程序包
我只是在尝试将libpng与Visual Studio 2013链接时遇到了问题。问题是该软件包文件仅包含Visual Studio 2010和2012的库。
正确的解决方案是希望开发人员发布更新的程序包然后进行升级,但是它通过修改VS2013的一个额外设置(指向VS2012库文件)为我工作。
我通过在packages
文件内找到packagename\build\native\packagename.targets
该文件并复制所有v110
部分来编辑该包(在解决方案目录内的文件夹中)。我将条件字段中的更改v110
为v120
,只是非常小心地将文件名路径都保留为v110
。这仅允许Visual Studio 2013链接到2012年的库,在这种情况下,它可以工作。
假设您有一个用c ++编写的大型项目,其中包含一千个.cpp文件和一千个.h文件,并且说该项目还依赖于十个静态库。假设我们在Windows上,并且在Visual Studio 20xx中构建了项目。当您按Ctrl + F7 Visual Studio开始编译整个解决方案时(假设我们在解决方案中只有一个项目)
编译的含义是什么?
编译的第二步由Linker完成.Linker应该合并所有目标文件并最终构建输出(可以是可执行文件或库)
链接项目的步骤
error LNK2001: unresolved external symbol "void __cdecl foo(void)" (?foo@@YAXXZ)
观察
如何解决这种错误
编译器时间错误:
链接器时间错误
#pragma once
允许编译器不包括一个头,如果它已经包括在编译当前的.cpp我最近遇到了这个问题,事实证明这是Visual Studio Express 2013中的错误。我必须从项目中删除源文件,然后重新添加它以克服该错误。
如果您认为这可能是编译器/ 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时突然收到链接器错误,那么这将是值得一试的事情。
不支持链接程序脚本的GNU ld包装器
一些.so文件实际上是GNU ld链接程序脚本,例如libtbb.so文件是具有以下内容的ASCII文本文件:
INPUT (libtbb.so.2)
一些更复杂的版本可能不支持此功能。例如,如果在编译器选项中包括-v,则可以看到mainwin gcc包装器mwdip放弃了要链接的库的详细输出列表中的链接器脚本命令文件。一个简单的解决方法是替换链接器脚本输入命令文件,而不是文件副本(或符号链接),例如
cp libtbb.so.2 libtbb.so
或者,您可以将-l参数替换为.so的完整路径,例如,代替-ltbb
do/home/foo/tbb-4.3/linux/lib/intel64/gcc4.4/libtbb.so.2
libfoo
要看libbar
,那么你的正确联动放libfoo
之前libbar
。undefined reference to
一些错误。#include
d 的头文件中声明,
并且实际上在要链接的库中定义。例子在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
而示例2的变体涉及pkg-config
:
$ 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
pkg-config
正确链接示例2的变体:
$ gcc -o eg2 eg2.o $(pkg-config --libs zlib)
$ ./eg2
1.2.8
从这里开始阅读是可选的。
默认情况下,GCC在您的发行版上生成的链接命令会按命令行顺序从左到右使用链接中的文件。当发现文件引用了某个东西 并且不包含该文件的定义时,它将在右边的文件中搜索一个定义。如果最终找到定义,则引用将被解析。如果最后没有解决任何引用,则链接失败:链接器不会向后搜索。
首先,示例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_lib
。因为那时还没有看到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
)链接共享库的上按需的基础。您有其中一个发行版。
这意味着当链接器发现 -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
,程序中没有对任何内容的引用:它们全部位于中eg2.o
,但尚未看到。因此,链接器认为它对没有用libz
。到达时eg2.o
,将其添加到程序中,然后具有对的未定义引用zlibVersion
,链接序列完成;该引用未解决,并且链接失败。
最后,pkg-config
示例2 的变体现在有了明显的解释。展开外壳后:
gcc -o eg2 $(pkg-config --libs zlib) eg2.o
变成:
gcc -o eg2 -lz eg2.o
这只是示例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
指出您需要编译+链接。因此,在幕后,它将生成一个编译命令,然后运行它,然后生成一个链接命令,然后运行它,就像您已经运行了这两个命令一样:
$ gcc -I. -c -o eg1.o eg1.c
$ gcc -o eg1 -L. -lmy_lib eg1.o
所以联动失败只是因为它,如果你不运行这两个命令。您在失败中注意到的唯一区别是gcc在compile + link情况下生成了一个临时目标文件,因为您没有告诉它使用eg1.o
。我们看:
/tmp/ccQk1tvs.o: In function `main'
代替:
eg1.o: In function `main':
将相互依存的库放错顺序只是获得需要定义的文件的一种方法,该文件需要在链接中晚于提供定义的文件。将库放在引用它们的目标文件之前是犯同样错误的另一种方法。
给定带有朋友运算符(或函数)的模板类型的代码段;
template <typename T>
class Foo {
friend std::ostream& operator<< (std::ostream& os, const Foo<T>& a);
};
在operator<<
被宣布为一个非模板函数。对于T
与一起使用的每种类型Foo
,都需要一个非模板的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 <typename T>
class Foo {
template <typename T1>
friend std::ostream& operator<<(std::ostream& os, const Foo<T1>& a);
// ...
};
或者,operator<<
可以在类定义中内联完成的实现;
template <typename T>
class Foo {
friend std::ostream& operator<<(std::ostream& os, const Foo& a)
{ /*...*/ }
// ...
};
注意,当运算符(或函数)的声明仅出现在类中时,该名称不适用于“常规”查找,仅适用于cppreference中依赖于参数的查找;
首先在类或类模板X的朋友声明中声明的名称成为X的最内层封闭名称空间的成员,但不可用于查找(考虑X的依赖于参数的查找除外),除非名称空间范围内的匹配声明为提供...
有关模板朋友的更多信息,请参见cppreference和C ++ FAQ。
附带说明失败的代码示例;g ++警告如下
warning: friend declaration 'std::ostream& operator<<(...)' declares a non-template function [-Wnon-template-friend]
note: (if this is not what you intended, make sure the function template has already been declared and add <> after the function name here)
当头文件及其关联的共享库(.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
(我在示例中通过包含不同的路径使其变得明显,但可能并不总是那么明显。也许在编译器设置中include路径有所不同) 。
请注意,在此示例中,链接器会告诉您找不到draw()
,而实际上您知道它显然已由库导出。您可能要花几个小时挠头想知道问题出在哪里。事实是,链接器看到了不同的签名,因为参数类型略有不同。在示例中,vec3
就编译器而言,两个项目中的类型是不同的。之所以会发生这种情况,是因为它们来自两个略有不同的包含文件(也许包含文件来自库的两个不同版本)。
如果您使用的是Visual Studio,则DUMPBIN是您的朋友。我敢肯定其他编译器也有其他类似的工具。
过程如下:
[1]我所说的项目是指一组链接在一起以生成库或可执行文件的源文件。
编辑1:重写第一部分以便于理解。请在下面发表评论,让我知道是否需要修复其他问题。谢谢!
UNICODE
定义不一致一个Windows UNICODE构建是建立与TCHAR
等被定义为wchar_t
等。如果没有使用建筑UNICODE
定义为构建与TCHAR
定义char
等,这些UNICODE
和_UNICODE
定义影响到所有的“ T
”字符串类型 ; LPTSTR
,LPCTSTR
和他们的麋鹿。
建立一个图书馆 UNICODE
定义的方法并尝试将其链接到未定义的项目中UNICODE
会导致链接器错误,因为定义会不匹配TCHAR
; char
与wchar_t
。
错误通常包括一个具有a char
或wchar_t
派生类型的值的函数,这些也可以包括std::basic_string<>
等。在代码中浏览受影响的功能时,通常会引用TCHAR
或std::basic_string<TCHAR>
等等。这是一个说明符号,表明该代码最初是为UNICODE和多字节字符(或“窄”)构建而设计的。 。
要更正此问题,请使用UNICODE
(和_UNICODE
)的一致定义来构建所有必需的库和项目。
这可以通过以下任一方式完成:
#define UNICODE
#define _UNICODE
或在项目设置中;
项目属性>常规>项目默认值>字符集
或在命令行上;
/DUNICODE /D_UNICODE
如果不打算使用UNICODE,请确保未设置定义,并且/或者在项目中使用了多字符设置并且始终应用了多字符设置,则替代方法也适用。
不要忘记在“发行版”和“调试版”之间保持一致。
const
变量声明/定义中(仅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;
或者,可以const
使用显式声明在file1.cpp中声明变量extern
即使这是一个很老的问题,但有多个可接受的答案,我还是想分享如何解决一个模糊的 “未定义的引用”错误。
我使用别名来引用std::filesystem::path
:自C ++ 17起,文件系统就位于标准库中,但是我的程序也需要在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:
请注意main.cpp和file.h中使用的不同库。由于main.cpp #include < filesystem > 之后的“ file.h ” ,因此使用的文件系统版本为C ++ 17。我以前使用以下命令来编译程序:
$ 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中使用的需要path_t
,因为给“未定义的引用”错误main.o简称std::filesystem::path
但file.o来std::experimental::filesystem::path
。
为了解决这个问题,我只需要将file.h中的<experimental :: filesystem>更改为<filesystem>即可。
gcc的默认行为是所有符号都是可见的。但是,当翻译单元使用option构建时-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
使用结果单元编译编译单元时-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
不同的架构
您可能会看到类似以下的消息:
library machine type 'x64' conflicts with target machine type 'X86'
在这种情况下,这意味着可用的符号用于与您要为其编译的体系结构不同的体系结构。
在Visual Studio上,这是由于错误的“平台”造成的,您需要选择适当的库或安装适当的库版本。
在Linux上,这可能是由于库文件夹错误(使用lib
而不是lib64
例如)造成的。
在MacOS上,可以选择将两个体系结构都存储在同一文件中。链接可能希望两个版本都存在,但只有一个版本存在。提取库的错误lib
/ lib64
文件夹也可能是一个问题。