C++ 未导出的虚拟函数在其他项目的派生类中导致LNK2001

C++ 未导出的虚拟函数在其他项目的派生类中导致LNK2001,c++,visual-c++,dll,linker-errors,C++,Visual C++,Dll,Linker Errors,我发现了这个关于虚拟函数和DLL的小把戏,并想与大家分享我对它的了解 假设您有两个项目,分别称为Alpha和BravoAlpha构建为DLL,Bravo引用它。现在,在Alpha中,您有了基类: 头文件:(Alpha.h) Cpp文件:(Alpha.Cpp) 现在,Alpha成功构建,生成了它的DLL,并继续它的快乐之路。然后,您进入buildBravo,您将得到LNK2001-未解析的外部符号基类::Foo(),即使您从未实际使用过它 那么,发生了什么?如果我们从未调用Foo(),为什么会生成

我发现了这个关于虚拟函数和DLL的小把戏,并想与大家分享我对它的了解

假设您有两个项目,分别称为
Alpha
Bravo
Alpha
构建为DLL,
Bravo
引用它。现在,在
Alpha
中,您有了基类:

头文件:(Alpha.h)

Cpp文件:(Alpha.Cpp)

现在,
Alpha
成功构建,生成了它的DLL,并继续它的快乐之路。然后,您进入build
Bravo
,您将得到
LNK2001-未解析的外部符号基类::Foo()
,即使您从未实际使用过它


那么,发生了什么?如果我们从未调用
Foo()
,为什么会生成链接器错误?

这是由于链接器如何填充虚拟表造成的。当您链接
Alpha
时,它既有虚拟函数的声明,又知道
Foo()
的汇编代码在哪里,因此它只是用汇编代码的地址填充基类的虚拟表。但是,由于未导出
Foo()
,因此它不会将函数的条目添加到相应的库中。因此,例如,如果使用注释编译DLL和静态库,它们可能会如下所示:

Alpha.dll:

# this is BaseClass's virtual table, located at some random address only known internally
0x00002000 # Function address of ~BaseClass()
0x00004000 # Function address of Foo()

# This is the machine code for Foo(), located at address 0x00004000
mov eax, [ebx]
add eax, ecx
...
Alpha.lib:

# Exports:
BaseClass()@BaseClass  : 0x00001000 # Address in the DLL of the constructor
~BaseClass()@BaseClass : 0x00002000 # Address in the DLL of the destructor
当它转到link
Bravo
时,它知道需要将
Foo()
的条目添加到DerivedClass的虚拟表中。(它知道,因为编译器在读取包含的头时告诉它。)因此,首先,链接器查找名为
Foo()@DerivedClass
的编译函数。没有,因此它会查找名为
Foo()@BaseClass
的编译函数。但是,静态库没有
Foo()@BaseClass
的条目,因为
Alpha
没有导出它。因此,链接器找不到
Foo()@BaseClass
的任何条目,因此无法使用
Foo()
的函数地址填充DerivedClass的虚拟表


这意味着您将在下游项目中获得链接器错误。这还意味着,如果
DerivedClass
Foo()
提供了一个实现,则除非该实现尝试调用基类的实现,否则不会发生此链接器错误。但是,解决此问题的正确方法是确保导出一个类中的所有虚拟函数,该类可能在下游项目中具有派生类(或者导出该类,或者导出该类本身)。

构造函数是内联的。尝试删除构造函数的内联定义,并在链接到DLL的转换单元中显式定义构造函数。在我看来,因为构造函数是内联定义的,所以当编译Bravo时,它会尝试构建基类,因此它需要对基虚拟方法的引用。虽然这似乎仍然可以工作,但虚拟基方法也是内联的。@SamVarshavchik是的,但是编译器/链接器不需要内联它,即使您添加了内联关键字。另外,这些函数之所以能够内联,是因为我编写的函数足够短,可以用作没有多余代码的示例。我看不出编译器可以不内联所有函数。当翻译单元包含此头文件时,编译器无法确定其他翻译单元是否也会看到相同的类声明,因此会发出构造函数和虚拟方法,因为它是构造虚拟分派表所必需的。因此,在我看来,编译器在编译“Bravo”时必须内联所有内容(这里,“内联”意味着将函数的代码作为翻译单元的一部分发出,而不是在某个调用点进行实际内联)。谢谢你指出错误。等等,你是说显示的代码实际上并没有再现编译错误,只是代表了实际失败的代码吗?如果是这样,这将是由于未能提供详细信息而结束此问题的理由。仅仅“解释”有问题的代码是不够的。如果DerivedClass实现了Foo(),您仍然需要对BaseClass::Foo的引用来作为构造和销毁的一部分。@RaymondChen实际上,不,您不需要。(尝试将Foo()的实现添加到派生类中,这样可以很好地构建。)虚拟表在链接时填充,如果派生类有Foo()的实现,那么它就不需要费心寻找基类的Foo(),因为它有它需要的实现的地址。构造函数所做的就是将vptr设置为实例化类的虚拟表地址。我的错误。我认为vtable交换发生在派生的析构函数中,这意味着派生需要能够访问基类的vtable。但是交换实际上发生在基类的析构函数的开头。
#include "Alpha.h"
#include <cstdio>

class DerivedClass : public BaseClass
{
public:
  DerivedClass() : BaseClass() {}
  virtual ~DerivedClass() {}
};

int main()
{
  DerivedClass* derived = new DerivedClass();
  printf( "Created instance of derived class.\n" );
  delete derived;
  return 0;
}
# this is BaseClass's virtual table, located at some random address only known internally
0x00002000 # Function address of ~BaseClass()
0x00004000 # Function address of Foo()

# This is the machine code for Foo(), located at address 0x00004000
mov eax, [ebx]
add eax, ecx
...
# Exports:
BaseClass()@BaseClass  : 0x00001000 # Address in the DLL of the constructor
~BaseClass()@BaseClass : 0x00002000 # Address in the DLL of the destructor