我发现了虚拟函数和DLL的这个小问题,我想我会分享我从中学到的东西。
假设您有两个名为Alpha
和Bravo
的项目。 Alpha
构建为DLL,Bravo
引用。现在,在Alpha
中,您拥有基类:
头文件:(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
中,你有派生类和main(称之为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()
,为什么会生成链接器错误?
答案 0 :(得分:1)
这是由于链接器如何填充虚拟表。当您链接Alpha
时,它同时具有虚函数的声明,并且因为它知道Foo()
的汇编代码的位置,它只是用汇编代码的地址填充BaseClass的虚拟表。 。但是,由于未导出Foo()
,因此它不会将函数的条目添加到相应的lib。因此,例如,如果使用注释编译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
当它转到链接Bravo
时,它知道它需要为DerivedClass的虚拟表添加Foo()
的条目。 (它知道,因为编译器在读取包含的头时告诉它。)因此,首先,链接器查找名为Foo()@DerivedClass
的编译函数。没有一个,所以它寻找一个名为Foo()@BaseClass
的编译函数。但是,静态库没有Foo()@BaseClass
的条目,因为Alpha
没有导出它。因此,链接器找不到Foo()@BaseClass
的任何条目,因此无法使用Foo()
的函数地址填充DerivedClass的虚拟表。
这意味着您将在下游项目中收到链接器错误。这也意味着如果DerivedClass
为Foo()
提供实现,则除非该实现尝试调用基类的实现,否则不会发生此链接器错误。但是,解决此问题的正确方法是确保在可能在下游项目中派生类的类中导出所有虚函数(或者导出类本身)。