为什么Visual Studio在指定外部"C"时无法给出未定义的引用错误?
Why does Visual Studio fail to give an undefined reference error when extern "C" is specified?
给定此代码:
回答 2.H
_declspec(dllimport) void SomeFunc();
struct Foo
{
Foo();
~Foo();
};
inline Foo::Foo() { }
inline Foo::~Foo()
{
SomeFunc();
}
回答 1.H
#include "A2.h"
extern "C" void TriggerIssue(); // <-- This!
extern "C" inline void TriggerIssue()
{
Foo f;
}
我的测试.cpp
#include "A1.h"
int main()
{
return 0;
}
有关该问题的背景,请参阅此处。
当 MyTest.cpp 编译为可执行文件时,链接器会抱怨SomeFunc()
是未解析的外部。
这似乎是由于一个无关的(错误的?)声明引起的 A1.h 中的触发器问题。注释掉会导致链接器错误消失。
有人可以告诉我这里发生了什么吗?我只是想了解具体是什么导致编译器在存在和不存在该声明的情况下行为不同。上面的代码片段是我尝试为我遇到的场景编写一个最低限度可验证的示例。请不要问我为什么这样写。
反对者注意:这不是关于如何修复未解决的外部符号错误的问题。因此,请停止投票以将其作为重复项关闭。我没有足够的信誉来删除不断出现在这篇文章顶部的链接,声称这个问题"可能有一个可能的答案"。
无论第一个声明如何,问题都存在,如果您注释掉第一个声明并在程序中调用TriggerIssue()
,问题仍然存在。
它是由cl
生成代码以在TriggerIssue()
退出时调用Foo
的析构函数时调用SomeFunc()
引起的,而不是由两个声明之间的任何怪癖或交互引起的。 如果您不注释掉非inline
声明,它会显示的原因是另一个声明告诉编译器您希望它为函数生成一个符号,以便它可以导出到其他模块,这会阻止它实际内联代码,而是强制它生成一个普通函数。 当函数的主体生成时,它以对~Foo()
的隐式调用结束,这是问题的根源。
但是,如果非inline
声明被注释掉,编译器将愉快地将代码视为内联代码,并且只有在您实际调用它时才生成它;由于您的测试程序实际上并没有调用TriggerIssue()
,因此代码永远不会生成,并且永远不会调用~Foo()
;由于析构函数也是inline
的,这允许编译器完全忽略它并且不为其生成代码。 但是,如果在测试程序中插入对TriggerIssue()
的调用,则会看到完全相同的错误消息。
测试 #1:两个声明都存在。
我直接编译了您的代码,将输出管道到日志文件。
cl MyTest.cpp > MyTest.log
生成的日志文件为:
MyTest.cpp
Microsoft (R) Incremental Linker Version 10.00.40219.01
Copyright (C) Microsoft Corporation. All rights reserved.
/out:MyTest.exe
MyTest.obj
MyTest.obj : error LNK2019: unresolved external symbol "__declspec(dllimport) void __cdecl SomeFunc(void)" (__imp_?SomeFunc@@YAXXZ) referenced in function "public: __thiscall Foo::~Foo(void)" (??1Foo@@QAE@XZ)
MyTest.exe : fatal error LNK1120: 1 unresolved externals
测试 2:非inline
声明被注释掉,TriggerIssue()
main()
调用。
我对你的代码做了一些更改:
// A2.h was unchanged.
// -----
// A1.h:
#include "A2.h"
//extern "C" void TriggerIssue(); // <-- This!
extern "C" inline void TriggerIssue()
{
Foo f;
}
// -----
// MyTest.cpp
#include "A1.h"
int main()
{
TriggerIssue();
return 0;
}
我再次编译代码并将结果传送到日志文件,使用与以前相同的命令行:
MyTest.cpp
Microsoft (R) Incremental Linker Version 10.00.40219.01
Copyright (C) Microsoft Corporation. All rights reserved.
/out:MyTest.exe
MyTest.obj
MyTest.obj : error LNK2019: unresolved external symbol "__declspec(dllimport) void __cdecl SomeFunc(void)" (__imp_?SomeFunc@@YAXXZ) referenced in function "public: __thiscall Foo::~Foo(void)" (??1Foo@@QAE@XZ)
MyTest.exe : fatal error LNK1120: 1 unresolved externals
请注意,如果您愿意,两次编译代码的尝试都会导致同一函数中同一符号的相同链接器错误。 这是因为问题实际上是由~Foo()
引起的,而不是TriggerIssue()
;TriggerIssue()
的第一个声明只是通过强制编译器生成~Foo()
代码来暴露它。
[请注意,根据我的经验,Visual C++ 将尝试尽可能安全地优化类,如果该类未实际使用,则拒绝为其inline
成员函数生成代码。 这就是为什么将TriggerIssue()
设置为inline
函数阻止SomeFunc()
被调用的原因:由于没有调用TriggerIssue()
,编译器可以自由地完全优化它,这允许它完全优化~Foo()
,包括对SomeFunc()
的调用。
测试 3:提供外部符号。
使用与测试 2中相同的A2.h
、A1.h
和MyTest.cpp
,我制作了一个简单的 DLL 来导出符号,然后告诉编译器与其链接:
// SomeLib.cpp
void __declspec(dllexport) SomeFunc() {}
编译方式:
cl SomeLib.cpp /LD
这将创建SomeLib.dll
和SomeLib.lib
,以及编译器和链接器使用的其他一些文件。 然后,您可以使用以下内容编译示例代码:
cl MyTest.cpp SomeLib.lib > MyTest.log
这将生成可执行文件和以下日志:
MyTest.cpp
Microsoft (R) Incremental Linker Version 10.00.40219.01
Copyright (C) Microsoft Corporation. All rights reserved.
/out:MyTest.exe
MyTest.obj
SomeLib.lib
解决方案:
若要解决此问题,需要向编译器或链接器提供与从中导入SomeFunc()
DLL 对应的库;如果提供给编译器,它将直接传递给链接器。 例如,如果SomeFunc()
包含在SomeFuncLib.dll
中,则可以使用以下方法进行编译:
cl MyTest.cpp SomeFuncLib.lib
为了说明差异,我成功编译了两次测试代码(每次都略有修改),并在生成的对象文件上使用dumpbin /symbols
。
dumpbin/symbols MyTest.obj > MyTest.txt
示例 1:非inline
声明被注释掉,TriggerIssue()
未调用。
此对象文件是通过注释掉示例代码中的第一个TriggerIssue()
声明生成的,但不以任何方式修改A2.h
或MyTest.cpp
。TriggerIssue()
是inline
,而不是被召唤。
如果未调用该函数,并且允许编译器inline
该函数,则只会生成以下内容:
COFF SYMBOL TABLE
000 00AB9D1B ABS notype Static | @comp.id
001 00000001 ABS notype Static | @feat.00
002 00000000 SECT1 notype Static | .drectve
Section length 2F, #relocs 0, #linenums 0, checksum 0
004 00000000 SECT2 notype Static | .debug$S
Section length 68, #relocs 0, #linenums 0, checksum 0
006 00000000 SECT3 notype Static | .text
Section length 7, #relocs 0, #linenums 0, checksum 96F779C9
008 00000000 SECT3 notype () External | _main
请注意,如果您愿意,生成的唯一函数符号是用于main()
(这是隐式extern "C"
,因此它可以链接到 CRT)。
示例 2:上述测试 3 的结果。
此目标文件是在成功编译上述测试 3后生成的。TriggerIssue()
是inline
,并被称为main()
。
COFF SYMBOL TABLE
000 00AB9D1B ABS notype Static | @comp.id
001 00000001 ABS notype Static | @feat.00
002 00000000 SECT1 notype Static | .drectve
Section length 2F, #relocs 0, #linenums 0, checksum 0
004 00000000 SECT2 notype Static | .debug$S
Section length 68, #relocs 0, #linenums 0, checksum 0
006 00000000 SECT3 notype Static | .text
Section length C, #relocs 1, #linenums 0, checksum 226120D7
008 00000000 SECT3 notype () External | _main
009 00000000 SECT4 notype Static | .text
Section length 18, #relocs 2, #linenums 0, checksum 6CFCDEF, selection 2 (pick any)
00B 00000000 SECT4 notype () External | _TriggerIssue
00C 00000000 SECT5 notype Static | .text
Section length E, #relocs 0, #linenums 0, checksum 4DE4BFBE, selection 2 (pick any)
00E 00000000 SECT5 notype () External | ??0Foo@@QAE@XZ (public: __thiscall Foo::Foo(void))
00F 00000000 SECT6 notype Static | .text
Section length 11, #relocs 1, #linenums 0, checksum DE24CF19, selection 2 (pick any)
011 00000000 SECT6 notype () External | ??1Foo@@QAE@XZ (public: __thiscall Foo::~Foo(void))
012 00000000 UNDEF notype External | __imp_?SomeFunc@@YAXXZ (__declspec(dllimport) void __cdecl SomeFunc(void))
通过比较这两个符号表,我们可以看到,当TriggerIssue()
inline
d时,如果调用,将生成以下四个符号,如果未调用,则省略:
_TriggerIssue
(extern "C" void TriggerIssue()
)??0Foo@@QAE@XZ
(public: __thiscall Foo::Foo(void)
)??1Foo@@QAE@XZ
(public: __thiscall Foo::~Foo(void)
)__imp_?SomeFunc@@YAXXZ
(__declspec(dllimport) void __cdecl SomeFunc(void)
)
如果未生成SomeFunc()
的符号,则链接器不需要链接它,无论它是否已声明。
所以,总结一下:
- 该问题是由
~Foo()
调用SomeFunc()
引起的,当链接器没有任何SomeFunc()
将调用链接到时。 TriggerIssue()
创建Foo
的实例暴露了问题,如果TriggerIssue()
被设为非inline
(通过第一个声明)或在inline
时调用,则会显示问题。- 如果您注释掉
TriggerIssue()
的第一个声明操作并且实际上没有调用它,则问题是隐藏的。 由于您希望函数被内联,并且实际上并未调用它,因此cl
可以自由地完全优化它。 优化TriggerIssue()
输出还可以优化Foo
的inline
成员函数输出,从而防止产生~Foo()
。 这反过来又可以防止链接器抱怨析构函数中的SomeFunc()
调用,因为从未生成要调用SomeFunc()
的代码。
甚至更短:
TriggerIssue()
的第一个声明间接阻止编译器优化对SomeFunc()
的调用。 如果你注释掉该声明,编译器可以自由地优化TriggerIssue()
并完全~Foo()
,这反过来又会阻止编译器生成对SomeFunc()
的调用,允许链接器完全忽略它。
若要修复它,需要提供一个库,link
可以使用该库生成正确的代码以从相应的 DLL 导入SomeFunc()
。
编辑:正如user657267在评论中指出的那样,TriggerIssue()
的第一个声明中暴露问题的具体部分是extern "C"
。 从问题的示例程序开始:
- 如果从两个声明中完全删除了
extern "C"
,并且没有其他任何更改,则编译器将在编译代码时优化TriggerIssue()
(并扩展为~Foo()
),生成与上面示例 1中相同的符号表。 - 如果从两个声明中删除了
"C"
,但函数保留为extern
,并且没有其他任何更改,则链接阶段将失败,生成与测试 1 和 2中相同的日志文件。
这表明extern
声明专门负责防止cl
优化问题代码,方法是强制编译器生成可以在其他模块中外部链接的符号。 如果编译器不需要担心外部链接,它将优化TriggerIssue()
,并通过扩展~Foo()
完全退出完成的程序,从而消除了链接到另一个模块SomeFunc()
的需要。
SomeFunc
在您的程序中使用 ODR,因此定义必须可用,但您尚未提供定义(在此翻译单元中或通过链接在另一个翻译单元中),并且您的程序具有未定义的行为,无需™诊断。
链接器给你一个错误的原因是编译器已经为TriggerIssue
生成了一个定义;当然,奇怪的是,根据额外声明的存在,行为是不同的,你期望它们至少具有相同的行为。撇开 UB 不谈,编译器仍然可以自由选择:函数是inline
的,因此您可以保证函数的任何和所有定义都是相同的,因此,如果在链接时有任何重复符号,链接器可以简单地将它们丢弃。
- 如何修复此错误:未定义对"距离(浮点数,浮点数,浮点数,浮点数,浮点数)"的引用
- 我的项目不会像"undefined reference to `grpc::g_core_codegen_interface'"那样使用未定义的引用错误进行编译
- 对C宏的未定义引用,但在定义它时会出现重新定义错误
- 尝试调用 .h 文件中定义的变量时出现变量未定义错误
- 在C++中使用内联方法时出现未定义的符号错误
- 尝试运行 wasm 函数时出现模块未定义错误
- c++中数组的未定义错误
- 基类未定义.错误 C2504
- 获取线函数未定义错误。无法在字符串中保存可验证的内容
- 对于我的 ComplexNumber 中的某些方法,我得到了一个奇怪的未定义错误引用.cpp,不过我对模板很陌生
- 类类型重定义和基类未定义错误
- C 中的基类未定义错误
- QML[未定义]错误
- 如何构建使用 OpenCV 的 XCode 6 iOS 应用程序 - 未定义错误__cplusplus
- 变量未定义错误
- 静态方法声明但未定义错误c++
- c++ cocos-2d-x未定义错误
- 基类未定义错误(C2504)
- 单例作为模板,未定义错误
- Solaris库与STLport4.6.2链接,出现与ostream相关的未定义错误