非导出的虚函数导致在其他项目中派生类中的LNK2001

Non-exported virtual function results in LNK2001 in derived classes in other projects

我发现了这个关于虚函数和 DLL 的小陷阱,我想我会分享我从中学到的东西。

假设您有两个项目,名为 AlphaBravoAlpha 构建为 DLL,Bravo 引用。现在,在 Alpha 中,你有基础 class:

头文件:(Alpha.h)

#pragma once

#if defined(EXPORT_ALPHA)
#define ALPHA_API __declspec(dllexport)
#else
#define ALPHA_API __declspec(dllimport)
#endif

class BaseClass
{
public:
  ALPHA_API BaseClass();
  ALPHA_API virtual ~BaseClass();

  virtual void Foo();
};

Cpp 文件:(Alpha.cpp)

#include "Alpha.h"
#include <cstdio>

BaseClass::BaseClass() {}
BaseClass::~BaseClass() {}
void BaseClass::Foo()
{
  printf( "Foo\n" );
}

然后,在 Bravo 中,你有派生的 class 和主要的(称之为 main.cpp):

#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;
}

现在,Alpha 构建成功,生成了它的 DLL,然后继续它的快乐之路。但是,你去构建 Bravo,你得到 LNK2001 - unresolved external symbol BaseClass::Foo(),即使你从未真正使用过它。

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

这是由于 table 是如何由 linker 填充的。当你 linking Alpha 时,它既有虚函数的声明,又因为它知道 Foo() 的汇编代码在哪里,它只是填充 BaseClass 的虚函数 table 与汇编代码的地址。但是,由于 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 的虚拟 table。 (它知道是因为编译器在读取包含的 headers 时告诉了它。)因此,首先,linker 查找名为 Foo()@DerivedClass 的已编译函数。没有,因此它会查找名为 Foo()@BaseClass 的已编译函数。但是,静态库没有 Foo()@BaseClass 的条目,因为 Alpha 没有导出它。因此,linker 找不到 Foo()@BaseClass 的任何条目,因此无法使用 Foo().

的函数地址填充 DerivedClass 的虚拟 table

这意味着您将在下游项目中遇到 linker 错误。这也意味着如果 DerivedClassFoo() 提供了一个实现,这个 linker 错误将不会发生,除非该实现尝试调用基础 class 的实现。但是,解决此问题的正确方法是确保导出 class 中的所有虚函数,这些虚函数可能在下游项目中派生了 classes(或者导出 class 本身).