题 什么是未定义的引用/未解析的外部符号错误,如何解决?


什么是未定义的引用/未解析的外部符号错误?什么是常见原因以及如何修复/预防它们?

随意编辑/添加自己的。


1198
2017-09-24 22:27


起源


要考虑添加的一件事是如何处理“未定义的vtable”和“未定义的typeinfo”错误(因为它们不如未定义的函数或变量明显)。 - Jeremiah Willcock
我一直在打标 这个问题 成为这个可能的骗子。但经过你所有的(精彩的)答案后,我看不到这个案例。我知道它具体关于IDE如何设置项目类型以及它的链接依赖性。但这是一个经常被问到的问题,我认为值得覆盖(可能只是链接到另一个适当的欺骗)。如果它已经存在,我只是没有发现它,请忘记这个请求/评论。 - πάντα ῥεῖ
@LuchianGrigore '随意添加答案' 如果您想允许,我更愿意将相关链接(恕我直言)添加到您的主要答案中。 - πάντα ῥεῖ
很常见的错误是您将函数定义为独立函数并忘记了类选择器(例如, A::) 在你的 的.cpp 文件: 你这样做(错误):  void myFunc() { /* do stuff */ }  而不是这个(右):  void A::myFunc() { /* do stuff */ } - jave.web
如果您使用Qt信号发生这种情况,您很可能忘记了Q_OBJECT宏。 - ManuelSchneid3r


答案:


编译C ++程序分几个步骤进行,如下所示 2.2  (以Keith Thompson为参考)

翻译语法规则的优先级由以下阶段指定 [见脚注]

  1. 物理源文件字符以实现定义的方式映射到基本源字符集   (如果,引入行尾指标的换行符)   必要。 [SNIP]
  2. 删除反斜杠字符(\)后面紧跟一个新行字符的每个实例,将物理源行拼接到   形成逻辑源代码行。 [SNIP]
  3. 源文件被分解为预处理标记(2.5)和空白字符序列(包括注释)。 [SNIP]
  4. 执行预处理指令,扩展宏调用,并执行_Pragma一元运算符表达式。 [SNIP]
  5. 字符文字或字符串文字中的每个源字符集成员,以及每个转义序列和通用字符名称   在字符文字或非原始字符串文字中,转换为   执行字符集的相应成员; [SNIP]
  6. 相邻的字符串文字标记是连接的。
  7. 分隔标记的空白字符不再重要。每个预处理令牌都转换为令牌。 (2.7)。该   得到的标记在语法和语义上进行分析   翻译为翻译单位。 [SNIP]
  8. 翻译的翻译单元和实例化单元组合如下: [SNIP]
  9. 解析所有外部实体引用。库组件被链接以满足对未定义的实体的外部引用   目前的翻译。所有这些翻译器输出都被收集到一个   程序映像,包含在其中执行所需的信息   执行环境。 (强调我的)

[脚注] 实现必须表现得好像发生了这些单独的阶段,尽管实际上不同的阶段可能会折叠在一起。

在编译的最后阶段发生指定的错误,通常称为链接。它基本上意味着您将一堆实现文件编译到目标文件或库中,现在您希望它们能够一起工作。

说你定义了符号 a 在 a.cpp。现在, b.cpp  声明 那个符号并用它。在链接之前,它只是假定该符号已定义 某处,但它还没关心在哪里。链接阶段负责查找符号并正确链接到符号 b.cpp (好吧,实际上是使用它的对象或库)。

如果您使用的是Microsoft Visual Studio,则会看到项目生成 .lib 文件。它们包含导出符号表和导入符号表。导入的符号将根据您链接的库进行解析,并为使用该符号的库提供导出的符号 .lib (如果有的话)。

其他编译器/平台也存在类似的机制。

常见错误消息是 error LNK2001error LNK1120error LNK2019 对于 Microsoft 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

常见原因包括:


695
2017-09-24 22:27



@MirroredFate有没有办法获得更好的VS链接器错误?例如,让它看起来更像是gcc。 - TankorSmash
@TankorSmash如果只是。我认为你可以使用修改输出 这个,但我还没试过。 - MirroredFate
就个人而言,我认为MS链接器错误消息与GCC错误一样可读。它们还具有包括未解决的外部的受损和未被篡改的名称的优点。当您需要直接查看库或目标文件以查看问题可能是什么时(例如,调用约定不匹配),具有受损的名称可能会有所帮助。此外,我不确定MSVC的哪个版本在这里产生了错误,但是较新的版本包括引用未解析的外部符号的函数的名称(包括错位和未解码)。 - Michael Burr
大卫德里斯代尔 写了一篇关于链接器如何工作的好文章: 初学者链接指南。鉴于这个问题的主题,我认为它可能有用。 - Pressacco
以及如何定义我的案例在哪里?! stackoverflow.com/questions/32915615/... - Aleksey Kontsevich


班级成员:

一个纯粹的 virtual 析构函数需要实现。

声明析构函数pure仍然需要您定义它(与常规函数不同):

struct X
{
    virtual ~X() = 0;
};
struct Y : X
{
    ~Y() {}
};
int main()
{
    Y y;
}
//X::~X(){} //uncomment this line for successful definition

发生这种情况是因为在隐式销毁对象时会调用基类析构函数,因此需要定义。

virtual 方法必须实现或定义为纯。

这类似于非virtual 没有定义的方法,增加了推理 pure声明生成一个虚拟vtable,你可能会在不使用该函数的情况下得到链接器错误:

struct X
{
    virtual void foo();
};
struct Y : X
{
   void foo() {}
};
int main()
{
   Y y; //linker error although there was no call to X::foo
}

要做到这一点,请声明 X::foo() 纯洁的:

struct X
{
    virtual void foo() = 0;
};

非-virtual 班级成员

即使未明确使用,也需要定义某些成员:

struct A
{ 
    ~A();
};

以下将产生错误:

A a;      //destructor undefined

在类定义本身中,实现可以是内联的:

struct A
{ 
    ~A() {}
};

或外面:

A::~A() {}

如果实现在类定义之外,但在标题中,则必须将方法标记为 inline 防止多重定义。

如果使用,则需要定义所有使用的成员方法。

一个常见的错误就是忘记了这个名字的资格:

struct A
{
   void foo();
};

void foo() {}

int main()
{
   A a;
   a.foo();
}

定义应该是

void A::foo() {}

static 数据成员必须在类外定义 单翻译单位

struct X
{
    static int x;
};
int main()
{
    int x = X::x;
}
//int X::x; //uncomment this line to define X::x

可以为a提供初始化程序 static  const 类定义中的整数或枚举类型的数据成员;但是,该成员的odr-use仍然需要如上所述的命名空间范围定义。 C ++ 11允许在类中为所有人进行初始化 static const数据成员。


149
2017-09-24 23:38



这个答案的最后一行是不正确的,一个类内声明永远不是一个定义。 (对于非常用的静态成员,不需要定义,这对于整数编译时常量是常见的) - Ben Voigt
不确定这部分 - C ++ 11允许在类中为所有人进行初始化 static const 数据成员 是正确的。 [class.static.data] / 3表示您需要标记静态数据成员 constexpr 如果他们不是整体或枚举类型。 - Praetorian
无需定义任何您从未使用过的非虚函数。此外,如果您从未构造类的对象,也不需要定义任何虚函数,也不需要从实际实例化的派生类中调用它。此外,可以定义所有纯虚函数。 - Deduplicator
@Deduplicator“需要”与“应该”。小心翼翼地,需要定义非纯虚函数(尽管如上所述,一些编译器在你调用它们之前不会抱怨,但有些会这样做)。我不认为我说你不能定义纯虚拟。 - Luchian Grigore
@Deduplicator参见“在类中声明的虚函数应该在该类中定义或声明为纯(10.4),或两者都有;但不需要诊断”(10.3虚函数) - 除非在C ++中更改了 - Luchian Grigore


无法链接到适当的库/目标文件或编译实现文件

通常,每个翻译单元将生成一个目标文件,其中包含该翻译单元中定义的符号的定义。 要使用这些符号,您必须链接这些对象文件。

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”。


98
2017-09-24 23:37





声明但未定义变量或函数。

典型的变量声明是

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)

其他不匹配的例子包括

  • 在一个命名空间中声明的函数/变量,在另一个命
  • 声明为类成员的函数/变量,定义为全局(反之亦然)。
  • 函数返回类型,参数号和类型以及调用约定都不完全一致。

来自编译器的错误消息通常会为您提供已声明但从未定义的变量或函数的完整声明。将其与您提供的定义进行比较。 确保每个细节都匹配。


92
2017-09-24 23:38



@Raymond我遗漏了函数名拼写错误,因为它很明显。至于参数名称 - 什么? - Luchian Grigore
人们确实会问 由于拼写错误的名字而导致未解决的外部因素,所以这并不完全是显而易见的。 (不确定您所指的参数名称。参数名称不属于该类型。) - Raymond Chen
@Raymond陈已经涵盖了 stackoverflow.com/a/12574420/673730 - Luchian Grigore
在VS中,cpp文件与标头中的文件匹配 #includes 不 添加 到源目录也属于缺少定义的类别。 - Laurie Stearn


指定相互依赖的链接库的顺序是错误的。

库链接的顺序如果库彼此依赖则很重要。一般来说,如果是库 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

所以再次重复,顺序 DOES 物!


74
2017-07-10 11:46



我很奇怪的事实是,在我的情况下,我有一个依赖于共享库的目标文件。我不得不修改Makefile并放入库 后 在Debian上使用gcc 4.8.4的对象。在带有gcc 4.4的Centos 6.5上,Makefile工作没有问题。 - Marco Sulla
-Wl, - start-group .....- Wl, - end-group解决了这个问题。 - user2672165


什么是“未定义的引用/未解析的外部符号”

我将尝试解释什么是“未定义的引用/未解析的外部符号”。

注意:我使用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

好的,我们破解它:)

因此,当链接器无法在目标文件中找到全局符号时,会发生“未定义的引用/未解析的外部符号错误”。


64
2017-09-24 23:39





符号在C程序中定义,并在C ++代码中使用。

功能(或变量) 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"
}

61
2017-12-03 18:11



或者相反,如果您开发了一个C库,一个不错的规则就是通过包含所有导出的声明来保护头文件。 #ifdef __cplusplus [\n] extern"C" { [\n] #endif 和 #ifdef __cplusplus [\n] } [\n] #endif ([\n] 真正的回车,但我不能在评论中写得恰到好处)。 - Bentoy13
如上面的评论所示,“创建混合语言标题”部分有助于: oracle.com/technetwork/articles/servers-storage-dev/... - zanbri


如果所有其他方法都失败,请重新编译。

我最近只能通过重新编译有问题的文件来摆脱Visual Studio 2012中未解决的外部错误。当我重建时,错误就消失了。

当两个(或更多)库具有循环依赖性时,通常会发生这种情况。库A尝试在B.lib和库B中使用符号尝试使用来自A.lib的符号。两者都不存在。当您尝试编译A时,链接步骤将失败,因为它找不到B.lib。将生成A.lib,但不会生成dll。然后编译B,它将成功并生成B.lib。现在可以重新编译A,因为现在可以找到B.lib。


59
2017-09-24 23:39



正确 - 当库具有循环依赖时会发生这种情况。 - Luchian Grigore
我扩展了你的答案,并在主要答案中链接。谢谢。 - Luchian Grigore


跨modules / 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
{
};

51
2017-09-24 23:38



要完成,这个答案应该提到GCC visibility 和Windows' .def 文件,因为它们也会影响符号名称和存在。 - rubenvb
@rubenvb我还没用过 .def 多年的文件。随意添加答案或编辑此答案。 - Luchian Grigore


模板实现不可见。

非专业化模板必须使其定义对使用它们的所有翻译单元都可见。这意味着您无法分离模板的定义 到一个实现文件。如果必须将实现分开,通常的解决方法是使用 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 到头文件或使用它的翻译单元可见的某个地方。

专用模板可以在实现文件中实现,并且实现不必是可见的,但必须事先声明专门化。

有关进一步说明和另一种可能的解决方案(显式实例化),请参阅 这个问题和答案


49
2018-04-13 16:42



有关“模板必须在标题中定义”的更多信息,请参阅 stackoverflow.com/questions/495021 - PlasmaHH