簡單的說,dll有如下幾個優勢:
1) 節省內存。同一個軟件模塊,如果以源代碼的形式重用,則會被編譯到不一樣的可執行程序中,同時運行這些exe時這些模塊的二進制碼會被重複加載到內存中。若是使用dll,則只在內存中加載一次,全部使用該dll的進程會共享此塊內存(固然,像dll中的全局變量這種東西是會被每一個進程複製一份的)。
2) 不需編譯的軟件系統升級,若一個軟件系統使用了dll,則該dll被改變(函數名不變)時,系統升級只須要更換此dll便可,不須要從新編譯整個系統。事實上,不少軟件都是以這種方式升級的。例如咱們常常玩的星際、魔獸等遊戲也是這樣進行版本升級的。
3) dll庫能夠供多種編程語言使用,例如用c編寫的dll能夠在vb中調用。這一點上dll還作得很不夠,所以在dll的基礎上發明了COM技術,更好的解決了一系列問題。要注意:com雖然也是以dll(或exe)的形式存在的,但它的調用方式卻不一樣於普通dll。ios
開始寫dll以前,你須要一個c/c++編譯器和連接器,並關閉你的IDE。是的,把你的VC和C++ BUILDER之類的東東都關掉,並打開你以往只用來記電話的記事本程序。不這樣作的話,你可能一生也不明白dll的真諦。我使用了VC自帶的cl編譯器和link連接器,它們通常都在vc的bin目錄下。(若你沒有在安裝vc的時候選擇註冊環境變量,那麼就馬上將它們的路徑加入path吧)若是你仍是由於離開了IDE而懼怕到哭泣的話,你能夠關閉這個頁面並繼續去看《VC++技術內幕》之類無聊的書了。c++
最簡單的dll並不比c的helloworld難,只要一個DllMain函數便可,包含objbase.h頭文件(支持COM技術的一個頭文件)。若你以爲這個頭文件名字難記,那麼用windows.h也能夠。源代碼以下:dll_nolib.cpp編程
BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved){ HANDLE g_hModule; switch(dwReason) { case DLL_PROCESS_ATTACH: cout<<"Dll is attached!"<<endl; g_hModule = (HINSTANCE)hModule; break; case DLL_PROCESS_DETACH: cout<<"Dll is detached!"<<endl; g_hModule=NULL; break; } return true; }
其中DllMain是每一個dll的入口函數,如同c的main函數同樣。DllMain帶有三個參數,hModule表示本dll的實例句柄(聽不懂就不理它,寫過windows程序的天然懂),dwReason表示dll當前所處的狀態,例如DLL_PROCESS_ATTACH表示dll剛剛被加載到一個進程中,DLL_PROCESS_DETACH表示dll剛剛從一個進程中卸載。固然還有表示加載到線程中和從線程中卸載的狀態,這裏省略。最後一個參數是一個保留參數(目前和dll的一些狀態相關,可是不多使用)。windows
從上面的程序能夠看出,當dll被加載到一個進程中時,dll打印"Dll is attached!"語句;當dll從進程中卸載時,打印"Dll is detached!"語句。api
編譯dll須要如下兩條命令:cl /c dll_nolib.cpp
這條命令會將cpp編譯爲obj文件,若不使用/c參數則cl還會試圖繼續將obj連接爲exe,可是這裏是一個dll,沒有main函數,所以會報錯。沒關係,繼續使用連接命令。
Link /dll dll_nolib.obj
這條命令會生成dll_nolib.dll。編程語言
注意,由於編譯命令比較簡單,因此本文不討論nmake,有興趣的可使用nmake,或者寫個bat批處理來編譯連接dll。函數
使用dll大致上有兩種方式,顯式調用和隱式調用。這裏首先介紹顯式調用。編寫一個客戶端程序:dll_nolib_client.cpp工具
#include <windows.h> #include <iostream.h> int main(void){ //加載咱們的dll HINSTANCE hinst=::LoadLibrary("dll_nolib.dll"); if (NULL != hinst) { cout<<"dll loaded!"<<endl; } return 0; }
注意,調用dll使用LoadLibrary函數,它的參數就是dll的路徑和名稱,返回值是dll的句柄。 使用以下命令編譯連接客戶端:Cl dll_nolib_client.cpp
visual-studio
並執行dll_nolib_client.exe,獲得以下結果:測試
Dll is attached! dll loaded! Dll is detached!
以上結果代表dll已經被客戶端加載過。可是這樣僅僅可以將dll加載到內存,不能找到dll中的函數。咱們可使用dumpbin命令查看DLL中的函數。
Dumpbin命令能夠查看一個dll中的輸出函數符號名,鍵入以下命令:Dumpbin –exports dll_nolib.dll
經過查看,發現dll_nolib.dll並無輸出任何函數。
那麼如何在dll中定義輸出函數呢?整體來講有兩種方法,一種是添加一個def定義文件,在此文件中定義dll中要輸出的函數;第二種是在源代碼中待輸出的函數前加上__declspec(dllexport)關鍵字。
首先寫一個帶有輸出函數的dll,源代碼以下:dll_def.cpp
#include <objbase.h> #include <iostream.h> void FuncInDll (void){ cout<<"FuncInDll is called!"<<endl; } BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved){ HANDLE g_hModule; switch(dwReason) { case DLL_PROCESS_ATTACH: g_hModule = (HINSTANCE)hModule; break; case DLL_PROCESS_DETACH: g_hModule=NULL; break; } return TRUE; }
這個dll的def文件以下:dll_def.def
; ; dll_def module-definition file ; LIBRARY dll_def.dll DESCRIPTION '(c)2007-2009 Wang Xuebin' EXPORTS FuncInDll @1 PRIVATE
你會發現def的語法很簡單,首先是LIBRARY關鍵字,指定dll的名字;而後一個可選的關鍵字DESCRIPTION,後面寫上版權等信息(不寫也能夠);最後是EXPORTS關鍵字,後面寫上dll中全部要輸出的函數名或變量名,而後接上@以及依次編號的數字(從1到N),最後接上修飾符。
用以下命令編譯連接帶有def文件的dll:
Cl /c dll_def.cpp Link /dll dll_def.obj /def:dll_def.def
再調用dumpbin查看生成的dll_def.dll:Dumpbin –exports dll_def.dll
獲得以下結果:
Dump of file dll_def.dll File Type: DLL Section contains the following exports for dll_def.dll 0 characteristics 46E4EE98 time date stamp Mon Sep 10 15:13:28 2007 0.00 version 1 ordinal base 1 number of functions 1 number of names ordinal hint RVA name 1 0 00001000 FuncInDll Summary 2000 .data 1000 .rdata 1000 .reloc 6000 .text
觀察這一行0 00001000 FuncInDll
,會發現該dll輸出了函數FuncInDll。
寫一個dll_def.dll的客戶端程序:dll_def_client.cpp
#include <windows.h> #include <iostream.h> int main(void){ //定義一個函數指針 typedef void (* DLLWITHLIB )(void); //定義一個函數指針變量 DLLWITHLIB pfFuncInDll = NULL; //加載咱們的dll HINSTANCE hinst=::LoadLibrary("dll_def.dll"); if (NULL != hinst) { cout<<"dll loaded!"<<endl; } //找到dll的FuncInDll函數 pfFuncInDll = (DLLWITHLIB)GetProcAddress(hinst, "FuncInDll"); //調用dll裏的函數 if (NULL != pfFuncInDll) { (*pfFuncInDll)(); } return 0; }
有兩個地方值得注意,第一是函數指針的定義和使用,不懂的隨便找本c++書看看;第二是GetProcAddress的使用,這個API是用來查找dll中的函數地址的,第一個參數是DLL的句柄,即LoadLibrary返回的句柄,第二個參數是dll中的函數名稱,即dumpbin中輸出的函數名(注意,這裏的函數名稱指的是編譯後的函數名,不必定等於dll源代碼中的函數名)。
編譯連接這個客戶端程序,並執行會獲得:
dll loaded! FuncInDll is called!
這代表客戶端成功調用了dll中的函數FuncInDll。
__declspec(dllexport)
聲明導出函數爲每一個dll寫def顯得很繁雜,目前def使用已經比較少了,更多的是使用__declspec(dllexport)在源代碼中定義dll的輸出函數。
Dll寫法同上,去掉def文件,並在每一個要輸出的函數前面加上聲明__declspec(dllexport),例如:
__declspec(dllexport) void FuncInDll (void)
這裏提供一個dll源程序dll_withlib.cpp,而後編譯連接。連接時不須要指定/DEF:參數,直接加/DLL參數便可,
Cl /c dll_withlib.cpp Link /dll dll_withlib.obj
而後使用dumpbin命令查看,獲得:
1 0 00001000 ?FuncInDll@@YAXXZ
可知編譯後的函數名爲?FuncInDll@@YAXXZ
,而並非FuncInDll,這是由於c++編譯器基於函數重載的考慮,會更改函數名,這樣使用顯式調用的時候,也必須使用這個更改後的函數名,這顯然給客戶帶來麻煩。爲了不這種現象,可使用extern 「C」指令來命令c++編譯器以c編譯器的方式來命名該函數。修改後的函數聲明爲:
extern "C" __declspec(dllexport) void FuncInDll (void)
dumpbin命令結果:
1 0 00001000 FuncInDll
這樣,顯式調用時只需查找函數名爲FuncInDll的函數便可成功。
使用extern 「C」關鍵字實際上至關於一個編譯器的開關,它能夠將c++語言的函數編譯爲c語言的函數名稱。即保持編譯後的函數符號名等於源代碼中的函數名稱。
雖然使用 extern C 的方法能夠解決名字改編的問題,但它有兩個缺陷,一個是它不能用來導出一個類的成員函數,只能導出全局函數,使全局函數的名字不發生改變。另外一個是若是咱們導出的函數的調用約定發生改變的話,即便咱們的函數用 extern C 做聲明,函數名仍會發生改變。
調用約定,通常沒有指定下,用 C 編譯器通常默認就是 C 調用約定(_cdcel),經常使用的調用約定有如下幾種:
顯式調用顯得很是複雜,每次都要LoadLibrary,而且每一個函數都必須使用GetProcAddress來獲得函數指針,這對於大量使用dll函數的客戶是一種困擾。而隱式調用可以像使用c函數庫同樣使用dll中的函數,很是方便快捷。
下面是一個隱式調用的例子:dll包含兩個文件dll_withlibAndH.cpp和dll_withlibAndH.h。
代碼以下:dll_withlibAndH.h
extern "C" __declspec(dllexport) void FuncInDll (void);
dll_withlibAndH.cpp
#include <objbase.h> #include <iostream.h> #include "dll_withLibAndH.h"//看到沒有,這就是咱們增長的頭文件 extern "C" __declspec(dllexport) void FuncInDll (void){ cout<<"FuncInDll is called!"<<endl; } BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved){ HANDLE g_hModule; switch(dwReason) { case DLL_PROCESS_ATTACH: g_hModule = (HINSTANCE)hModule; break; case DLL_PROCESS_DETACH: g_hModule=NULL; break; } return TRUE; }
編譯連接命令:
Cl /c dll_withlibAndH.cpp Link /dll dll_withlibAndH.obj
在進行隱式調用的時候須要在客戶端引入頭文件,並在連接時指明dll對應的lib文件(dll只要有函數輸出,則連接的時候會產生一個與dll同名的lib文件)位置和名稱。而後如同調用api函數庫中的函數同樣調用dll中的函數,不須要顯式的LoadLibrary和GetProcAddress。使用最爲方便。客戶端代碼以下:dll_withlibAndH_client.cpp
#include "dll_withLibAndH.h" //注意路徑,加載 dll的另外一種方法是 Project | setting | link 設置裏 #pragma comment(lib,"dll_withLibAndH.lib") int main(void){ FuncInDll();//只要這樣咱們就能夠調用dll裏的函數了 return 0; }
__declspec(dllexport)
和__declspec(dllimport)
配對使用
上面一種隱式調用的方法很不錯,可是在調用DLL中的對象和重載函數時會出現問題。由於使用extern 「C」修飾了輸出函數,所以重載函數確定是會出問題的,由於它們都將被編譯爲同一個輸出符號串(c語言是不支持重載的)。
事實上不使用extern 「C」是可行的,這時函數會被編譯爲c++符號串,例如(?FuncInDll@@YAXH@Z、 ?FuncInDll@@YAXXZ
),當客戶端也是c++時,也能正確的隱式調用。
這時要考慮一個狀況:若DLL1.CPP是源,DLL2.CPP使用了DLL1中的函數,但同時DLL2也是一個DLL,也要輸出一些函數供Client.CPP使用。那麼在DLL2中如何聲明全部的函數,其中包含了從DLL1中引入的函數,還包括本身要輸出的函數。這個時候就須要同時使用__declspec(dllexport)和__declspec(dllimport)了。前者用來修飾本dll中的輸出函數,後者用來修飾從其它dll中引入的函數。
值得關注的是DLL1和DLL2中都使用的一個編碼方法,見DLL2.H
#ifdef DLL_DLL2_EXPORTS #define DLL_DLL2_API __declspec(dllexport) #else #define DLL_DLL2_API __declspec(dllimport) #endif DLL_DLL2_API void FuncInDll2(void); DLL_DLL2_API void FuncInDll2(int);
在頭文件中以這種方式定義宏DLL_DLL2_EXPORTS和DLL_DLL2_API,能夠確保DLL端的函數用__declspec(dllexport)
修飾,而客戶端的函數用__declspec(dllimport)
修飾。固然,記得在編譯dll時加上參數/D 「DLL_DLL2_EXPORTS」,或者乾脆就在dll的cpp文件第一行加上#define DLL_DLL2_EXPORTS。
解決了重載函數的問題,那麼dll中的全局變量和對象都不是問題了,只是有一點語法須要注意。如源代碼所示:dll_object.h
#ifdef DLL_OBJECT_EXPORTS #define DLL_OBJECT_API __declspec(dllexport) #else #define DLL_OBJECT_API __declspec(dllimport) #endif DLL_OBJECT_API void FuncInDll(void); extern DLL_OBJECT_API int g_nDll; class DLL_OBJECT_API CDll_Object { public: CDll_Object(void); show(void); // TODO: add your methods here. };
Cpp文件dll_object.cpp以下:
#define DLL_OBJECT_EXPORTS #include <objbase.h> #include <iostream.h> #include "dll_object.h" DLL_OBJECT_API void FuncInDll(void){ cout<<"FuncInDll is called!"<<endl; } DLL_OBJECT_API int g_nDll = 9; CDll_Object::CDll_Object(){ cout<<"ctor of CDll_Object"<<endl; } CDll_Object::show(){ cout<<"function show in class CDll_Object"<<endl; } BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved){ HANDLE g_hModule; switch(dwReason) { case DLL_PROCESS_ATTACH: g_hModule = (HINSTANCE)hModule; break; case DLL_PROCESS_DETACH: g_hModule=NULL; break; } return TRUE; }
編譯連接完後Dumpbin一下,能夠看到輸出了5個符號:
1 0 00001040 ??0CDll_Object@@QAE@XZ 2 1 00001000 ??4CDll_Object@@QAEAAV0@ABV0@@Z 3 2 00001020 ?FuncInDll@@YAXXZ 4 3 00008040 ?g_nDll@@3HA 5 4 00001069 ?show@CDll_Object@@QAEHXZ
它們分別表明類CDll_Object,類的構造函數,FuncInDll函數,全局變量g_nDll和類的成員函數show。下面是客戶端代碼:dll_object_client.cpp
#include "dll_object.h" #include <iostream.h> //注意路徑,加載 dll的另外一種方法是 Project | setting | link 設置裏 #pragma comment(lib,"dll_object.lib") int main(void){ cout<<"call dll"<<endl; cout<<"call function in dll"<<endl; FuncInDll();//只要這樣咱們就能夠調用dll裏的函數了 cout<<"global var in dll g_nDll ="<<g_nDll<<endl; cout<<"call member function of class CDll_Object in dll"<<endl; CDll_Object obj; obj.show(); return 0; }
運行這個客戶端能夠看到:
call dll call function in dll FuncInDll is called! global var in dll g_nDll =9 call member function of class CDll_Object in dll ctor of CDll_Object function show in class CDll_Object
可知,在客戶端成功的訪問了dll中的全局變量,並建立了dll中定義的C++對象,還調用了該對象的成員函數。
牢記一點,說到底,DLL是對應C語言的動態連接技術,在輸出C函數和變量時顯得方便快捷;而在輸出C++類、函數時須要經過各類手段,並且也並無完美的解決方案,除非客戶端也是c++。
記住,只有COM是對應C++語言的技術。
下面開始對各個問題一一小結。
什麼時候使用顯式調用?什麼時候使用隱式調用?我認爲,只有一個時候使用顯式調用是合理的,就是當客戶端不是C/C++的時候。這時是沒法隱式調用的。例如用VB調用C++寫的dll。(VB我不會,因此沒有例子)
我認爲:顯示調用更能節省內存,並且效率更高。因此在大型項目中應該多用顯示調用的方法,下面從網上摘來一些更詳細的說明:
靜態調用方式所需的代碼較動態調用方式所需的少,但存在着一些不足,一是若是要加載的DLL不存在或者DLL中沒有要引入的例程,這時候程序就自動終止運行;二是 DLL一旦加載就一直駐留在應用程序的地址空間,即便DLL已再也不須要了。動態調用方式就可解決以上問題,它在須要用到DLL的時候才經過 LoadLibrary函數引入,用完後經過FreeLibrary函數從內存中卸載,並且經過調GetProcAddress函數能夠指定不一樣的例程。最重要的是,若是指定的DLL出錯,至可能是API調用失敗,不會致使程序終止。如下將經過具體的實例說明說明這調用方式的使用方法。 動態鏈接庫最大的特色就是能節省磁盤空間.當多個進程共享同一個DLL的時候,內存中只有一個DLL的代碼.經過映射來使各個進程得以調用.
調用DLL,首先須要將DLL文件映像到用戶進程的地址空間中,而後才能進行函數調用,這個函數和進程內部通常函數的調用方法相同。Windows提供了兩種將DLL映像到進程地址空間的方法:隱式調用(經過lib和頭文件)和顯式調用(只經過提供的dll文件)。下面對這兩種方式在vc中如何調用作詳細的說明:
一、隱式調用(也叫隱式連接)
這種方法須要DLL工程經編譯產生的LIB文件,此文件中包含了DLL容許應用程序調用的全部函數的列表,當連接器發現應用程序調用了LIB文件列出的某個函數,就會在應用程序的可執行文件的文件映像中加入一些信息,這些信息指出了包含這個函數的DLL文件的名字。當這個應用程序運行時,也就是它的可執行文件被操做系統產生映像文件時,系統會查看這個映像文件中關於DLL的信息,而後將這個DLL文件映像到進程的地址空間。
系統經過DLL文件的名稱,試圖加載這個文件到進程地址空間時,它尋找DLL 文件的路徑按照前後順序以下:
VC中加載DLL的LIB文件的方法有如下三種:
①LIB文件直接加入到工程文件列表中
在VC中打開 File View 一頁,選中工程名,單擊鼠標右鍵,而後選中 Add Files to Project 菜單,在彈出的文件對話框中選中要加入DLL的LIB文件便可。
②設置工程的 Project Settings 來加載DLL的LIB文件
打開工程的 Project Settings菜單,選中Link,而後在Object/library modules下的文本框中輸入DLL的LIB文件。
③經過程序代碼的方式
加入預編譯指令#pragma comment (lib,」*.lib」),這種方法優勢是能夠利用條件預編譯指令連接不一樣版本的LIB文件。由於,在Debug方式下,產生的LIB文件是Debug版本,如Regd.lib;在Release方式下,產生的LIB文件是Release版本,如Regr.lib。
當應用程序對DLL的LIB文件加載後,還須要把DLL對應的頭文件(*.h)包含到其中,在這個頭文件中給出了DLL中定義的函數原型,而後聲明。
二、顯式調用(也叫顯示加載)
隱式連接雖然實現較簡單,但除了必須的.dll文件外還須要DLL的.h文件和.lib文件,在那些只提供.dll文件的場合就沒法使用,而只能採用顯式連接的方式。這種方式經過調用API函數來完成對DLL的加載與卸載,其能更加有效地使用內存,在編寫大型應用程序時每每採用此方式。這種方法編程具體實現步驟以下:
①使用Windows API函數Load Library或者MFC提供的AfxLoadLibrary將DLL模塊映像到進程的內存空間,對DLL模塊進行動態加載。
②使用GetProcAddress函數獲得要調用DLL中的函數的指針。
③不用DLL時,用Free Library函數或者AfxFreeLibrary函數從進程的地址空間顯式卸載DLL。
Def和__declspec(dllexport)
其實def的功能至關於extern 「C」 __declspec(dllexport)
,因此它也僅能處理C函數,而不能處理重載函數。而__declspec(dllexport)
和__declspec(dllimport)
配合使用可以適應任何狀況,所以__declspec(dllexport)是更爲先進的方法。因此,目前廣泛的見解是不使用def文件,我也贊成這個見解。
從其它編程語言中調用DLL,有兩個最大的問題,第一個就是函數符號的問題,前面已經屢次提過了。這裏有個兩難選擇,若使用extern 「C」,則函數名稱保持不變,調用較方便,可是不支持函數重載等一系列c++功能;若不使用extern 「C」,則調用前要查看編譯後的符號,很是不方便。
第二個問題就是函數調用壓棧順序的問題,即__cdecl和__stdcall的問題。__cdecl是常規的C/C++調用約定,這種調用約定下,函數調用後棧的清理工做是由調用者完成的。__stdcall是標準的調用約定,即這些函數將在返回到調用者以前將參數從棧中刪除。
這兩個問題DLL都不能很好的解決,只能說湊合着用。可是在COM中,都獲得了完美的解決。因此,要在Windows平臺實現語言無關性,仍是隻有使用COM中間件。
總而言之,除非客戶端也使用C++,不然dll是不便於支持函數重載、類等c++特性的。DLL對c函數的支持很好,我想這也是爲何windows的函數庫使用C加dll實現的理由之一。
在VC中建立、編譯、連接dll是很是方便的,點擊file → New → Project → Win32 Dynamic-Link Library,輸入dll名稱dll_InVC而後點擊肯定。而後選擇A DLL that export some symbols,點擊Finish。便可獲得一個完整的DLL。
用Visual Sudio 6.0新建一個工程,工程的類型選擇Win32 Dynamic-Link Library.工程名任意,其餘全部選項取默認
新建一個cpp文件,代碼以下:
int add(int a ,int b){ return a+b; }
若是工程類型是Win32 Console Application,那麼在編譯連接之後,會產生一個Debug目錄,而且裏面有一個exe文件
這裏咱們的工程類型是Win32 Dynamic-Link Library,在編譯連接之後,咱們指望產生一個Debug目錄,而且裏面有一個dll文件
事實正是如此
咱們能夠用depends工具打開這個dll文件以查看它導出了什麼函數
depends工具在Tools菜單下.實際上它是D:\Program Files\Microsoft Visual Studio\Common\Tools
下的一個文件
咱們發現,這個dll沒有導出任何東西
這是由於咱們並無說明咱們要導出的東西.在那個cpp裏的函數並非默認會被導出的.由於它們可能只是被咱們要導出的函數的調用的"內部函數".
要導出一個函數,咱們需要加上_declspec(dllexport)
,代碼變爲:
int _declspec(dllexport) add(int a ,int b){ return a+b; }
再連接一次
再查看該dll文件,發現有一個?add@@YAHHH@Z的函數.好像很怪,不過總算看到東西了
如今來測試一下這個dll
新建一個工程,類型選Win32 Console Application
新建一個cpp文件,代碼以下
#include <iostream.h> #include <Windows.h> void main(){ typedef int (*ADD)(int ,int);//函數指針類型 HINSTANCE Hint = ::LoadLibrary("DLL.dll");//加載咱們剛纔生成的dll ADD add = (ADD)GetProcAddress(Hint,"add");//取得dll導出的add方法 cout<<add(3,4)<<endl; }
其中LoadLibrary都是Windows.h裏面聲明瞭的函數
編譯連接,都沒問題
運行.出錯了!
分析一下,程序怎麼知道去哪裏找咱們的dll呢?
它會按以下順序搜索:當前可執行模塊所在的目錄,當前目錄, Windows 系統目錄,Windows 目錄。GetWindowsDirectory 函數檢索此目錄的路徑,PATH 環境變量中列出的目錄。
因此咱們要把咱們的dll複製一份到這個測試工程的Debug目錄以後,再運行
仍是出錯了!
分析一下.咱們剛纔看到的是一個叫?add@@YAHHH@Z函數.那麼,是否是這個緣由呢?
把代碼改成:
#include <iostream.h> #include <Windows.h> void main(){ typedef int (*ADD)(int ,int);//函數指針類型 HINSTANCE Hint = ::LoadLibrary("DLL.dll");//加載咱們剛纔生成的dll ADD add = (ADD)GetProcAddress(Hint,"?add@@YAHHH@Z");//取得dll導出的add方法 cout<<add(3,4)<<endl; }
再編譯連接,運行,成功了!
那麼怎麼能夠正確導出咱們函數的名字呢?
在生成dll的工程的代碼加上extern "C",改成:
extern "C" int _declspec(dllexport) add(int a ,int b)...{ return a+b; }
編譯連接後,查看dll文件,能夠看到導出的函數變爲add了
這時下面代碼能夠正常工做了
#include <iostream.h> #include <Windows.h> void main()...{ typedef int (*ADD)(int ,int);//函數指針類型 HINSTANCE Hint = ::LoadLibrary("DLL.dll");//加載咱們剛纔生成的dll ADD add = (ADD)GetProcAddress(Hint,"add");//取得dll導出的add方法 cout<<add(3,4)<<endl; }
除了用_declspec(dllexport)
指明要導出的函數,用extern "C"來糾正名字,咱們還可用一個.def文件來達到以上目的
在dll工程裏新建一個文件,類型選Text File,在名字要帶上後綴.def
內容以下:
LIBRARY EXPORTS add
剩下的步驟就和以前同樣了
用def文件還能夠改變導出的函數的名字,例如
LIBRARY EXPORTS myadd = add
使得導出的函數叫myadd,而不是add
還能夠給函數指定一個序號
如:
LIBRARY EXPORTS myadd=add @4
給myadd指定了一個序號
在測試工程裏,能夠根據序號取得咱們的函數:
#include <iostream.h> #include <Windows.h> void main(){ typedef int (*ADD)(int,int); HINSTANCE hInstance=::LoadLibrary("DLL.dll"); ADD add=(ADD)GetProcAddress(hInstance,MAKEINTRESOURCE(4));//根據序號取得函數 cout<<add(3,4)<<endl; add=(ADD)GetProcAddress(hInstance,"myadd");//在def文件裏指定的名字 cout<<add(3,4)<<endl; FreeLibrary(hInstance);//釋放加載了的dll文件佔的資源 }
以上講的是運行時動態加載dll,下面講啓動時動態加載dll
產生dll的工程不用變,仍是上面這個(名字是myadd,序號爲4)
測試代碼改成:
//先把DLL.lib文件複製到本工程目錄裏 #include <iostream.h> #pragma comment(lib,"DLL.lib") extern int myadd(int ,int );//沒有加這句而只加上面這句(或在工程設置里加上DLL.lib)會連接錯誤 void main() { cout<<myadd(3,4)<<endl; }
這種方法調用dll,在連接的時候,會在咱們exe裏包含要引用的符號,在啓動程序的時候就會加載全部須要的dll.(以前說錯了,說這是靜態連接)
#pragma comment(lib,"DLL.lib")
指明瞭用到哪一個dll,其中DLL.lib能夠在Debug找到.咱們也要把DLL.lib複製到測試工程目錄(不是Debug目錄).咱們也能夠在工程屬性裏添加.方法是Project--Settings--Link,在Object/libraries Modules最後加上 DLL.lib
extern int add(int ,int )
指明瞭咱們的add是一個外部函數,而不是在本文件定義的
最後,強調一下,要把該複製的文件複製到正確的地方.
當你產生的dll文件和我說的不一致時,試一下選Build-Rebuild All
創建項目,請選擇Win32 控制檯項目(Win32 Console Application),選擇dll和空項目選項。
首先寫頭文件(header file),稱爲dllTutorial.h。這個文件與其它頭文件同樣,其中只是一些函數的原型。
#ifndef _dll_TUTORIAL_H_ #define _dll_TUTORIAL_H_ #include <iostream> #if defined dll_EXPORT #define DECLDIR __declspec(dllexport) #else #define DECLDIR __declspec(dllimport) #endif extern "C" { DECLDIR int Add( int a, int b ); DECLDIR void Function( void ); } #endif
此代碼中,前面兩行指示編譯器只包含這個文件一次。extern "C"告訴編譯器該部分能夠在C/C++中使用。
在VC++中這裏有兩個方法來導出函數:
一、使用__declspec
,一個Microsoft定義的關鍵字。
二、建立一個模塊定義文件(Module-Definition File即.DEF)。
第一種方法稍稍比第二種方法簡單些,但兩種都工做得很好。
接下來實現dll_Tutorial.cpp文件
#include <iostream> #include "dll_Tutorial.h" #define dll_EXPORT extern "C" { DECLDIR int Add( int a, int b ) { return( a + b ); } DECLDIR void Function( void ) { std::cout << "dll Called!" << std::endl; } }
模塊定義文件是一個有着.def文件擴展名的文本文件。它被用於導出一個dll的函數,和__declspec(dllexport)很類似,可是.def文件並非Microsoft定義的。一個.def文件中只有兩個必需的部分:LIBRARY 和 EXPORTS。讓咱們先看一個基本的.def文件稍後我將解析之。
LIBRARY dll_tutorial DESCRIPTION "our simple dll" EXPORTS Add @1 Function @2
第一行,LIBRARY
是一個必需的部分。它告訴連接器(linker)如何命名你的dll。下面被標識爲''DESCRIPTION''的部分並非必需的,可是我喜歡把它放進去。該語句將字符串寫入 .rdata 節,它告訴人們誰可能使用這個dll,這個dll作什麼或它爲了什麼(存在)。再下面的部分標識爲EXPORTS
是另外一個必需的部分;這個部分使得該函數能夠被其它應用程序訪問到而且它建立一個導入庫。此外,它還有其它四個部分:NAME, STACKSIZE, SECTIONS, 和VERSION。
建立一個新的空的Win32控制檯項目並添加一個源文件,將生成的dll放入你的新項目相同的目錄下。
連接到一個dll有兩種方式:
#pragma comment(lib, "dllTutorial.lib")
語句替代#include <iostream> #include <dllTutorial.h> int main() { Function(); std::cout << Add(32, 58) << "/n"; return(1); }
顯示連接加載dll的方式不須要引入dll所對應的頭文件,而僅需一個dll文件便可。
#include <iostream> #include <windows.h> typedef int (*AddFunc)(int,int); typedef void (*FunctionFunc)(); int main() { AddFunc _AddFunc; FunctionFunc _FunctionFunc; HINSTANCE hInstLibrary = LoadLibrary("dll_Tutorial.dll"); if (hInstLibrary == NULL) { FreeLibrary(hInstLibrary); } _AddFunc = (AddFunc)GetProcAddress(hInstLibrary, "Add"); _FunctionFunc = (FunctionFunc)GetProcAddress(hInstLibrary, "Function"); if ((_AddFunc == NULL) || (_FunctionFunc == NULL)) { FreeLibrary(hInstLibrary); } std::cout << _AddFunc(23, 43) << std::endl; _FunctionFunc(); std::cin.get(); FreeLibrary(hInstLibrary); return(1); }
在此文件中,開頭進行了函數聲明。
typedef int (*AddFunc)(int,int); typedef void (*FunctionFunc)();
一個HINSTANCE是一個Windows數據類型:是一個實例的句柄;在此狀況下,這個實例將是這個dll,能夠經過使用函數LoadLibrary()得到dll的實例,它得到一個名稱做爲參數。在調用LoadLibrary函數後,必須查看一下函數返回是否成功,這能夠經過檢查HINSTANCE是否等於NULL(在Windows.h中定義爲0或Windows.h包含的一個頭文件)來實現。若是其等於NULL,該句柄將是無效的,須要及時釋放這個庫。換句話說,必須釋放dll得到的內存。若是函數返回成功,你的HINSTANCE就包含了指向dll的句柄。
一旦你得到了指向dll的句柄,就能夠從dll中從新得到函數。爲了這樣作,你必須使用函數GetProcAddress(),它將dll的句柄(可使用HINSTANCE)和函數的名稱做爲參數,可讓函數指針得到由GetProcAddress()返回的值,同時必須將GetProcAddress()轉換爲那個函數定義的函數指針。例如,對於Add()函數,須要將GetProcAddress()轉換爲AddFunc。
一旦函數指針擁有dll的函數,你如今就可使用它們了,可是這裏有一個須要注意的地方:你不能使用函數的實際名稱;你必需使用函數指針來調用它們。在那之後,全部你須要作的是釋放庫。