什么是未定义的参考/未解析的外部符号错误?常见原因是什么?如何解决/预防?
当前回答
尽管这是一个有多个公认答案的老问题,但我想分享如何解决一个晦涩的“未定义引用”错误。
不同版本的库
我使用别名来引用std::filesystem::path:filesystem自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的<实验::filesystem>,并包含上面的代码file.cpp,file.h的实现,#include的“file.h”main.cpp#include的<文件系统>和“file.h”
注意main.cpp和file.h中使用的不同库。由于main.cpp#在<filesystem>之后包含了“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可执行文件main.o file.o-lsdc++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>。
其他回答
已声明但未定义变量或函数。
典型的变量声明是
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
请注意,您实现的函数与您声明的函数完全匹配。例如,您可能有不匹配的简历限定符:
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)
不匹配的其他示例包括
函数/变量在一个命名空间中声明,在另一个命名空间定义。函数/变量声明为类成员,定义为全局(反之亦然)。函数返回类型、参数编号和类型以及调用约定并不完全一致。
来自编译器的错误消息通常会给出已声明但从未定义的变量或函数的完整声明。将其与您提供的定义进行比较。确保每个细节都匹配。
什么是“未定义的引用/未解析的外部符号”
我将尝试解释什么是“未定义的引用/未解析的外部符号”。
注意:我使用的是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
好的,我们破解它:)
因此,当链接器在对象文件中找不到全局符号时,就会发生“未定义的引用/未解析的外部符号错误”。
在我的例子中,语法是正确的,但当一个类调用同一DLL中的第二个类时,我出现了错误。第二个类的CPP文件在visual studio中具有错误的财产->项类型,在我的例子中,它被设置为C/C++头,而不是正确的C/C++编译器,因此编译器在构建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(){}
指定相互依赖的链接库的顺序是错误的。
如果库相互依赖,则库的链接顺序也很重要。通常,如果库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和clang;通常会在命令行中添加-v-Wl、--verbose或-v-Wl和-v。更多详情请点击此处;
Linux ld手册页。LLVM链接器页。“GCC简介”第9章。
对于MSVC,/VERBOSE(特别是/VERBOSE:LIB)将添加到链接命令行。
/VERBOSE链接器选项上的MSDN页面。