動態連結庫(DLL)的建立和使用
最近想做個記錄日誌的C++庫,方便後續使用。想著使用動態庫,正好沒用過,學習下。概念這裡不贅述。學習過程中碰到的幾點,記錄下來。學習是個漸進的過程,本文也是一個逐漸完善的過程。
一、Static Library
標準Turbo 2.0中的C函式庫(scanf、pringf、memcpy等)來自靜態庫。建立方法很簡單,建立win32 application工程,選擇static library,新增變數、方法和類等就可以了。使用的方法如下:
#include "../LogBuilderSL/LogBuilder.h"
#pragma comment(lib, "../Debug/LogBuilderSL.lib")
之後便可以像C庫函式一樣正常使用了。
#pragma comment(lib, "../Debug/LogBuilderSL.lib")表明,本工程與靜態庫(引數指定的路徑下的*.lib)一起編譯。
或者將該lib新增到【Project Property】-->【Linker】-->【Input】下的Additional Dependencies中,新增的方式為全路徑,如:“C:\Users\ SAMSUNG-PC\ Desktop\ C S\ LogBuilderSL\ Debug\ LogBuilderSL.lib”。
再或者將*.lib放置到Library Directories下(或者在其中新增*.lib路徑),在上面的【Input】中新增LogBuilderSL.lib。
二、Dynamic Link Library
對於DLL,VC支援的有三類:No-MFC DLL、MFC Regular DLL和MFC Extension DLL。
- No-MFC DLL:匯出函式為標準的C介面(extern "C");
- MFC Regular DLL:包含一個繼承自CWinApp的類,無訊息迴圈;
- MFC Extension DLL:採用MFC動態連結版本建立,只用於MFC類庫的應用程式。
1、No-MFC DLL
動態連結庫通過匯出函式對外提供的介面,有兩種匯出函式的方法:a、通過模組定義(.ref)檔案宣告;b、通過關鍵字__declspec(dllexport)宣告匯出函式。這裡僅討論第二種方式,模組定義檔案的方式請自行查閱。
給出簡單的DLL建立方法,標頭檔案宣告瞭類LogBuilder和兩個匯出函式,其中CreateLogBuilder()函式為C風格函式。CPP檔案照常定義即可。
<LogBuilderDL.h>
class LogBuilder{ ... };
extern "C" __declspec(dllexport) LogBuilder* CreateLogBuilder(string path);
__declspec(dllexport) void DeleteLogBuilder(LogBuilder *lpLogBuilder);
1)顯示(動態)載入該DLL
<Main.cpp>
#include "../LogBuilderDL/LogBuilder.h"
typedef LogBuilder*(*CreatorByPath)(string); // 巨集定義函式指標型別
int _tmain(int argc, _TCHAR* argv[])
{
HINSTANCE hDll; // DLL控制程式碼
CreatorByPath creator; // 函式指標
hDll = LoadLibrary(L"..\\Debug\\LogBuilderDL.dll");
if (hDll != NULL)
{
creator = (CreatorByPath)GetProcAddress(hDll, "CreateLogBuilder");
if (creator != NULL)
{
LogBuilder* log = creator("log.log");
log->WriteLog("Liwuqingxin", true);
}
FreeLibrary(hDll);
}
getchar();
return 0;
}
- 首先,載入DLL;
- 然後,獲取了CreateLogBuilder()函式的地址;
- 最後,通過函式地址呼叫該函式。
這裡需要注意兩點。
其一,以上DLL間接匯出了C++類,這裡通過C風格函式封裝類的獲取過程,獲取到類的例項後可正常使用該類,但類的靜態成員(需要使用域作用符訪問的成員)便無法匯出。另外可直接匯出C++類,第三點深入討論。當需要使用DLL中的型別、巨集定義或者變數時,需要包含該DLL的標頭檔案(顯式(動態)呼叫時,僅僅使用函式時並不需要)。
其二,CreateLogBuilder()為C風格函式。如果不宣告為extern "C",該函式被C編譯器編譯後在符號庫中的名字為"CreateLogBuilder",而C++編譯器則會產生名稱為"?CreateLogBuilder@@YAPAVLogBuilder@@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z"之類的外部連結符號(不同的編譯器可能生成的名字不同,但是都採用了相同的機制,生成的新名字稱為“mangled name”)[參考:http://www.jianshu.com/p/5d2eeeb93590]。顯示(動態)載入DLL時,GetProcAddress()函式需要通過上述真實的外部連結符號名稱去獲取函式地址,隱式(靜態)載入沒有影響。因此,匯出函式應使用extern "C"宣告為C風格函式更加合適(對載入方式沒有要求)。
其三,匯出C++類:在class關鍵字與類名中間新增匯出宣告(這裡需要使用巨集代替__declspec(dllexport),因為在呼叫DLL時需要宣告匯入類,直接使用該宣告則DLL使用者需要另外定義.h檔案)。這樣,DLL使用者可直接使用該類。但是靜態成員需要額外宣告為匯出。如下:
<LogBuilder.cpp>
API_DECLSPEC int LogBuilder::s = 0;
API_DECLSPEC int LogBuilder::fun()
{
return 0;
}
並且在使用者使用時需要加上匯入lib的宣告:
<Main.cpp>
...
#pragma comment(lib, "../Debug/LogBuilderDL.lib") // 使用類的靜態成員時需要
...
所謂的顯示(動態)載入,是通過windows API函式載入DLL,並獲取需要的函式地址,這個工作由API完成。而在客戶程式中直接使用類的靜態成員,編譯會報無法解析外部符號的錯,因為編譯器無法找到這些符號(未呼叫API),那麼我們只能自己顯示載入.lib檔案,並在DLL中宣告匯出靜態成員。更深入理解為,我們還可以直接將“?fun@LogBuilder@@SAHXZ”傳遞給GetProcAddress()函式獲取靜態成員的地址,這樣也能不載入.lib直接使用。
2)隱式(靜態)載入DLL
<Main.cpp>
#include "../LogBuilderDL/LogBuilder.h"
#pragma comment(lib, "LogBuilderDL.lib")
extern "C" __declspec(dllimport) LogBuilder* CreateLogBuilder(std::string path);
int _tmain(int argc, _TCHAR* argv[])
{
LogBuilder *log = CreateLogBuilder("log.log");
if (log != NULL)
log->WriteLog("Liwuqingxin<span style="font-family: Arial, Helvetica, sans-serif;">", true);
getchar();
return 0;
}
- 首先,包含DLL的標頭檔案;
- 然後,告訴編譯器.lib檔案的路徑(方式和1中的靜態庫方法一致);
- 再次,宣告匯入函式,對應於DLL匯出函式;
- 最後,可以直接像正常函式一樣使用了。
需要注意幾點。
其一,CreateLogBuilder()為匯出函式,可以用來建立類的物件。若使用匯出類(前面有提到),還可以直接例項化該類(但是不推薦,會導致DLL HELL,後面詳述)。
其二,全域性變數需要宣告匯出,否則客戶程式包含標頭檔案後使用的全域性變數和DLL的中的全域性變數將是兩份副本!
其三,extern "C" __declspec(dllimport) LogBuilder* CreateLogBuilder(std::string path);這句宣告沒有似乎也可以呼叫該函式[參見:http://bbs.csdn.net/topics/330169671 ]。總結一下這裡查閱資料的收穫:
前文中,使用__declspec(dllexport)宣告匯出函式,這個方法沒錯,但是程式碼的寫法有些問題。明確一下:一個DLL建立後,需要提供給使用者的有三個檔案:.h、.lib、.dll。DLL建立者和使用者共用.h檔案,但需求不一樣:建立者需要宣告函式為__declspec(dllexport);使用者需要宣告函式為__declspec(dllimport)。因此,出於維護性和規範性考慮,使用預編譯巨集和巨集定義區分.h檔案的包含者:DLL自身加入預編譯巨集***_EXPORTING。否則,假如一個DLLA呼叫另一個DLLB而包含其標頭檔案時,將會使用__declspec(dllexport)而錯誤地將DLLB中匯入的函式作為DLLA的函式匯出了。(如此,Main.cpp中應該不用再加入extern "C" __declspec(dllimport) LogBuilder* CreateLogBuilder(std::string path);語句了)程式碼如下:
#ifdef HFILENAME_EXPORTING
#define API_DECLSPEC __declspec(dllexport)
#else
#define API_DECLSPEC __declspec(dllimport)
#endif
使用DLL時,__declspec(dllimport)宣告編譯時明確函式為從DLL匯入的外部函式,不需要間接定址,效率更高
3)DLL HELL
bz剛開始學習DLL相關,這裡參考:DLL匯出類。總結一下。[參考:http://m.blog.csdn.net/blog/guyue35/16996713]
1、DLL和客戶程式是分開編譯的,這會導致某些編譯時確定的內容在DLL中修改無法更新到客戶程式(除非你重新編譯客戶程式,這不現實)。以下情況會導致錯誤:
- 應用程式直接訪問類的公有變數,而該公有變數在新DLL中定義的位置發生了變化;
- 應用程式呼叫類的一個虛擬函式,而新的類中,該虛擬函式的前面又增加了一個虛擬函式;
- 新類的後面增加了成員變數,並且新類的成員函式將訪問、修改這些變數;
- 修改了新類的基類,基類的大小發生了變化;
- 其他編譯時確定的內容,如C的常量(C++新特性常量為執行時確定),巨集等。
2、匯出類的大小、成員的位置等的改變無法通知到客戶程式,要想做一個可升級的DLL,以下三點用來使DLL遠離地獄:
不直接生成類的例項。對於類的大小,當我們定義一個類的例項,或使用new語句生成一個例項時,記憶體的大小是在編譯時決定的。要使應用程式不依賴於類的大小,只有一個辦法:應用程式不生成類的例項,使用DLL中的函式來生成。把匯出類的建構函式定義為私有的(privated),在匯出類中提供靜態(static)成員函式(如NewInstance())(靜態成員函式能夠用類名直接呼叫,而一般的成員函式要使用類物件來呼叫,這樣只有先宣告物件才能呼叫一般成員函式,此處要先用函式來構造類物件,故為靜態的)用來生成類的例項。因為NewInstance()函式在新的DLL中會被重新編譯,所以總能返回大小正確的例項記憶體。
不直接訪問成員變數。應用程式直接訪問類的成員變數時會用到該變數的偏移地址。所以避免偏移地址依賴的辦法就是不要直接訪問成員變數。把所有的成員變數的訪問控制都定義為保護型(protected)以上的級別,併為需要訪問的成員變數定義Get或Set方法。Get或Set方法在編譯新DLL時會被重新編譯,所以總能訪問到正確的變數位置。
忘了虛擬函式吧,就算有也不要讓應用程式直接訪問它。因為類的建構函式已經是私有(privated)的了,所以應用程式也不會去繼承這個類,也不會實現自己的多型。如果匯出類的父類中有虛擬函式,或設計需要(如類工場之類的框架),一定要把這些函式宣告為保護的(protected)以上的級別,併為應用程式重新設計呼叫該慮函式的成員函式。這一點也類似於對成員變數的處理。
事實上,建議你在釋出匯出類的DLL的時候,重新定義一個類的宣告,這個宣告可以不管原來的類裡的成員變數之類的,只把介面函式列在類的宣告裡。
[主要參考:《VC++動態連結庫(DLL)程式設計》 系列,作者:宋寶華,http://21cnbao.blog.51cto.com/109393/120777。
PS:本文參考了很多優秀的部落格、論壇等,感謝這些大蝦們的總結。在參考的地方基本上給出了原文連結。本文在此基礎上進行了實驗驗證並做了一些整理和總結。
相關文章
- 動態連結庫的生成和使用(二)
- IIS無法訪問動態連結庫DLL的原因
- P/Invoke之C#呼叫動態連結庫DLLC#
- windows和linux gcc生成動態連結庫DLL和SO並用python呼叫WindowsLinuxGCPython
- lua——alien庫實現lua呼叫C動態連結庫(dll、so)
- linux下靜態連結庫和動態連結庫的區別有哪些Linux
- 動態連結庫與靜態連結庫
- cmake 連結動態連結庫
- Windows環境下,動態連結庫(DLL)的“匯入”與“匯出”概念Windows
- 動態庫的建立和呼叫
- DLL動態庫動態載入
- 動態連結串列的建立(程式碼)
- 靜態連結動態連結的連結順序問題和makefile示例
- VS中呼叫DLL動態庫的方法
- 在 Linux中如何使用動態連結模組庫?Linux
- 使用js動態新增連結隨機連結JS隨機
- C++呼叫C#的動態庫dllC++C#
- Linux環境下:程式的連結, 裝載和庫[靜態連結]Linux
- 【連結 1】與靜態連結庫連結
- 動態庫的生成和使用(二)
- 使用Visual Studio 2022 建立lib和dll並使用
- 編譯 pyav 成 wheel 並使用 auditwheel 固化動態連結庫編譯
- 資料結構-malloc申請動態空間-連結串列的建立資料結構
- 載入動態連結庫——dlopen dlsym dlclose
- C#呼叫C++動態連結庫C#C++
- Gazebo新增模型並控制模型運動作為動態障礙物(Ubuntu16.04, Gazebo7.16),附錄動態連結庫和靜態連結庫區別模型Ubuntu
- 如何連結兩個名字一樣動態庫
- TCP連結的建立和釋放TCP
- 動態連結的相關結構
- 封裝動態庫dll與靜態庫lib(原理及簡單例項)封裝單例
- 靜態庫和動態庫的製作以及Bundle資原始檔的使用
- 動態連結的PLT與GOTGo
- iOS動態庫的使用iOS
- CMake 進行多專案中dll的編譯和連結編譯
- 動態庫使用
- ios靜態庫和動態庫iOS
- FFmpeg開發筆記(四)FFmpeg的動態連結庫介紹筆記
- iOS動態庫和靜態庫的運用iOS