[轉]從普通DLL中導出C++類 – dllexport和dllimport的使用方法(中英對照、附注解)


 
這幾天寫幾個小程序練手,在准備將一個類導出時,發現還真不知道如果不用MFC的擴展DLL,是怎么導出的。但我知道dllexport可以導出函數和變量,而且MFC擴展DLL就算是使用了MFC的功能,但能否導出類應該也不是必須用MFC才能夠做到,一定是有相應的機制可以實現。於是查了一下MSDN,發現這個機制簡單的可怕,原來就和導出函數一樣,把dllexport關鍵字加到類名前就可以了。估計和我一樣的同學大有人在,把MSDN的相關文檔翻譯出來,附上我的注解,希望對大家有用。

評注程序均在Visual Studio.NET 2005 Professional中測試以得到結果,給出的代碼均省略了大量語句,只保留了與主題最相關的部分,因此可能無法編譯。
MSDN文檔選自2007年4月更新的MSDN Library。
Using dllimport and dllexport in C++ Classes
在C++類中使用dllimport和dllexport
Microsoft Specific
You can declare C++ classes with the dllimport or dllexport attribute. These forms imply that the entire class is imported or exported. Classes exported this way are called exportable classes.
The following example defines an exportable class. All its member functions and static data are exported:
可以在聲明C++類時使用dllimport和dllexport屬性。這兩個形式將隱含導入或導入整個類。通過這種方法導出的類稱為可導出類。
下列范例定義了一個可導出類,其所有的成員函數和靜態將被導出:
#define DllExport   __declspec( dllexport )
 
class DllExport C {
   int i;
   virtual int func( void ) { return 1; }
};
 
Note that explicit use of the dllimport and dllexport attributes on members of an exportable class is prohibited.
注意,禁止在一個可導出類的成員上顯式的使用dllimport和dllexport屬性。
balon注:如你不能像下例這樣寫:
#define DllExport   __declspec( dllexport )
 
class DllExport C {
   DllExport int i;    // 不可以在成員上使用dllexport
   DllExport int func( void ) { return 1; } // 不可以在成員上使用dllexport
}; 

 
dllexport Classes
通過dllexport導出類
When you declare a class dllexport, all its member functions and static data members are exported. You must provide the definitions of all such members in the same program. Otherwise, a linker error is generated. The one exception to this rule applies to pure virtual functions, for which you need not provide explicit definitions. However, because a destructor for an abstract class is always called by the destructor for the base class, pure virtual destructors must always provide a definition. Note that these rules are the same for nonexportable classes.
If you export data of class type or functions that return classes, be sure to export the class.
當你聲明一個類為dllexport,其所有的成員函數和靜態數據成員將被導出。你必須在同一個程序中定義所有此類成員,否則會產生一個鏈接錯誤。
balon注:如在下面的導出類中,func必須在這個DLL的工程中或是在使用DLL的程序中被定義,否則在使用時會出現無法解析的外部符號的鏈接錯誤。注意,定義可以放在DLL中,也可放在DLL的使用者工程中,只要在使用前被定義就可以。
方法一:在Dll工程中定義,在使用者工程中直接使用。
// dll.h
#define DllExport   __declspec( dllexport )
class DllExport C {
    int func( void );
}; 
// dll.cpp
int C::func(void)
{
    return 1;

 
方法二:在DLL工程中只有一個聲明,沒有定義。可以在使用者使用前給出該函數的定義。另外,如果你在使用者程序中根本就沒有用到func,可以不提供其定義,不會出鏈接錯誤。
// dll.h
#define DllExport   __declspec( dllexport )
class DllExport C {
    int func( void );
};  // client.cpp
int C::func(void)
{
    return 1;
}
 
int main()
{
    C c;
    c.func();    // ok
    return 1;
}

這個規則的唯一例外是對純虛函數你可以不用提供顯示的定義。
balon注:如下面例子中的純虛函數func,可以不必提供定義。因為這個導出的類可能本身就是一個抽象類,func就是一個沒有實現的函數,當然可以不提供定義。

但是,由於抽象類的析構函數總是會被基類的析構函數調用的,因此純虛析構函數必須提供一個定義。這條規則對於不可導出的類同樣適用。
balon注:根據我的試驗結果,將各種情況下的編譯結果列了一個表:
  DLL工程中沒有對應函數定義的編譯結果 客戶程序使用情況 
成員函數 正常鏈接。 可以定義此類的實例,不調用此成員函數就正常鏈接;調用此成員函數鏈接出錯,提示此函數為未決的外部符號。 
虛成員函數 鏈接出錯,提示此函數為未決的外部符號。 - 
純虛成員函數 正常鏈接。 不能定義此類實例,編譯出錯,提示無法實例化一個抽象類。 
析構函數 正常鏈接。 鏈接出錯,提示析構函數為未決的外部符號 
虛析構函數 鏈接出錯,提示析構函數為未決的外部符號。 - 
純虛析構函數 鏈接出錯,提示析構函數為未決的外部符號。 -

可見文檔中說的規則屬實,純虛析構函數被當作虛析構函數對待。但另一個問題是,為什么虛函數(包括析構函數)就一定要在其聲明所在的工程中被定義,而普通函數就不必呢?我分析原因估計如下:
我們知道,C++類如果有虛函數的話(包括其基類有虛函數的情況),C++編譯器會在編譯時為其生成一個虛表,並在構造函數中,將對應的虛表首地址填到類實例的虛表指針成員中。如果一個虛函數在派生體系中被多次實現,虛表中填入的是最底層(most-derived)實現。這個填表過程是,首先基類對象被構造,其構造函數先填入基類的虛表實現入口(這就是為什么,在構造函數中調用虛函數,只會調用到當前類層次的實現,無法調用到派生類的實現的原因),接着派生類被構造,其構造函數將派生類的虛表入口填入,覆蓋掉剛才基類記錄的入口地址。這個過程一直進行,直到整個對象構造完成。
說了這么多,其實其中最關鍵的一點就是:構造函數必須知道當前類的虛表入口地址,而這就需要知道虛表里填寫的所有虛函數的入口地址!否則,編譯器將無法生成構造函數的填虛表的功能。於是,編譯器只好開始抱怨了。
因此,所有的虛函數必須在其聲明所在的工程中有定義,在鏈接時能正確找到其入口地址以便編譯器可以生成構造函數的填虛表功能。

如果你導出一個類類型的數據成員,或是一個返回類的函數,請確保導出那個類。
balon注:這個是一個指導原則,不是一個強制的編譯鏈接要求。只要你不使用返回的類的成員函數,並且返回類沒有虛表,可以不必導出類。但多數情況下是要導出的,所以作為一個好的原則,按MSDN的建議做就好了。在本文的下一篇中將對此有詳細介紹。

dllimport Classes
通過dllimport導入類
When you declare a class dllimport, all its member functions and static data members are imported. Unlike the behavior of dllimport and dllexport on nonclass types, static data members cannot specify a definition in the same program in which a dllimport class is defined.
當你將一個類聲明為dllimport,其所有的成員函數和靜態數據成員將被導入。與在非類類型上使用dllimport和dllexport不同的是,不能在有dllimport類的定義的同一個程序中指給出靜態數據成員的定義。
balon注:這句話譯的有些拗口,原文也好不到哪里。其實看了通過dllexport導出類一節的注解,就好理解這里想要說的意思了。意思就是:靜態數據成員不能像其它成員函數那樣,可以在使用者工程中定義,而不在DLL本身工程中定義。
方法一、按要求在DLL中定義靜態數據成員:
// dll.h
#define DllExport   __declspec( dllexport )
class DllExport C {
    static int x;
}; 
// dll.cpp
int C::x = 0; 
// client.cpp
int main()
{
    C c;
    c.x = 10;    // ok
    return 1;
}
方法二、試圖“在有dllimport類的定義的同一個程序中指給出靜態數據成員的定義”,則在客戶程序編譯時 出現編譯錯誤:
// dll.h
#define DllExport   __declspec( dllexport )
class DllExport C {
    static int x;
};  // client.cpp
int C::x = 0;    // C4273
 
int main()
{
    C c;
    c.x = 10;
    return 1;
}

 
Inheritance and Exportable Classes
繼承與可導出類
All base classes of an exportable class must be exportable. If not, a compiler warning is generated. Moreover, all accessible members that are also classes must be exportable. This rule permits a dllexport class to inherit from a dllimport class, and a dllimport class to inherit from a dllexport class (though the latter is not recommended). As a rule, everything that is accessible to the DLL's client (according to C++ access rules) should be part of the exportable interface. This includes private data members referenced in inline functions.
一個可導出類的所有基類都必須可導出,否則會產生一個編譯錯誤。
balon注:事實上,這條規則也不是一定的,在滿足一定的條件情況下,基類不可導出程序也是正常的。當然,多數情況下還是要導出的,建議還是按MSDN的要求做好了。在本文的下一篇中將對此有詳細介紹。
如下例中,A是一個可導出類,則A的基類Base也應當是一個可導出類。
#define DllExport   __declspec( dllexport )
class DllExport A : public Base
{
    // ...
}; 

 此外,所有可訪問的類類型成員也必須是可導出的。這條規則允許了一個dllexport類派生自一個dllimport類,或一個dllimport類派生自一個dllexport類(盡管后者是不被推薦的)。
balon注:如下例中,Base是一個dllimport類,
// BaseDll的頭文件basedll.h
#define DllImport   __declspec( dllimport )
class DllImport Base
{
};  // DerivedDll的頭文件Deriveddll.h
#include “basedll.h” // 將一個dllimport類聲明包含進來
#define DllExport   __declspec( dllexport )
class DllExport A : public Base // A派生自dllimport類Base
{
    // ...
};

結果就是,這個DLL的客戶可訪問的所有東西(依照C++的訪問規則)都應當是導出接口的一部分。包括被inline函數引用的私有成員。
balon注:這句話其實是全文中最重要的一句話,其實這篇文檔如果把這句話展開說清楚了,也不用我在這里寫這篇文章了。在本文的下一篇中將有對於這句話的深入討論

Selective Member Import/Export
選擇性成員導入導出
Because member functions and static data within a class implicitly have external linkage, you can declare them with the dllimport or dllexport attribute, unless the entire class is exported. If the entire class is imported or exported, the explicit declaration of member functions and data as dllimport or dllexport is prohibited. If you declare a static data member within a class definition as dllexport, a definition must occur somewhere within the same program (as with nonclass external linkage).
因為類中的成員函數和靜態數據隱含進行外部鏈接,你可以在沒有將整個類導出的情況下,在他們聲明中加上dllimport或是dllexport屬性。如果整個類被導入或是導出,將不允許顯式的以dllimport和dllexport對成員函數和數據進行聲明。如果你將類中的一個靜態數據成員聲明為dllexport,在同一個程序中的某個地方應當有它的定義(如同非類外部鏈接那樣)。
balon注:前面幾句很好理解。最后一句實際上是在說,你可以把導出一個靜態數據成員,當作與一個從DLL中導出一個非類成員的普通變量那樣對待,要在DLL所在工程中有定義。導出一個普通變量方法就是在DLL中的某一個CPP文件中定義此變量,並加上dllexport聲明:
// dll.cpp
__declspec( dllexport ) int x = 0; 
那么,對比一下將一個類的靜態數據成員導出的方法:
// dll.h
#define DllExport   __declspec( dllexport )
class A // 注意,這里沒有導出類A
{
public:
    DllExport static int x; // 所以這里才可以導出個別成員
};  // dll.cpp
int A::x = 0;

Similarly, you can declare member functions with the dllimport or dllexport attributes. In this case, you must provide a dllexport definition somewhere within the same program.
類似的,你也可以為一個成員函數聲明加上dllimport或dllexport屬性。這種情況下,你必須在同一個程序中的某處提供dllexport定義。
It is worthwhile to note several important points regarding selective member import and export: 
·         Selective member import/export is best used for providing a version of the exported class interface that is more restrictive; that is, one for which you can design a DLL that exposes fewer public and private features than the language would otherwise allow. It is also useful for fine-tuning the exportable interface: when you know that the client, by definition, is unable to access some private data, you need not export the entire class.
·         If you export one virtual function in a class, you must export all of them, or at least provide versions that the client can use directly.
·         If you have a class in which you are using selective member import/export with virtual functions, the functions must be in the exportable interface or defined inline (visible to the client).
·         If you define a member as dllexport but do not include it in the class definition, a compiler error is generated. You must define the member in the class header.
·         Although the definition of class members as dllimport or dllexport is permitted, you cannot override the interface specified in the class definition.
·         If you define a member function in a place other than the body of the class definition in which you declared it, a warning is generated if the function is defined as dllexport or dllimport (if this definition differs from that specified in the class declaration). 
關於選擇性成員導入導出的一些重點值得我們關注:
·         選擇性成員導入導出最好用在為一個導出的類接口提供一個更具限制的版本;即是說允許你設計一個DLL導出比正常情況下語言允許的更少的公共或私有特性。這對於微調可導出的接口也很有用:如果根據定義,客戶無法訪問一些私有數據,你沒必要導出整個類。
·         如果你導出一個類中的某一個虛函數,那你就必須把所有虛函數一並導出,或至少提供用戶可以直接訪問的版本。
·         如果你在一個類的虛函數上使用了選擇性成員導入導出,那么這些函數必須是在可導出接口中,或是內聯定義(對客戶可見)。
·         如果你將一個成員定義為dllexport,但沒有將定義包含在類的定義中,將產生一個編譯器錯誤。你必須在類的頭文件中定義這個成員。
·         盡管允許將類成員定義為dllimport和dllexport,但你無法覆寫這個類的定義。
·         如果你沒有在聲明成員函數的類體定義處定義一個成員函數,並且此成員函數被定義為dllexport或dllimport,將產生一個警告(如果定義與在類中指定的聲明不同時)。
END Microsoft Specific

 

那么,整個系統的底層機制是怎么樣的?是通過什么途徑,使得我們可以在另一個程序中使用一個DLL中導出的類的呢?
我們知道,要使用一個C++類,必要的條件是在編譯期能得到這個類的頭文件,並在鏈接期可以找到對應的符號的鏈接地址(比如成員函數、靜態數據成員等)。如果這個C++類與你的使用者在同一個工程,那這個條件很好滿足:
首先,C++類的頭文件很好獲得。直接在使用者那里將類的頭文件include即可
其次,C++類往往被編譯器作為一個編譯單元,生成一個obj文件。在最后進行鏈接的過程中,鏈接器會把工程中所有的obj鏈接以生成最終的二進制目標文件。所以鏈接器在遇到一處對類成員函數(或其它形式的符號引用)時,會在這個類生成的obj文件中找到符號的鏈接地址。
那么,在代碼中使用一個C++類,編譯期和鏈接期需要的到底是些什么東西呢?換句話說,滿足了什么樣的條件,編譯器和鏈接器就不會抱怨了呢?
根據C++語言的定義,一個C++類實際上是聲明或定義了如下幾類內容:
1.    聲明了一個數據結構,類中的非靜態數據成員、代碼中看不到但如果有虛函數就會生成的虛表入口地址指針等。
2.    聲明並定義了一堆函數,它們第一個參數都是一個指向這個數據結構的指針。這些實際上就是類中那些非靜態成員函數(包括虛函數),它們雖然在類聲明中是寫在類的一對大括號內部,但實際上沒有任何東西被加到前面第1條中所說的內部數據結構中。實際上,這樣的聲明只是為這些函數增加了兩個屬性:函數名標識符的作用域被限制在類中;函數第一個參數是this,被省略不寫了。
3.    聲明並定義了另一堆函數,它們看上去就是一些普通函數,與這個類幾乎沒有關系。這些實際上就是類中那些靜態函數,它們也是一樣,不會在第1條中所說的內部數據結構中增加什么東西,只是函數名標識符的作用域被限制在類中。
4.    聲明並定義了一堆全局變量。這些實際上就是類中那些靜態數據成員。
5.    聲明並定義了一個全局變量,此全局變量是一個函數指針數組,用來保存此類中所有的虛函數的入口地址。當然,這個全局變量生成的前提是這個類有虛函數。
下面是一個例子。
class MyClass
{
public:
    int x;
    int y;
    void Foo();
    void Bar(int newX, int newY);
    virtual void VFoo();
    virtual void VBar(int newX, int newY) = 0;
    static void SFoo();
    static void SBar(int newX, int newY);
    static int sx;
    static int sy;
};

對於上面列出的這個類MyClass,C++編譯器多數會以如下的方式進行編譯:

現在我們再來看一下為什么編譯器需要頭文件和符號地址就可以編譯鏈接一個使用MyClass的程序了。
首先,由於編譯器需要在編譯期就知道類的內存布局,以保證可以生成正確的開辟內存的代碼,及那些sizeof(MyClass)的值。有了頭文件,編譯器就知道,一個MyClass占用12字節的內存空間(見上圖,兩個整數和一個指針)。
其次,在調用MyClass的成員函數、靜態函數時,鏈接器需要知道這些函數的入口地址,如果無法提供入口地址,鏈接器就會報錯。
最后,在引用MyClass的靜態數據成員時,實際上與引用一個外部全局對象一樣,鏈接器需要知道這些變量的地址。如果無法提供這些變量的地址,鏈接器也會報錯。
可以看出:
1.    編譯期:必須要提供的是類的頭文件,以使編譯器可以得知類實例的尺寸和內存布局。
2.    鏈接期:必須要提供的是程序中引用過的,類的成員函數、靜態函數、靜態數據成員的地址,以使鏈接器可以正確的生成最終程序。
到這里,我們可以猜到,實際上,導出一個類,編譯器實際上只需要將這個類中的:成員函數、靜態函數、靜態數據成員當成普通的函數、全局變量導出即可。也就是說,我們實際上沒有“導出一個類”,而是把這個類中需要被引用的“有定義的實體”的入口地址像普通函數和變量那樣正常導出即可。
最后我們來看一下,實際上生成的一個導出前面列的的那個MyClass類的DLL。用Dependence來查看,可以看到下面的結果:

可以看到,除了VBar函數是一個純虛函數外,其它函數、靜態數據成員的入口地址都被導出。另外可以看到,vtable也被導出,以便操作虛函數時引用。
  
Balon白話MSDN:從普通DLL中導出C++類(1) – dllexport和dllimport的使用方法(中英對照、附注解)
我寫這些內容時偷了個懶,避開了虛表的一大堆復雜內容。謝謝houdy的提示,他的文章對於虛表,以及從DLL導出虛表的底層機制進行了詳細的剖析,想對此刨根問底的同學一定要看下:
虛函數表放在哪里?


本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/hxb20082008/archive/2009/09/02/4510494.aspx


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM