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


当前回答

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

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

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


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

其他回答

跨模块.dll(编译器特定)错误地导入/导出方法/类。

MSVS要求您使用__declspec(dllexport)和__declsspec(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
{
};

当包含路径不同时

当头文件及其关联的共享库(.lib文件)不同步时,可能会发生链接器错误。让我解释一下。

链接器是如何工作的?链接器通过比较函数声明(在头中声明)和函数定义(在共享库中)的签名来匹配它们。如果链接器找不到完全匹配的函数定义,则可能会出现链接器错误。

即使声明和定义似乎匹配,仍然可能出现链接器错误吗?对它们在源代码中看起来可能相同,但这实际上取决于编译器所看到的内容。基本上,你可能会遇到这样的情况:

// header1.h
typedef int Number;
void foo(Number);

// header2.h
typedef float Number;
void foo(Number); // this only looks the same lexically

注意,即使两个函数声明在源代码中看起来相同,但根据编译器的不同,它们确实不同。

你可能会问,一个人在这样的情况下是如何结束的?当然包括路径!如果在编译共享库时,include路径指向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(...);
}

繁荣你得到了一个链接器错误,你不知道它为什么会失败。原因是公共库使用相同includecommon_math.h的不同版本(我在本例中通过包含不同的路径来说明这一点,但可能并不总是那么明显。可能编译器设置中的包含路径不同)。

注意,在这个例子中,链接器会告诉你它找不到draw(),而实际上你知道它显然是由库导出的。你可以花几个小时挠头,想知道出了什么问题。问题是,链接器看到的签名不同,因为参数类型略有不同。在本例中,就编译器而言,vec3在两个项目中都是不同的类型。这可能是因为它们来自两个稍微不同的包含文件(可能包含文件来自库的两个不同版本)。

调试链接器

如果您正在使用Visual Studio,DUMPBIN是您的朋友。我相信其他编译器也有类似的工具。

过程如下:

注意链接器错误中给出的奇怪的损坏名称。(例如。draw@graphics@XYZ)。将库中导出的符号转储到文本文件中。搜索导出的感兴趣符号,并注意损坏的名称不同。请注意,为什么被弄乱的名字最终会不同。您将能够看到参数类型不同,即使它们在源代码中看起来相同。它们不同的原因。在上面给出的示例中,它们是不同的,因为包含文件不同。

[1] 我所说的项目是指一组链接在一起以生成库或可执行文件的源文件。

编辑1:改写第一节,使其更容易理解。请在下面评论,让我知道是否需要修复其他问题。谢谢

链接的.lib文件与.dll关联

我也有同样的问题。假设我有项目MyProject和TestProject。我已经有效地将MyProject的lib文件链接到TestProject。然而,此lib文件是在构建MyProject的DLL时生成的。此外,我没有包含MyProject中所有方法的源代码,只包含对DLL入口点的访问。

为了解决这个问题,我将MyProject构建为LIB,并将TestProject链接到此.LIB文件(我将生成的.LIB文件复制粘贴到TestProject文件夹中)。然后我可以再次将MyProject构建为DLL。它正在编译,因为TestProject链接到的库确实包含MyProject中类中所有方法的代码。

模板实现不可见。

非专用模板的定义必须对使用它们的所有翻译单位可见。这意味着不能分离模板的定义到实现文件。如果必须分离实现,通常的解决方法是在头的末尾包含一个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的定义移动到头文件或使用它的翻译单元可见的某个位置。

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

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

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

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

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

好的,我们破解它:)

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