为什么Visual Studio在指定外部"C"时无法给出未定义的引用错误?

Why does Visual Studio fail to give an undefined reference error when extern "C" is specified?

本文关键字:未定义 错误 引用 Studio Visual 外部 为什么      更新时间:2023-10-16

给定此代码:

回答 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.hA1.hMyTest.cpp,我制作了一个简单的 DLL 来导出符号,然后告诉编译器与其链接:

// SomeLib.cpp
void __declspec(dllexport) SomeFunc() {}

编译方式:

cl SomeLib.cpp /LD

这将创建SomeLib.dllSomeLib.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.hMyTest.cppTriggerIssue()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()inlined时,如果调用,将生成以下四个符号,如果未调用,则省略:

  • _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()输出还可以优化Fooinline成员函数输出,从而防止产生~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的,因此您可以保证函数的任何和所有定义都是相同的,因此,如果在链接时有任何重复符号,链接器可以简单地将它们丢弃。