什么是未定义的参考/未解析的外部符号错误?常见原因是什么?如何解决/预防?


当前回答

班级成员:

纯虚拟析构函数需要实现。

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

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

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

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

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

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

要使其工作,请将X::foo()声明为纯:

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

非虚拟类成员

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

struct A
{ 
    ~A();
};

以下内容将产生错误:

A a;      //destructor undefined

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

struct A
{ 
    ~A() {}
};

或外部:

A::~A() {}

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

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

一个常见的错误是忘记限定名称:

struct A
{
   void foo();
};

void foo() {}

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

定义应为

void A::foo() {}

静态数据成员必须在类外部的单个转换单元中定义:

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

可以为类定义中的整型或枚举类型的静态常量数据成员提供初始值设定项;然而,odr使用这个成员仍然需要如上所述的命名空间范围定义。C++11允许在类内初始化所有静态常量数据成员。

其他回答

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

如果库相互依赖,则库的链接顺序也很重要。通常,如果库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

再说一遍,顺序很重要!

因为当涉及到链接器错误时,人们似乎都在关注这个问题,所以我将在这里添加这个问题。

GCC 5.2.0中出现链接器错误的一个可能原因是现在默认选择了新的libstdc++库ABI。

如果您在std::__cxx11命名空间或标记[abi:cx11]中获得了有关符号未定义引用的链接器错误,则可能表明您正在尝试将使用_GLIBCXX_USE_cxx11_abi宏的不同值编译的对象文件链接在一起。这通常发生在链接到使用旧版本GCC编译的第三方库时。如果无法使用新的ABI重建第三方库,则需要使用旧的ABI重新编译代码。

因此,如果在5.1.0之后切换到GCC时突然出现链接器错误,这将是一件值得检查的事情。

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

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

注意:我使用的是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;
}

and

// 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

Linker看到导出的符号并将其链接起来

// 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

to

    .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:
; ...

我们已经更改了localvarname的可见性,并将其值设置为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)

link

$ g++ src1.o src2.o -o prog

并运行它

$ ./prog 
123456789

好的,我们破解它:)

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

对的未定义引用WinMain@16或类似的“不寻常”main()入口点引用(特别是对于visual studio)。

您可能错过了使用实际IDE选择正确的项目类型。IDE可能希望将例如Windows应用程序项目绑定到这样的入口点函数(如上面缺失的引用中所指定的),而不是通常使用的int main(int argc,char**argv);签名

如果IDE支持普通控制台项目,您可能希望选择此项目类型,而不是windows应用程序项目。


下面是从真实世界问题中更详细地处理的案例1和案例2。

假设您有一个用c++编写的大型项目,它有一千个.cpp文件和一千个.h文件。假设该项目还依赖于十个静态库。假设我们在Windows上,我们在Visual Studio 20xx中构建项目。当您按Ctrl+F7 Visual Studio开始编译整个解决方案时(假设解决方案中只有一个项目)

编译的意义是什么?

Visual Studio搜索文件.vcxproj并开始编译扩展名为.cpp的每个文件。编译顺序未定义。因此,您不能假设首先编译了main.cpp文件如果.cpp文件依赖于其他.h文件来查找符号可以在.cpp文件中定义,也可以不定义如果存在一个.cpp文件,编译器在其中找不到一个符号,则编译器时间错误将引发消息symbol x not be found对于每个扩展名为.cpp的文件,都会生成一个对象文件.o,并且Visual Studio会将输出写入名为ProjectName.cpp.Clean.txt的文件中,该文件包含链接器必须处理的所有对象文件。

编译的第二步是由Linker完成的。Linker应该合并所有对象文件并最终生成输出(可能是可执行文件或库)

链接项目的步骤

分析所有对象文件,找到仅在头文件中声明的定义(例如:前面的答案中提到的类的一个方法的代码,或者初始化类内部的静态变量的事件)如果在目标文件中找不到一个符号,也会在其他库中搜索。对于将新库添加到项目的配置财产->VC++目录->库目录,您在此处指定了用于搜索库的其他文件夹,以及用于指定库名称的配置财产->链接器->输入。-如果链接器找不到您在一个.cpp中编写的符号,则会引发一个链接器时间错误,听起来可能像错误LNK2001:未解析的外部符号“void __cdecl foo(void)”(?foo@@YAXXZ)

观察

一旦链接器找到一个符号,他就不会在其他库中搜索它链接库的顺序很重要。如果Linker在一个静态库中找到一个外部符号,他会在项目的输出中包含该符号。但是,如果库是共享的(动态的),他不会在输出中包含代码(符号),但可能会发生运行时崩溃

如何解决这种错误

编译器时间错误:

确保编写的c++项目语法正确。

链接器时间错误

定义在头文件中声明的所有符号使用#pragma一次,如果编译的当前.cpp中已包含一个标头,则允许编译器不包含该标头确保外部库中不包含可能与头文件中定义的其他符号冲突的符号使用模板时,请确保在头文件中包含每个模板函数的定义,以允许编译器为任何实例化生成适当的代码。