什么是未定义的参考/未解析的外部符号错误?常见原因是什么?如何解决/预防?
当前回答
尽管这是一个有多个公认答案的老问题,但我想分享如何解决一个晦涩的“未定义引用”错误。
不同版本的库
我使用别名来引用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>。
其他回答
Visual Studio NuGet包需要更新以获得新的工具集版本
我在尝试将libpng与Visual Studio 2013链接时遇到了这个问题。问题是,包文件只有Visual Studio 2010和2012的库。
正确的解决方案是希望开发人员发布更新的软件包,然后进行升级,但这对我来说是有效的,因为我在VS2013的一个额外设置中进行了黑客攻击,指向了VS2012库文件。
我通过找到packagename\build\native\packagename.targets并在该文件中编辑了包(在解决方案目录中的packages文件夹中),复制了所有v110部分。我在条件字段中将v110更改为v120,只是非常小心地将文件名路径全部保留为v110。这只是允许Visual Studio 2013链接到2012年的库,在本例中,它起了作用。
班级成员:
纯虚拟析构函数需要实现。
声明析构函数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允许在类内初始化所有静态常量数据成员。
已声明但未定义变量或函数。
典型的变量声明是
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)
不匹配的其他示例包括
函数/变量在一个命名空间中声明,在另一个命名空间定义。函数/变量声明为类成员,定义为全局(反之亦然)。函数返回类型、参数编号和类型以及调用约定并不完全一致。
来自编译器的错误消息通常会给出已声明但从未定义的变量或函数的完整声明。将其与您提供的定义进行比较。确保每个细节都匹配。
因为当涉及到链接器错误时,人们似乎都在关注这个问题,所以我将在这里添加这个问题。
GCC 5.2.0中出现链接器错误的一个可能原因是现在默认选择了新的libstdc++库ABI。
如果您在std::__cxx11命名空间或标记[abi:cx11]中获得了有关符号未定义引用的链接器错误,则可能表明您正在尝试将使用_GLIBCXX_USE_cxx11_abi宏的不同值编译的对象文件链接在一起。这通常发生在链接到使用旧版本GCC编译的第三方库时。如果无法使用新的ABI重建第三方库,则需要使用旧的ABI重新编译代码。
因此,如果在5.1.0之后切换到GCC时突然出现链接器错误,这将是一件值得检查的事情。
清理和重建
对构建进行“清理”可以清除以前的构建、失败的构建、不完整的构建和其他与构建系统相关的构建问题可能留下的“枯木”。
一般来说,IDE或构建将包含某种形式的“清理”功能,但这可能未正确配置(例如,在手动生成文件中)或可能失败(例如,中间或生成的二进制文件是只读的)。
一旦“清理”完成,请验证“清理”是否成功,以及所有生成的中间文件(例如自动生成文件)是否已成功删除。
这一过程可以被视为最后的手段,但往往是良好的第一步;特别是如果最近添加了与错误相关的代码(本地或从源存储库)。