C++類庫開發詳解(轉)

前言:這是一篇總結性的文章,需要有一點C++和dll基本知識的基礎,在網上查閱了很多資料感覺沒有一篇詳細、具體、全面的dll開發介紹,我這是根據最近項目和網上資料整理出來的,並附帶實例的一個總結性的文章(由於篇幅較長故不附帶源碼解釋)。另外,個人愚昧地認爲以後C++的開發會更多地面向庫的開發,所以學會庫的開發必不可少。

 https://blog.csdn.net/z702143700/article/details/45989993

1、 靜態鏈接庫和動態鏈接庫

1.   靜態鏈接庫(LIB)只用在程序開發期間使用,而動態鏈接庫(DLL)在執行期間使用。

2.   靜態鏈接庫和動態鏈接庫的另外一個區別在於靜態鏈接庫中不能再包含其他的動態鏈接庫或者靜態庫,而在動態鏈接庫中還可以再包含其他的動態或靜態鏈接庫。

3.   靜態鏈接庫,浪費空間和資源,因爲所有相關的目標文件與牽涉到的函數庫被鏈接合成一個可執行文件。另一個問題是靜態庫對程序的更新、部署和發佈頁會帶來麻煩。如果靜態庫libname.lib更新了,所有使用它的應用程序都需要重新編譯、發佈給用戶(對於用戶來說,可能是一個很小的改動,卻導致整個程序重新下載,全量更新)。

4.   動態庫則實現了增量更新,進程間可以共享動態庫,節約了資源,當程序第一次調用動態庫時,系統將該庫加載到內存中,當另一個程序也調用這個庫時,系統不再加載,而是將狀態+1,當某個程序退出或釋放該庫時,狀態則-1,直到當系統中沒有程序調用該庫時,系統自動將其清理並釋放內存。

 

2、 知識點

1.   在VS中創建的類庫有2種類型,一種是直接選擇VC++類庫(是使用微軟版本的C++創建的類庫),一種是Win32項目或Win32控制檯程序,然後選擇對應的類庫類型,也就是ANSI標準的C++類庫,一般我們用這種方式創建的類庫,而它又分三種:Non-MFC DLL(非MFC動態庫)、MFC Regular DLL(MFC規則DLL)、MFC ExtensionDLL(MFC擴展DLL)。

2.   非MFC動態庫不採用MFC類庫結構,其導出函數爲標準的C接口,能被非MFC或MFC編寫的應用程序所調用;MFC規則DLL包含一個繼承自CWinApp的類,但其無消息循環;MFC擴展DLL採用MFC的動態鏈接版本創建,它只能被用MFC類庫所編寫的應用程序所調用。

3.   當Windows要執行一個使用了動態鏈接庫的程序而需要加載該鏈接庫時,動態鏈接庫文件必須儲存在含有該.EXE程序的目錄下、目前的目錄下、Windows系統目錄下、Windows目錄下,或者是在通過MS-DOS環境中的PATH可以存取到的目錄下(Windows會按順序搜索這些目錄)。

4.   動態鏈接庫模塊可能有其它擴展名(如.EXE或.FON),但標準擴展名是.DLL。只有帶.DLL擴展名的動態鏈接庫才能被Windows自動加載。如果文件有其它擴展名,則程序必須另外使用LoadLibrary或者LoadLibraryEx函數加載該模塊。

5.   DLL 的編制與具體的編程語言及編譯器無關。只要遵循約定的DLL接口規範和調用方式,用各種語言編寫的DLL都可以相互調用。

 

3、 關鍵字

1.   當建立一個DLL時,它應該包含處理字符和字符串的Unicode和非Unicode版的所有函數,比如實現ANSI版和寬字符版。

#ifdef UNICODE
#define TextW //定義寬字符版的函數
#else
#define TextA //定義ANSI版的函數
#endif
 

2.  __declspec(dllexport),該關鍵字位於類/函數的聲明和定義中,表示該類/函數爲DLL的導出類/函數。而DLL內的類/函數分兩種,一種是DLL導出類/函數供外部程序調用,一種是DLL內部函數供DLL自己調用。

3.  __declspec(dllimport),該關鍵字說明類/函數爲導入函數,與__declspec(dllexport)匹配對應,爲了在應用程序中使用其聲明的類/函數。

 

4.  extern"C",是爲了解決導出函數名的問題,因爲C++編譯器,爲實現函數重載,在編譯生成的彙編代碼中要對函數名進行一些處理,而用 extern "C"聲明的函數將使用函數名作符號名,這時C的處理方式。因此,只有非成員函數才能被聲明爲extern "C",並且不能被重載。但是,冠以extern "C"限定符後,並不意味着函數中無法使用C++代碼了,相反,它仍然是一個完全的C++函數,可以使用任何C++特性和各種類型的參數。但這隻解決了C/C++之間調用的問題,更可靠的方法是定義一個.def文件。

5.  __cplusplus,是cpp中自定義宏,定義了這個宏表示它是一段cpp的代碼。並且這是一個C++編譯器保留的宏定義,意味着C++編譯器認爲這個宏已經定義了。

如果函數這樣定義:extern”C” int add(int a, int b);由於C編譯器不能識別extern”C”指令,那麼C調用C++程序就會出現問題,這時候__cplusplus就起作用了。如下:

#ifdef __cplusplus
extern "C" {
#endif

//....聲明代碼

#ifdef __cplusplus
}
#endif


6.  #pragma comment( lib , "..//debug//LIBProject.lib" )的意思是指本文件(應用程序)生成的.obj文件應與LIBProject.lib一起連接。

7.  __stdcall,它是Standard Call的縮寫,是C的標準函數調用方式:所有參數從右到左依次入棧,如果調用的是類成員的話,最後一個入棧的是this指針。堆棧中的參數由被調用的函數在返回後清除,函數在編譯的時候就必須嚴格控制參數生成,否則返回後會出錯。

8.  __fastcall,是編譯器指定的快速調用方式。由於使用堆棧傳遞比較費時,因此__fastcall通常規定將前N(一般2個,不同的編譯器規定使用的寄存器個數不同)個參數由CPU寄存器傳遞,其餘的還是用內存的堆棧傳遞。返回方式和__stdcall相當。由於其涉及到編譯器決定參數傳遞方式,故不能作爲跨編譯器的接口。

 

9.  __cdecl,它是C Declaration的縮寫,表示C語言默認的函數調用方式:所有的參數從右到左依次入棧,參數由調用者清除(手動清除,調用者一般指編譯器)。特點在於可以使用不定個數的參數。

 

如果通過VC++編寫的DLL欲被其他語言編寫的程序調用,應將函數的調用方式聲明爲__stdcall方式,而C/C++缺省的調用方式卻爲__cdecl(默認調用方式)。__stdcall與__cdecl的區別在於生成函數名最終符號的方式不同。若採用C編譯方式(在C++中將函數聲明爲extern "C"),__stdcall約定在輸出函數名前面加下劃線,後面加“@”符號和參數的字節數,形如_functionname@number;而__cdecl約定僅在輸出函數名前面加下劃線,形如_functionname。

注意,聲明函數形式:extern “C” int __stdcall add(int x, int y);,應用程序中定義函數指針爲:typedef int(__stdcall *lpAddFun)(int, int);

 

4、 靜態庫(lib/a)

由於靜態庫是跟隨着應用程序一起編譯連接到源文件(exe)的,所以代碼寫法基本沒有特殊的地方,需要注意的是extern"C"來確定是否使用C的方式編譯,然後就是利用#ifndef來管理一類型的類/函數的調用。

值得注意的是在應用程序中使用靜態庫時的聲明#include"MathTool.h"、#pragma comment(lib,"LIBProject.lib"):直接將.h和.lib文件放在當前目錄似乎是不行的(我測試時不行的),兩種方法:1.使用相對路徑或絕對路徑包含.h和.lib文件。2.通過”屬性”--”C/C++” --“常規”--”附加包含目錄”,來指定.h的文件目錄。通過”屬性”--”鏈接器” -- “常規”--”附加庫目錄”,來添加.lib目錄。這種方法動態庫也適用。

如果不想用#pragmacomment來指定lib,還可以在”屬性”--”鏈接器” -- “常規”--”附加依賴庫”來指定庫。也可以不設置庫目錄和依賴庫名,而是直接“屬性”--“鏈接器”--”命令行”,輸入靜態庫的完整路徑即可。但一般不推薦。

示例見:LIBProject。

 

5、 動態庫(dll/so)

1.  動態庫的lib文件和靜態庫的lib文件

靜態庫對應的lib文件叫靜態庫,動態庫對應的lib文件叫導入庫。實際上靜態庫本身就包含了實際執行代碼、符號表等等,而對於導入庫而言,其實際的執行代碼位於動態庫中,導入庫只包含了地址符號表等,確保程序找到對應函數的一些基本地址信息。

 

2.  聲明導出函數(介紹.def)

DLL導出函數的聲明有兩種方式:一種是在聲明中加__declspec(dllexport); 一種是採用模塊定義(.def) 文件聲明。.def文件爲鏈接器提供了有關被鏈接程序的導出、屬性及其他方面的信息。

.def文件的規則爲:

(1)LIBRARY語句說明.def文件相應的DLL;

 (2)EXPORTS語句後列出要導出函數的名稱。可以在.def文件中的導出函數名後加@n,表示要導出函數的序號爲n(在進行函數調用時,這個序號將發揮其作用);

(3).def 文件中的註釋由每個註釋行開始處的分號(;) 指定,且註釋不能與語句共享一行。

GetInstance = GetInstance這樣可以指定了DLL的函數導出後的名稱仍然不變。

注:使用了.def文件記得在編譯器:”屬性”--”鏈接器”--”輸入”--”模塊定義文件”中設置改def文件。

如果使用__declspec(dllexport)的方式聲明導出,在C++中使用當然沒有問題,但是當其他語言(C#、VB)調用時,就會出現找不到函數名的情況,是因爲C++中爲實現重載機制將函數名改變了,這時我們爲了能夠讓其他語言正確的調用,就要使用extern “C”+ __declspec(dllexport)+__stdcall的方式。而使用def文件這些都可以不要,這樣更方便,但是用到類的地方還是要用第一種方式。

 

3.  調用方式

DLL的調用方式也有兩種:一種是動態調用;一種是靜態調用。

動態調用:它完全由編程者用 API 函數加載和卸載 DLL,程序員可以決定 DLL文件何時加載或不加載,顯式鏈接在運行時決定加載哪個 DLL文件。由LoadLibrary->GetProcAddress->FreeLibrary系統API提供的三位一體“DLL加載-DLL函數地址獲取-DLL釋放”方式。

動態調用注意的地方,在函數前面加extern “C”或定義def文件。顯式調用類庫中的class是很危險和繁瑣的,因此能隱式不顯式,能靜態不動態。

靜態調用: 它的特點是由編譯系統完成對DLL的加載和應用程序結束時 DLL 的卸載。靜態調用方式的當調用某DLL的應用程序結束時,若系統中還有其它程序使用該 DLL,則Windows對DLL的應用記錄減1,直到所有使用該DLL的程序都結束時才釋放它。當程序員通過靜態鏈接方式編譯生成應用程序時,應用程序中調用的與.lib文件中導出符號相匹配的函數符號將進入到生成的EXE 文件中,.lib文件中所包含的與之對應的DLL文件的文件名也被編譯器存儲在 EXE文件內部。當應用程序運行過程中需要加載DLL文件時,Windows將根據這些信息發現並加載DLL,然後通過符號名實現對DLL 函數的動態鏈接。這樣,EXE將能直接通過函數名調用DLL的輸出函數,就像調用程序內部的其他函數一樣。靜態調用方式簡單實用,但不如動態調用方式靈活。

靜態調用需要兩步:

1.  告訴編譯器與DLL相對應的.lib文件所在的路徑及文件名, #pragma comment(lib,"dllTest.lib")。程序員在建立一個DLL文件時,連接器會自動爲其生成一個對應的.lib文件,該文件包含了DLL 導出函數的符號名及序號(並不含有實際的代碼)。在應用程序裏,.lib文件將作爲DLL的替代文件參與編譯。具體可以這樣做,將.h、.lib、.dll文件拷貝到客戶端程序當前目錄下,然後在程序中#include<*.h> + #pragmacomment(lib,"dllTest.lib");或者在客戶端程序的工程屬性裏面增加對該lib文件的引入。

2.  聲明導入函數, 用__declspec(dllimport)說明爲導入函數。有時是不用聲明的,在類庫的編寫過程中定義好了。

 

4.  DllMain函數

Windows在加載DLL的時候,需要一個入口函數,就如同控制檯或DOS程序需要main函數、WIN32程序需要WinMain函數一樣。當程序中沒有寫DllMain函數時,系統會從其它運行庫中引入一個不做任何操作的缺省DllMain函數版本,並不是DLL可以放棄DllMain函數。

根據編寫規範,Windows必須查找並執行DLL裏的DllMain函數作爲加載DLL的依據,它使得DLL得以保留在內存裏。這個函數並不屬於導出函數,而是DLL的內部函數。這意味着不能直接在應用程序中引用DllMain函數,DllMain是自動被調用的。

DllMain函數在DLL被加載和卸載時被調用,在單個線程啓動和終止時,DLLMain函數也被調用,ul_reason_for_call指明瞭被調用的原因。

 

BOOL APIENTRY DllMain( HMODULEhModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}

解析:

APIENTRY被定義爲__stdcall,它意味着這個函數以標準Pascal的方式進行調用,也就是WINAPI方式;

ul_reason_for_call的四個參數DLL_PROCESS_ATTACH表示進程調用;DLL_THREAD_ATTACH表示線程調用;DLL_THREAD_DETACH線程釋放;DLL_PROCESS_DETACH線程釋放。

lpReserved是一個保留字,基本沒有什麼作用,無需瞭解。

進程中的每個DLL模塊被全局唯一的32字節的HINSTANCE句柄標識,只有在特定的進程內部有效,句柄代表了DLL模塊在進程虛擬空間中的起始地址。在Win32中,HINSTANCE和HMODULE的值是相同的,這兩種類型可以替換使用,這就是函數參數hModule的來歷。

GetProcAddress( hDll, MAKEINTRESOURCE ( 1 ) )要注意,它直接通過.def文件中爲add函數指定的順序號訪問add函數,具體體現在MAKEINTRESOURCE ( 1 ),MAKEINTRESOURCE是一個通過序號獲取函數名的宏,定義爲(節選自winuser.h):

#defineMAKEINTRESOURCEA(i) (LPSTR)((DWORD)((WORD)(i)))
#defineMAKEINTRESOURCEW(i) (LPWSTR)((DWORD)((WORD)(i)))
#ifdef UNICODE
#defineMAKEINTRESOURCE MAKEINTRESOURCEW
#else
#defineMAKEINTRESOURCE MAKEINTRESOURCEA


5.  DLL導出變量

DLL定義的全局變量可以被調用進程訪問;DLL也可以訪問調用進程的全局數據。步驟:

1.  庫的頭文件.h中聲明,extern int dllGlobalVar;

2.  在.cpp文件中聲明使用,聲明:int dllGlobalVar;使用:dllGlobalVar = 100;

3.  .def文件中導出,”GlobalVar_a@[n]”的方式,有人說是dllGlobalVar CONSTANT(已過時)/ dllGlobalVar DATA這個方式,但是我用的不行。

4.  在應用程序中引用DLL中定義的全局變量,方式有兩種:一種方法(使用def聲明的變量):首先聲明extern int dllGlobalVar;然後int a = *(int*)dllGlobalVar。 特別要注意的是用externint dllGlobalVar聲明所導入的並不是DLL中全局變量本身,而是其地址。另一種方法:externint _declspec(dllimport) dllGlobalVar; 通過_declspec(dllimport)方式導入的就是DLL中全局變量本身而不再是其地址了。建議第二種方式,第一種方式我測試中遇到一些很奇怪的問題。希望,有人根據我的測試,幫我解決下。

示例見:DLLProjec

 

6.  DLL導出類

導出類,我們一般使用靜態調用,這樣我們可以將整個類導出,或者只將類中的指定成員導出。如果使用動態調用類,我們一般使用一個全局函數來返回一個類的對象。

最後,學習到這裏,最大的領悟就是,類庫的編寫,主要理解extern”C”、_declspec(dllexport)、_declspec(dllimport)、__stdcall、__cdecl這幾個基本關鍵字,類庫是提供給別人用的,我需要做的就是指定將哪些類或函數指定爲用戶使用的。還有就是使用類庫,知道靜態調用和動態調用的方法。

示例見:DLLProjec

 附源碼鏈接:http://download.csdn.net/detail/z702143700/8738467

後記:本文很長,也是我花費了很多個工作日學習實踐的成果,如果有錯誤或意見希望能給我留言,結合示例來看會更加好些,如果可以直接看懂示例,那麼長的文字也就可以隨便瀏覽了。接下來,我會介紹其他語言調用C++類庫常用的方法(C#調用C++的庫)。
---------------------
作者:奔跑的小河
來源:CSDN
原文:https://blog.csdn.net/z702143700/article/details/45989993
版權聲明:本文爲博主原創文章,轉載請附上博文鏈接!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章