什么是未定义的参考 / 未解决的外部符号错误?什么是常见原因以及如何修复 / 预防它们?
随意编辑 / 添加您自己的。
编译 C ++ 程序分为几个步骤,由2.2指定(Keith Thompson 的信用作为参考) :
翻译语法规则的优先级由以下阶段指定[见脚注] 。
- 如果需要,物理源文件字符以实现定义的方式映射到基本源字符集(引入行尾指示符的换行符)。 [SNIP]
- 删除反斜杠字符(\)后面紧跟一个新行字符的每个实例,拼接物理源代码行以形成逻辑源代码行。 [SNIP]
- 源文件被分解为预处理标记(2.5)和空白字符序列(包括注释)。 [SNIP]
- 执行预处理指令,扩展宏调用,并执行_Pragma 一元运算符表达式。 [SNIP]
- 字符文字或字符串文字中的每个源字符集成员,以及字符文字或非原始字符串文字中的每个转义序列和通用字符名称,都将转换为执行字符集的相应成员; [SNIP]
- 相邻的字符串文字标记是连接的。
- 分隔标记的空白字符不再重要。每个预处理令牌都转换为令牌。 (2.7)。由此产生的标记在语法和语义上进行分析并翻译为翻译单元。 [SNIP]
- 翻译的翻译单元和实例化单元组合如下: [SNIP]
- 解析所有外部实体引用。链接库组件以满足对当前转换中未定义的实体的外部引用。所有这样的翻译器输出被收集到程序映像中,该程序映像包含在其执行环境中执行所需的信息。 (强调我的)
[脚注]实现必须表现得好像发生了这些不同的阶段,尽管实际上不同的阶段可能会折叠在一起。
在编译的最后阶段发生指定的错误,通常称为链接。它基本上意味着你将一堆实现文件编译成目标文件或库,现在你想让它们一起工作。
假设您在a.cpp
定义了符号a
。现在, b.cpp
声明了该符号并使用了它。在链接之前,它只是假设该符号已在某处定义,但它还不关心在哪里。链接阶段负责查找符号并正确地将其链接到b.cpp
(实际上,链接到使用它的对象或库)。
如果您使用的是 Microsoft Visual Studio,您将看到项目生成.lib
文件。它们包含导出符号表和导入符号表。导入的符号将根据您链接的库进行解析,并为使用该.lib
(如果有)的库提供导出的符号。
其他编译器 / 平台也存在类似的机制。
常见错误消息是error LNK2001
, error LNK1120
, Microsoft Visual Studio error LNK2019
和GCC 的 symbolName undefined reference to
。
代码:
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
常见原因包括:
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
可以为类定义中的整数或枚举类型的static
const
数据成员提供初始化程序; 但是,此成员的 odr 使用仍将需要如上所述的命名空间范围定义。 C ++ 11 允许在类中为所有static const
数据成员进行初始化。
通常,每个翻译单元将生成一个目标文件,其中包含该翻译单元中定义的符号的定义。要使用这些符号,您必须链接这些目标文件。
在gcc 下,您将指定要在命令行中链接在一起的所有目标文件,或者一起编译实现文件。
g++ -o test objectFile1.o objectFile2.o -lLibraryName
这里的libraryName
只是库的裸名,没有特定于平台的添加。因此,例如在 Linux 库文件中通常称为libfoo.so
但您只能编写-lfoo
。在 Windows 上,同一个文件可能被称为foo.lib
,但您使用的是相同的参数。您可能必须使用-L‹directory›
添加可以找到这些文件的-L‹directory›
。确保在-l
或-L
之后不写空格。
对于XCode :添加用户标题搜索路径 - > 添加库搜索路径 - > 将实际库引用拖放到项目文件夹中。
在MSVS 下 ,添加到项目中的文件会自动将其目标文件链接在一起,并生成一个lib
文件(通常用法)。要在单独的项目中使用符号,您需要在项目设置中包含lib
文件。这可以在项目属性的 “链接器” 部分中的 “ Input -> Additional Dependencies
。 (应该在Linker -> General -> Additional Library Directories
添加lib
文件的路径)当使用随lib
文件提供的第三方库时,如果不这样做通常会导致错误。
您也可能忘记将文件添加到编译中,在这种情况下,不会生成目标文件。在gcc 中,您将文件添加到命令行。在MSVS 中,将文件添加到项目中将使其自动编译(尽管可以手动地将文件从构建中单独排除)。
在 Windows 编程中,没有链接必要库的__imp_
符号是未解析符号的名称以__imp_
。在文档中查找函数的名称,它应该说明您需要使用哪个库。例如,MSDN 将信息放在每个函数底部的一个框中,名为 “Library”。