非导出的虚函数导致在其他项目中派生类中的LNK2001
Non-exported virtual function results in LNK2001 in derived classes in other projects
我发现了这个关于虚函数和 DLL 的小陷阱,我想我会分享我从中学到的东西。
假设您有两个项目,名为 Alpha
和 Bravo
。 Alpha
构建为 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 错误。这也意味着如果 DerivedClass
为 Foo()
提供了一个实现,这个 linker 错误将不会发生,除非该实现尝试调用基础 class 的实现。但是,解决此问题的正确方法是确保导出 class 中的所有虚函数,这些虚函数可能在下游项目中派生了 classes(或者导出 class 本身).
我发现了这个关于虚函数和 DLL 的小陷阱,我想我会分享我从中学到的东西。
假设您有两个项目,名为 Alpha
和 Bravo
。 Alpha
构建为 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()
.
这意味着您将在下游项目中遇到 linker 错误。这也意味着如果 DerivedClass
为 Foo()
提供了一个实现,这个 linker 错误将不会发生,除非该实现尝试调用基础 class 的实现。但是,解决此问题的正确方法是确保导出 class 中的所有虚函数,这些虚函数可能在下游项目中派生了 classes(或者导出 class 本身).