什么是未定义的参考/未解决的外部符号错误?什么是常见原因以及如何修复/预防它们?
随意编辑/添加您自己的。
编译C ++程序分为几个步骤,由2.2 (credits to Keith Thompson for the reference)指定:
翻译语法规则的优先级由以下阶段指定[见脚注]。
- 如果需要,物理源文件字符以实现定义的方式映射到基本源字符集(引入行尾指示符的换行符)。 [SNIP]
- 删除反斜杠字符(\)后面紧跟一个换行符的每个实例,拼接物理源代码行以形成逻辑源代码行。 [SNIP]
- 源文件被分解为预处理标记(2.5)和空白字符序列(包括注释)。 [SNIP]
- 执行预处理指令,扩展宏调用,并执行_Pragma一元运算符表达式。 [SNIP]
- 字符文字或字符串文字中的每个源字符集成员,以及字符文字或非原始字符串文字中的每个转义序列和通用字符名称,都将转换为执行字符集的相应成员; [SNIP]
- 相邻的字符串文字标记是连接的。
- 分隔标记的空白字符不再重要。每个预处理令牌都转换为令牌。 (2.7)。由此产生的标记在语法和语义上进行分析并翻译为翻译单元。 [SNIP]
- 翻译的翻译单元和实例化单元组合如下:[SNIP]
- 解析所有外部实体引用。链接库组件以满足对当前转换中未定义的实体的外部引用。所有这样的翻译器输出被收集到程序映像中,该程序映像包含在其执行环境中执行所需的信息。 (强调我的)
[脚注]实现必须表现得好像发生了这些单独的阶段,尽管实际上不同的阶段可能会折叠在一起。
在编译的最后阶段发生指定的错误,通常称为链接。它基本上意味着你将一堆实现文件编译成目标文件或库,现在你想让它们一起工作。
假设您在a
中定义了符号a.cpp
。现在,b.cpp
宣布该符号并使用它。在链接之前,它只是假设该符号已在某处定义,但它还不关心在哪里。链接阶段负责查找符号并正确地将其链接到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
常见原因包括:
#pragma
(Microsoft Visual Studio)UNICODE
definitions这是每个VC ++程序员一次又一次看到的最令人困惑的错误消息之一。让我们先把事情弄清楚。
A.什么是符号?简而言之,符号就是一个名称。它可以是变量名,函数名,类名,typedef名,或者除了那些属于C ++语言的名称和符号之外的任何名称。它由用户定义或由依赖库(另一个用户定义的)引入。
B.什么是外部的?在VC ++中,每个源文件(.cpp,.c等)都被视为转换单元,编译器一次编译一个单元,并为当前转换单元生成一个目标文件(.obj)。 (请注意,此源文件包含的每个头文件都将被预处理,并将被视为此翻译单元的一部分)翻译单元中的所有内容都被视为内部,其他所有内容都被视为外部。在C ++中,您可以使用extern
,__declspec (dllimport)
等关键字引用外部符号。
C.什么是“决心”? Resolve是一个链接时间术语。在链接时,链接器尝试在内部无法找到其定义的目标文件中查找每个符号的外部定义。该搜索过程的范围包括:
此搜索过程称为resolve。
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
的定义移动到头文件或使用它的翻译单元可见的某个位置。
专用模板可以在实现文件中实现,并且实现不必是可见的,但必须事先声明专门化。
有关进一步说明和另一种可能的解决方案(显式实例化),请参阅this question and answer。
未定义引用WinMain@16
或类似的'不寻常'main()
入口点参考(特别是对于visual-studio)。
您可能错过了使用实际IDE选择正确的项目类型。 IDE可能想要绑定例如Windows应用程序投射到这样的入口点函数(如上面缺少的参考中所指定的),而不是常用的int main(int argc, char** argv);
签名。
如果您的IDE支持Plain Console Projects,您可能希望选择此项目类型,而不是Windows应用程序项目。
此外,如果您使用第三方库,请确保您具有正确的32/64位二进制文件
Microsoft提供#pragma
以在链接时引用正确的库;
#pragma comment(lib, "libname.lib")
除了库路径(包括库的目录)之外,这应该是库的全名。
需要针对新工具集版本更新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中,它们已编译)我最近有这个问题,结果发现it was a bug in Visual Studio Express 2013。我不得不从项目中删除一个源文件并重新添加它来克服这个bug。
如果您认为它可能是编译器/ IDE中的错误,请尝试的步骤:
大多数现代链接器都包含一个不同程度打印的冗长选项;
对于gcc和clang;您通常会将-v -Wl,--verbose
或-v -Wl,-v
添加到命令行。更多详情可在这找到;
对于MSVC,/VERBOSE
(特别是/VERBOSE:LIB
)被添加到链接命令行。
/VERBOSE
linker option上的MSDN页面。链接的.lib文件与.dll相关联
我遇到过同样的问题。说我有项目MyProject和TestProject。我已经有效地将MyProject的lib文件链接到了TestProject。但是,这个lib文件是在构建MyProject的DLL时生成的。另外,我没有包含MyProject中所有方法的源代码,只能访问DLL的入口点。
为了解决这个问题,我将MyProject构建为LIB,并将TestProject链接到此.lib文件(我将生成的.lib文件复制粘贴到TestProject文件夹中)。然后,我可以再次构建MyProject作为DLL。它正在编译,因为TestProject链接的lib确实包含MyProject中类中所有方法的代码。
virtual
destructor needs an implementation.声明析构函数pure仍然需要您定义它(与常规函数不同):
struct X
{
virtual ~X() = 0;
};
struct Y : X
{
~Y() {}
};
int main()
{
Y y;
}
//X::~X(){} //uncomment this line for successful definition
发生这种情况是因为在隐式销毁对象时会调用基类析构函数,因此需要定义。
virtual
methods must either be implemented or defined as pure.这与没有定义的非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
class members即使未明确使用,也需要定义某些成员:
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
data members must be defined outside the class in a single translation unit: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 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 linker scripts,例如libtbb.so文件是包含以下内容的ASCII文本文件:
INPUT (libtbb.so.2)
一些更复杂的构建可能不支持这一点。例如,如果在编译器选项中包含-v,则可以看到mainwin gcc wrapper mwdip丢弃了要链接的库的详细输出列表中的链接器脚本命令文件。一个简单的解决方法是将链接器脚本输入命令文件替换为代替文件的副本(或符号链接),例如
cp libtbb.so.2 libtbb.so
或者你可以用.so的完整路径替换-l参数,例如而不是-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<<
被声明为非模板函数。对于与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上有关于模板朋友的进一步阅读。
Code listing showing the techniques above。
作为失败代码示例的旁注; 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)
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
以及涉及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
并发现它指的是静态库./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
进行未定义的引用,链接序列就完成了;该引用未解析,并且链接失败。
最后,示例2的pkg-config
变体现在有一个明显的解释。 shell扩展后:
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':
The order in which interdependent linked libraries are specified is wrong
将相互依赖的库置于错误的顺序只是一种方法,在这种方法中,您可以获得需要在链接中稍后出现的事物定义的文件,而不是提供定义的文件。将库放在引用它们的目标文件之前是另一种造成同样错误的方法。
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,请确保未设置定义,和/或在项目中使用多字符设置并始终如一地应用。
不要忘记在“Release”和“Debug”构建之间保持一致。
当头文件及其关联的共享库(.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
在两个项目中都是不同的类型。这可能是因为它们来自两个略有不同的包含文件(可能包含文件来自两个不同版本的库)。
如果您使用的是Visual Studio,DUMPBIN是您的朋友。我相信其他编译器还有其他类似的工具。
过程如下:
[1]项目是指一组源文件,它们链接在一起以生成库或可执行文件。
编辑1:重写第一部分更容易理解。如果需要修复其他内容,请在下方发表评论以告诉我。谢谢!
构建的“干净”可以移除可能遗留在先前构建,失败构建,不完整构建和其他构建系统相关构建问题的“死木”。
通常,IDE或构建将包括某种形式的“干净”功能,但这可能没有正确配置(例如在手动makefile中)或者可能失败(例如,中间或结果二进制文件是只读的)。
完成“清理”后,验证“clean”是否已成功,并且已成功删除所有生成的中间文件(例如自动化makefile)。
这个过程可以被视为最后的手段,但往往是一个良好的第一步;特别是如果最近添加了与错误相关的代码(本地或从源存储库)。
const
变量声明/定义中缺少“extern”(仅限C ++)对于来自C的人来说,在C ++中,全局const
variables具有内部(或静态)链接可能会令人感到意外。在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中使用的不同库。由于在<filestystem>之后main.cpp #include'd“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的默认行为是所有符号都可见。但是,当使用选项-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类似,可以使用define标记公共函数,例如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
通常,每个翻译单元将生成一个目标文件,其中包含该翻译单元中定义的符号的定义。要使用这些符号,您必须链接这些目标文件。
在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将信息放在每个函数底部的一个框中,名为“Library”。
不同的架构
您可能会看到如下消息:
library machine type 'x64' conflicts with target machine type 'X86'
在这种情况下,这意味着可用符号的结构与您编译的不同。
在Visual Studio上,这是由于错误的“平台”,您需要选择正确的版本或安装适当版本的库。
在Linux上,可能是由于错误的库文件夹(例如使用lib
而不是lib64
)。
在MacOS上,可以选择在同一文件中传送两种体系结构。链接可能需要两个版本,但只有一个版本。这也可能是错误的lib
/ lib64
文件夹中存在库的问题。
典型的变量声明是
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有绑定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();
}
同样地,函数(或变量)void foo()
不是在C程序中定义,而是在C ++中定义,但使用C链接:
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
{
};