2017-02-23 67 views
1

我發現了這個有關虛函數和DLL的小問題,並且認爲我會分享我對它的瞭解。非導出的虛函數導致LNK2001在其他項目的派生類中

假設您有兩個項目,名爲AlphaBravoAlpha被構建爲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

構造函數是內聯的。嘗試刪除構造函數的內聯定義,並在鏈接到DLL的翻譯單元中顯式定義構造函數。在我看來,因爲構造函數是內聯定義的,當Bravo編譯時它會嘗試構建基類,所以它需要對基本虛方法的引用。雖然這看起來應該仍然有效,但虛擬基本方法也是內聯的。 –

+0

@SamVarshavchik是的,但編譯器/鏈接器不需要內聯它,即使添加了inline關鍵字。另外,這些函數是可以內聯的,只是因爲我寫了足夠短的內容才能用作一個沒有額外代碼混亂的例子。 –

+0

我沒有看到編譯器***沒有選項內聯一切。當翻譯單元包含此頭文件時,編譯器無法確定其他翻譯單元是否也會看到相同的類聲明,並因此發出構造函數和虛方法,因爲它需要構造虛函數調度表。因此,在我看來編譯器必須在編譯「Bravo」時嵌入所有內容(這裏,「inline」意思是將函數的代碼作爲翻譯單元的一部分發布,而不是在某個調用點實際內聯)。 –

回答

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,它知道它需要將Foo()的條目添加到DerivedClass的虛擬表。 (它知道是因爲編譯器在讀取包含的頭文件時告訴它。)因此,首先,鏈接程序查找名爲Foo()@DerivedClass的編譯函數。沒有一個,所以它會尋找一個名爲Foo()@BaseClass的編譯函數。但是,靜態庫沒有Foo()@BaseClass的條目,因爲Alpha未導出它。因此,鏈接程序找不到Foo()@BaseClass的任何條目,因此無法用Foo()的函數地址填充DerivedClass的虛擬表。

這意味着您將在下游項目中收到鏈接器錯誤。這也意味着如果DerivedClass提供Foo()的實現,則除非該實現嘗試調用基類的實現,否則不會發生此鏈接器錯誤。但是,解決此問題的正確方法是確保將所有虛擬函數導出到可能在下游項目中具有派生類的類中(或者導出類本身)。

+0

如果DerivedClass實現Foo(),您仍然需要對BaseClass :: Foo的引用作爲構造和銷燬的一部分。 –

+0

@ RaymondChen其實,不,你沒有。 (嘗試將Foo()的實現添加到派生類 - 它會生成很好)。虛擬表在鏈接時填充,如果派生類具有Foo()的實現,則它不需要爲了尋找基類的Foo(),因爲它具有它所需要的實現的地址。所有構造函數都會將vptr設置爲正在實例化的類的虛擬表的地址。 –

+0

我的錯誤。我認爲vtable交換髮生在Derived的析構函數中,這意味着Derived需要能夠訪問BaseClass的vtable。但交換實際上發生在BaseClass的析構函數的開始。 –

相關問題