什么是未定义的引用/未解决的外部符号错误,我该如何解决?
Posted
技术标签:
【中文标题】什么是未定义的引用/未解决的外部符号错误,我该如何解决?【英文标题】:What is an undefined reference/unresolved external symbol error and how do I fix it? 【发布时间】:2022-01-06 15:35:56 【问题描述】:什么是未定义的引用/未解决的外部符号错误?有哪些常见原因以及如何解决/预防它们?
【问题讨论】:
@LuchianGrigore '随意添加答案'如果您愿意,我更愿意添加相关链接(恕我直言)您的主要答案。 @jave.web:虽然确实发生了这种情况,但程序员通常会注意到他没有this
指针,也无法访问类成员。当非静态成员函数缺少其限定名时,完成编译并且仅在链接期间失败是非常罕见的。
@jave.web:这正是我的问题。谢谢!我是 cpp 的新手,但据我所知,我遇到了 Ben Voigt 所说的非常罕见的确切问题。我认为您的解决方案会是一个很好的答案。
它们可能很有用,就像对标记为过于笼统的问题的许多答案一样。
老实说,我希望看到最小的可重现示例作为我们对大多数新用户的要求。我没有任何意思,只是 - 我们不能指望人们遵守我们没有强加给自己的规则。
【参考方案1】:
需要考虑的一些拼写错误:(作为初学者经常发生在我身上)
如果您使用的是类:检查您是否在定义函数的 cpp 文件中的函数名称前忘记了“classname::”。 如果您使用前向声明:请务必声明正确的类型。例如:如果您想转发声明一个“结构”,请使用“结构”而不是“类”。【讨论】:
【参考方案2】:当你使用错误的编译器来构建你的程序时
如果您使用gcc
或clang
编译器套件,则应根据您使用的语言使用正确的编译器驱动程序。使用g++
或clang++
编译和链接C++ 程序。改用gcc
或clang
会导致对C++ 标准库符号的引用未定义。示例:
$ gcc -o test test.cpp
/usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/../../../../x86_64-pc-linux-gnu/bin/ld: /tmp/ccPv7MvI.o: warning: relocation against `_ZSt4cout' in read-only section `.text'
/usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/../../../../x86_64-pc-linux-gnu/bin/ld: /tmp/ccPv7MvI.o: in function `main': test.cpp:(.text+0xe): undefined reference to `std::cout'
/usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/../../../../x86_64-pc-linux-gnu/bin/ld: test.cpp:(.text+0x13): undefined reference to `std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)'
/usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/../../../../x86_64-pc-linux-gnu/bin/ld: /tmp/ccPv7MvI.o: in function `__static_initialization_and_destruction_0(int, int)':
test.cpp:(.text+0x43): undefined reference to `std::ios_base::Init::Init()'
/usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/../../../../x86_64-pc-linux-gnu/bin/ld: test.cpp:(.text+0x58): undefined reference to `std::ios_base::Init::~Init()'
/usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/../../../../x86_64-pc-linux-gnu/bin/ld: warning: creating DT_TEXTREL in a PIE
collect2: error: ld returned 1 exit status
【讨论】:
【参考方案3】:在我的例子中,语法是正确的,但是当一个类在同一个 DLL 中调用第二个类时,我遇到了这个错误。第二类的 CPP 文件在 Visual Studio 中的 Properties->Item Type
错误,在我的情况下,它设置为 C/C++ Header
而不是正确的 C/C++ compiler
,因此编译器在构建 CPP 文件时没有编译它并导致错误 LNK2019
这是一个示例,假设语法正确,您应该通过更改属性中的项目类型来获得错误
//class A header file
class ClassB; // FORWARD DECLERATION
class ClassA
public:
ClassB* bObj;
ClassA(HINSTANCE hDLL) ;
// member functions
--------------------------------
//class A cpp file
ClassA::ClassA(HINSTANCE hDLL)
bObj = new ClassB();// LNK2019 occures here
bObj ->somefunction();// LNK2019 occures here
/*************************/
//classB Header file
struct mystruct
class ClassB
public:
ClassB();
mystruct somefunction();
------------------------------
//classB cpp file
/* This is the file with the wrong property item type in visual studio --C/C++ Header-*/
ClassB::somefunction()
ClassB::ClassB()
【讨论】:
【参考方案4】:当我们在程序中引用对象名称(类、函数、变量等)并且链接器在尝试搜索时找不到其定义时,会发生“未定义的引用”错误在所有链接的对象文件和库中为它。
因此,当链接器找不到链接对象的定义时,它会发出“未定义引用”错误。从定义中可以清楚地看出,此错误发生在链接过程的后期阶段。导致“未定义引用”错误的原因有多种。
一些可能的原因(更频繁):
#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()”函数。当我们分别编译这些文件时,第一个文件为 print 函数提供了“未定义的引用”,而第二个文件为 main 函数提供了“未定义的引用”。
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) 相关文件未编译
程序员必须确保我们事先编译好项目的所有依赖项,以便在我们编译项目时,编译器找到所有依赖项并编译成功。如果缺少任何依赖项,则编译器会给出“未定义的引用”。
除了上面讨论的原因之外,“未定义引用”错误还可能在许多其他情况下发生。但归根结底是程序员做错了,为了防止这个错误,他们应该被纠正。
【讨论】:
【参考方案5】:我的例子:
头文件
const string GMCHARACTER("character");
class GameCharacter : public GamePart
private:
string name;
static vector<GameCharacter*> characterList;
public:
GameCharacter(cstring nm, cstring id) :
GamePart(GMCHARACTER, id, TRUE, TRUE, TRUE),
name(nm)
...
.cpp 文件:
vector<GameCharacter*> characterList;
...
这产生了“未定义”加载程序错误,因为“characterList”被声明为静态成员变量,但被定义为全局变量。
我添加这个是因为 -- 虽然其他人将此案例列在一长串需要注意的事项中 -- 但该列表没有给出示例。这是一个需要寻找更多东西的示例,尤其是在 C++ 中。
【讨论】:
这个答案没有为这个问题添加合适的信息。您使用非标准宏(?)定义,如 TRUE。因此,在此处搜索答案的用户可能会因您使用向量和继承而不知所措。【参考方案6】:这个问题是通过在头文件中声明函数的原型而发生的:
int createBackground(VertexArray rVA, IntRect arena);
但随后使用带有第一个参数的引用在别处定义函数:
int createBackground(VertexArray& rVA, IntRect arena)
原型没有在第一个参数中使用引用,而定义是,这一事实导致了这个问题。当我将两者都更改为正确匹配包含引用或不包含引用时,问题已解决。
干杯。
【讨论】:
【参考方案7】:按照 2.2 (credits to Keith Thompson for the reference):
的规定,编译 C++ 程序需要几个步骤翻译语法规则的优先级由以下阶段指定[见脚注]。
物理源文件字符以实现定义的方式映射到基本源字符集 (为行尾指示符引入换行符)如果 必要的。 [剪辑] 每个反斜杠字符 (\) 的实例后面紧跟一个换行符都被删除,将物理源代码行拼接到 形成逻辑源代码行。 [剪辑] 源文件分解为预处理标记 (2.5) 和空白字符序列(包括 cmets)。 [剪辑] 执行预处理指令,扩展宏调用,并执行 _Pragma 一元运算符表达式。 [剪辑] 字符文字或字符串文字中的每个源字符集成员,以及每个转义序列和通用字符名称 在字符文字或非原始字符串文字中,转换为 执行字符集的相应成员; [剪辑] 连接相邻的字符串文字标记。 分隔标记的空白字符不再重要。每个预处理令牌都被转换为一个令牌。 (2.7)。这 对生成的标记进行语法和语义分析,并 翻译为翻译单元。 [剪辑] 翻译后的翻译单元和实例化单元组合如下:[SNIP] 所有外部实体引用均已解析。链接库组件以满足对未在 当前翻译。所有这样的翻译输出都被收集到一个 程序映像,其中包含执行所需的信息 执行环境。(强调我的)
[footnote] 实现必须表现得好像这些单独的阶段发生了一样,尽管在实践中不同的阶段可能被折叠在一起。
指定的错误发生在编译的最后阶段,通常称为链接。这基本上意味着您将一堆实现文件编译成目标文件或库,现在您想让它们一起工作。
假设您在a.cpp
中定义了符号a
。现在,b.cpp
声明该符号并使用它。在链接之前,它只是假设该符号是在 somewhere 定义的,但它并不关心在哪里。链接阶段负责找到符号并将其正确链接到b.cpp
(嗯,实际上是到使用它的对象或库)。
如果您使用的是 Microsoft Visual Studio,您会看到项目生成 .lib
文件。其中包含导出符号表和导入符号表。导入的符号将根据您链接的库进行解析,并为使用 .lib
(如果有)的库提供导出的符号。
其他编译器/平台也存在类似机制。
常见的错误消息是error LNK2001
、error LNK1120
、error LNK2019
(用于Microsoft Visual Studio 和undefined reference to
symbolName(用于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
常见原因包括:
Failure to link against appropriate libraries/object files or compile implementation files Declared and undefined variable or function. Common issues with class-type members Template implementations not visible. Symbols were defined in a C program and used in C++ code. Incorrectly importing/exporting methods/classes across modules/dll. (MSVS specific) Circular library dependency undefined reference to `WinMain@16' Interdependent library order Multiple source files of the same name Mistyping or not including the .lib extension when using the#pragma
(Microsoft Visual Studio)
Problems with template friends
Inconsistent UNICODE
definitions
Missing "extern" in const variable declarations/definitions (C++ only)
【讨论】:
就个人而言,我认为 MS 链接器错误消息与 GCC 错误一样可读。它们还具有包含未解析外部名称的损坏和未损坏名称的优点。当您需要直接查看库或目标文件以了解问题可能是什么(例如,调用约定不匹配)时,使用错误名称会很有帮助。另外,我不确定哪个版本的 MSVC 在这里产生了错误,但较新的版本包括引用未解析外部符号的函数的名称(包括损坏和未损坏)。 David Drysdale 写了一篇关于链接器如何工作的精彩文章:Beginner's Guide to Linkers。鉴于这个问题的主题,我认为它可能会很有用。 @TankorSmash 使用 gcc?更准确地说是 MinGW。 @luchian 如果您添加正确的,修复上述错误会很好 我会在列表中添加一个常见案例 - 只是不将文件添加到其中使用的 MSVC 项目中:)【参考方案8】:尽管这是一个很老的问题,但有多个已被接受的答案,我还是想分享一下如何解决晦涩“未定义的引用”错误。
不同版本的库
我使用别名来引用 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:
file.h #include 的 experimental::filesystem> 并包含上面的代码 file.cpp,file.h的实现,#include的“file.h” main.cpp #include 的 filesystem> 和 "file.h"注意 main.cpp 和 file.h 中使用的不同的库。由于 main.cpp #include'd "file.h" 在 filesystem> 之后,使用的文件系统版本是 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 中的 <:filesystem> 更改为
【讨论】:
【参考方案9】:函数或类方法在源文件中使用inline
说明符定义。
一个例子:-
main.cpp
#include "gum.h"
#include "foo.h"
int main()
gum();
foo f;
f.bar();
return 0;
foo.h (1)
#pragma once
struct foo
void bar() const;
;
gum.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;
gum.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
的调用替换为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 文件中,您在每个源文件中 include
您编译其中可能会调用gum
。做以下两件事之一:
不要内联定义
从源文件定义中删除inline
说明符:
foo.cpp (2)
#include "foo.h"
#include <iostream>
void foo::bar() const
std::cout << __PRETTY_FUNCTION__ << std::endl;
gum.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
成功。
或正确内联
头文件中的内联定义:
foo.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
gum.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
【讨论】:
【参考方案10】:不同的架构
您可能会看到如下消息:
library machine type 'x64' conflicts with target machine type 'X86'
在这种情况下,这意味着可用符号用于与您正在编译的架构不同的架构。
在 Visual Studio 上,这是由于“平台”错误,您需要选择正确的平台或安装正确版本的库。
在 Linux 上,这可能是由于库文件夹错误(例如使用 lib
而不是 lib64
)。
在 MacOS 上,可以选择将两种架构放在同一个文件中。可能是该链接希望两个版本都存在,但只有一个版本存在。这也可能是拾取库的错误lib
/lib64
文件夹的问题。
【讨论】:
【参考方案11】:链接共享库时,确保使用的符号没有隐藏。
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 for code-section:
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-version:
#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
【讨论】:
【参考方案12】:未定义对WinMain@16
或类似的引用'unusual' main()
入口点引用(尤其是visual-studio)。
您可能错过了使用实际 IDE 选择正确的项目类型。 IDE 可能想要绑定,例如Windows 应用程序投射到这样的入口点函数(如上面缺少的参考中所指定),而不是常用的int main(int argc, char** argv);
签名。
如果您的 IDE 支持 普通控制台项目,您可能希望选择此项目类型,而不是 Windows 应用程序项目。
这里是case1 和case2 从一个现实世界 问题中得到更详细的处理。
【讨论】:
不得不指出this question,事实上,这更多是因为没有主函数而不是没有WinMain
。有效的 C++ 程序需要 main
。【参考方案13】:
当您的包含路径不同时
当头文件及其关联的共享库(.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:重写第一部分以便于理解。请在下面发表评论,让我知道是否需要修复其他问题。谢谢!
【讨论】:
【参考方案14】:假设您有一个用 c++ 编写的大项目,其中包含一千个 .cpp 文件和一千个 .h 文件。假设该项目还依赖于十个静态库。假设我们在 Windows 上,我们在 Visual Studio 20xx 中构建我们的项目。当您按 Ctrl + F7 Visual Studio 开始编译整个解决方案时(假设我们的解决方案中只有一个项目)
编译是什么意思?
Visual Studio 搜索文件 .vcxproj 并开始编译每个扩展名为 .cpp 的文件。编译顺序是不确定的。所以你不能假设文件 main.cpp 是先编译的 如果 .cpp 文件依赖于其他 .h 文件来查找符号 在文件 .cpp 中可能定义也可能不定义 如果存在编译器在其中找不到一个符号的 .cpp 文件,则编译器时错误会引发消息找不到符号 x 为每个扩展名为 .cpp 的文件生成一个目标文件 .o,Visual Studio 还将输出写入名为 ProjectName.Cpp.Clean.txt 的文件中,该文件包含所有必须为由链接器处理。编译的第二步由链接器完成。链接器应该合并所有目标文件并最终构建输出(可能是可执行文件或库)
链接项目的步骤
解析所有目标文件并找到仅在标题中声明的定义(例如:前面的答案中提到的类的一种方法的代码,或事件初始化静态变量,该变量是类中的成员) 如果在目标文件中找不到某个符号,也会在其他库中搜索该符号。对于向项目添加新库配置属性 -> VC++ 目录 - > Library Directories 在这里您指定了用于搜索库和 Configuration properties -> Linker -> Input 的附加文件夹指定库的名称。 - 如果链接器找不到您在一个 .cpp 中写入的符号,他会引发 链接器时间错误,这听起来像error LNK2001: unresolved external symbol "void __cdecl foo(void)" (?foo@@YAXXZ)
观察
-
一旦链接器找到一个符号,他就不会在其他库中搜索它
链接库的顺序很重要。
如果链接器在一个静态库中找到一个外部符号,他会将符号包含在项目的输出中。但是,如果库是共享的(动态),他不会在输出中包含代码(符号),但 运行时可能会发生崩溃
如何解决此类错误
编译器时间错误:
确保您编写的 C++ 项目语法正确。链接器时间错误
定义您在头文件中声明的所有符号 使用#pragma once
允许编译器不包含一个头文件,如果它已经包含在当前编译的.cpp 中
确保您的外部库不包含可能与您在头文件中定义的其他符号发生冲突的符号
当您使用模板时,请确保在头文件中包含每个模板函数的定义,以允许编译器为任何实例化生成适当的代码。
【讨论】:
您的答案不是特定于视觉工作室吗?该问题未指定任何 IDE/编译器工具,因此它使您的答案对非可视化工作室部分毫无用处。 你是对的。但是每个 IDE 的编译/链接过程都略有不同。但是文件的处理方式完全相同(甚至 g++ 在解析标志时也会做同样的事情..) 问题实际上与 IDE 无关,而与链接问题的答案有关。链接问题与 IDE 无关,而与编译器和构建过程有关。 是的。但是构建/链接过程是在 g++/Visual Studio(Microsoft 为 VS 提供的编译器)/Eclipse/Net Beans 中以相同的方式完成的【参考方案15】:这是每个 VC++ 程序员一次又一次看到的最令人困惑的错误消息之一。让我们先把事情弄清楚。
A.什么是符号? 简而言之,符号就是名称。它可以是一个变量名、一个函数名、一个类名、一个 typedef 名,或者除了那些属于 C++ 语言的名称和符号之外的任何东西。它是用户定义的或由依赖库引入的(另一个用户定义的)。
B.什么是外部?
在VC++中,每个源文件(.cpp、.c等)都被视为一个翻译单元,编译器一次编译一个单元,并为当前翻译单元生成一个目标文件(.obj)。 (请注意,此源文件包含的每个头文件都将被预处理并被视为此翻译单元的一部分)翻译单元内的所有内容都被视为内部内容,其他所有内容都被视为外部内容。在 C++ 中,您可以使用 extern
、__declspec (dllimport)
等关键字来引用外部符号。
C.什么是“解决”? Resolve 是一个链接时间术语。在链接时,链接器尝试为目标文件中无法在内部找到其定义的每个符号查找外部定义。这个搜索过程的范围包括:
编译时生成的所有目标文件 显式或隐式的所有库 (.lib) 指定为此构建应用程序的附加依赖项。这个搜索过程称为解析。
D.最后,为什么是 Unresolved External Symbol? 如果链接器找不到内部没有定义的符号的外部定义,则会报告 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,因为当前搜索范围内不存在该定义。
【讨论】:
【参考方案16】:什么是“未定义的引用/未解析的外部符号”
我将尝试解释什么是“未定义的引用/未解析的外部符号”。
注意:我使用 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 已绑定 GLOBAL(原为 LOCAL)
链接
$ g++ src1.o src2.o -o prog
并运行它
$ ./prog
123456789
好的,我们破解它:)
因此,当链接器在目标文件中找不到全局符号时,会发生“未定义的引用/未解析的外部符号错误”。
【讨论】:
【参考方案17】:
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;
或者,可以使用显式extern
在file1.cpp 中声明const
变量
【讨论】:
【参考方案18】:
UNICODE
定义不一致
Windows UNICODE 构建是使用TCHAR
等定义为wchar_t
等构建的。当不使用UNICODE
构建时定义为使用TCHAR
定义为char
等构建。这些UNICODE
和@ 987654329@定义影响所有"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,请确保未设置定义,和/或在项目中使用多字符设置并始终如一地应用。
不要忘记在“发布”和“调试”版本之间保持一致。
【讨论】:
【参考方案19】:您的链接在引用它们的目标文件之前使用库
您正在尝试使用 GCC 工具链编译和链接您的程序。 您的链接指定了所有必要的库和库搜索路径 如果libfoo
依赖于libbar
,那么您的链接正确地将libfoo
放在libbar
之前。
您的链接失败并出现undefined reference to
something 错误。
但所有未定义的 something 都在您拥有的头文件中声明
#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 生成的链接命令在您的发行版中, 从左到右消耗链接中的文件 命令行序列。当它发现一个文件引用了 something 并且不包含它的定义,将搜索定义 在右侧的文件中。如果最终找到定义,则 参考解决。如果最后有任何引用仍未解决, 链接失败:链接器不向后搜索。
首先,示例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
时,no 对任何内容的引用
节目中:都在eg2.o
,目前还没有看到。所以
链接器决定它对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
发现你想要编译 +
连锁。所以在幕后,它会生成一个编译命令,然后运行
它,然后生成一个链接命令并运行它,就好像 你 已经运行了
两个命令:
$ 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':
另见
The order in which interdependent linked libraries are specified is wrong
以错误的顺序放置相互依赖的库只是一种方法 您可以在其中获取需要定义即将发生的事物的文件 比提供定义的文件更晚。将库放在前面 引用它们的目标文件是犯同样错误的另一种方式。
【讨论】:
【参考方案20】:未能链接到适当的库/目标文件或编译实现文件
通常,每个翻译单元都会生成一个目标文件,其中包含在该翻译单元中定义的符号的定义。 要使用这些符号,您必须链接到这些目标文件。
在 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 文件之前经常这样做)。
【参考方案21】:
与模板交朋友...
给定带有友元运算符(或函数)的模板类型的代码sn-p;
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 <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)
【讨论】:
【参考方案22】:清理并重建
构建的“清理”可以去除之前构建、失败构建、不完整构建和其他构建系统相关构建问题中可能遗留的“死木”。
一般而言,IDE 或构建将包含某种形式的“清理”功能,但这可能没有正确配置(例如在手动 makefile 中)或可能失败(例如中间或生成的二进制文件是只读的)。
“清理”完成后,验证“清理”是否成功并且所有生成的中间文件(例如自动生成文件)都已成功删除。
这个过程可以看作是最后的手段,但通常是很好的第一步;特别是如果最近添加了与错误相关的代码(本地或来自源存储库)。
【讨论】:
【参考方案23】:使用链接器帮助诊断错误
大多数现代链接器都包含一个详细的选项,可以在不同程度上打印出来;
链接调用(命令行), 关于链接阶段包含哪些库的数据, 图书馆的位置, 使用的搜索路径。对于 gcc 和 clang;您通常会将-v -Wl,--verbose
或-v -Wl,-v
添加到命令行。更多细节可以在这里找到;
对于 MSVC,/VERBOSE
(特别是 /VERBOSE:LIB
)被添加到链接命令行中。
/VERBOSE
linker option 上的 MSDN 页面。
【讨论】:
【参考方案24】:跨模块/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
;
【讨论】:
为了完整起见,这个答案应该提到 GCC 的visibility
和 Windows 的 .def
文件,因为这些也会影响符号名称和存在。
@rubenvb 我已经很久没有使用过.def
文件了。随意添加答案或编辑此答案。【参考方案25】:
由于人们似乎在涉及链接器错误时被引导到这个问题,所以我将在此处添加此问题。
GCC 5.2.0 出现链接器错误的一个可能原因是现在默认选择了新的 libstdc++ 库 ABI。
如果您收到有关未定义符号引用的链接器错误,这些符号涉及 std::__cxx11 命名空间或标签 [abi:cxx11] 中的类型,则可能表明您正在尝试将使用不同值编译的目标文件链接在一起对于 _GLIBCXX_USE_CXX11_ABI 宏。当链接到使用旧版本 GCC 编译的第三方库时,通常会发生这种情况。如果第三方库不能用新的 ABI 重建,那么你需要用旧的 ABI 重新编译你的代码。
因此,如果您在 5.1.0 之后切换到 GCC 时突然遇到链接器错误,则需要检查一下。
【讨论】:
【参考方案26】: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 年的库,在这种情况下,它起作用了。
【讨论】:
这似乎过于具体 - 也许一个新线程会是这个答案的更好地方。 @LuchianGrigore:我确实想发布here,因为这个问题正是这个问题,但它被标记为这个问题的重复,所以我无法在那里回答。所以我在这里发布了我的答案。 那个问题已经有一个可接受的答案。它被标记为重复,因为上面列出了一般原因。如果我们在此处为未包含的库的每个问题提供答案,会发生什么? @LuchianGrigore:此问题并非特定于库,它会影响使用 Visual Studio 包管理系统的所有库。我只是碰巧找到了另一个问题,因为我们都遇到了 libpng 的问题。对于 libxml2、libiconv 和 glew,我也有同样的问题(使用相同的解决方案)。这个问题是关于 Visual Studio 的包管理系统的问题,我的回答解释了原因并提供了解决方法。有人刚刚看到“未解决的外部问题”,并认为这是一个标准的链接器问题,而实际上它是一个包管理问题。【参考方案27】:编译器/IDE 中的错误
我最近遇到了这个问题,结果是it was a bug in Visual Studio Express 2013。我不得不从项目中删除一个源文件并重新添加它以克服这个错误。
如果您认为这可能是编译器/IDE 中的错误,请尝试以下步骤:
清理项目(某些 IDE 有执行此操作的选项,您也可以 通过删除目标文件手动完成) 尝试开始一个新项目, 复制原始源代码中的所有源代码。【讨论】:
相信你的工具坏了很可能会让你远离真正的原因。与编译器导致您的问题相比,您犯错误的可能性要大得多。清理您的解决方案或重新创建您的构建配置可能会修复构建错误,但这并不意味着编译器中存在错误。链接的“原来这是一个错误”未经微软确认且不可重现。 @JDiMatteo 这个问题有 21 个答案,因此大量答案不会成为“可能”的解决方案。如果您忽略所有低于您的可能性阈值的答案,那么这个页面实际上变得毫无用处,因为大多数常见案例很容易被发现。【参考方案28】:已声明但未定义变量或函数。
一个典型的变量声明是
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)
其他不匹配的例子包括
在一个命名空间中声明的函数/变量,在另一个命名空间中定义。 函数/变量声明为类成员,定义为全局(反之亦然)。 函数返回类型、参数编号和类型以及调用约定并不完全一致。来自编译器的错误消息通常会为您提供已声明但从未定义的变量或函数的完整声明。将其与您提供的定义进行仔细比较。 确保每个细节都匹配。
【讨论】:
在VS中,匹配头文件#includes
而不是added到源目录的cpp文件也属于缺少定义的类别。【参考方案29】:
不支持链接器脚本的 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
【讨论】:
【参考方案30】:模板实现不可见。
非专业化模板的定义必须对所有使用它们的翻译单元可见。这意味着您不能分离模板的定义
到一个实现文件。如果您必须分离实现,通常的解决方法是在标题末尾包含一个 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。
【讨论】:
以上是关于什么是未定义的引用/未解决的外部符号错误,我该如何解决?的主要内容,如果未能解决你的问题,请参考以下文章