1. 程式人生 > 其它 >在Visual Studio中使用C++建立和使用DLL

在Visual Studio中使用C++建立和使用DLL

什麼是DLL(動態連結庫)?

DLL是一個包含可由多個程式同時使用的程式碼和資料的庫。例如:在Windows作業系統中,Comdlg32 DLL執行與對話方塊有關的常見函式。因此,每個程式都可以使用該DLL中包含的功能來實現“開啟”對話方塊。這有助於促進程式碼重用和記憶體的有效使用。這篇文章的目的就是讓你一次性就能瞭解和掌握DLL。

為什麼要使用DLL(動態連結庫)?

程式碼複用是提高軟體開發效率的重要途徑。一般而言,只要某部分程式碼具有通用性,就可以將它構造成相對獨立的功能模組並在之後的專案中重複使用。比較常見的例子是各種應用程式框架,它們都以原始碼的形式釋出。由於這種複用是原始碼級別的,原始碼完全暴露給了程式設計師,因而稱之為“白盒複用”。白盒複用有以下三個缺點:

  1. 暴露原始碼,多份拷貝,造成儲存浪費;
  2. 容易與程式設計師的原生代碼發生命名衝突;
  3. 更新模組功能比較困難,不利於問題的模組化實現;

為了彌補這些不足,就提出了“二進位制級別”的程式碼複用了。使用二進位制級別的程式碼複用一定程度上隱藏了原始碼,對於“黑盒複用”的途徑不只DLL一種,靜態連結庫,甚至更高階的COM元件都是。

使用DLL主要有以下優點:

  1. 使用較少的資源;當多個程式使用同一函式庫時,DLL可以減少在磁碟和實體記憶體中載入的程式碼的重複量。這不僅可以大大影響在前臺執行的程式,而且可以大大影響其它在Windows作業系統上執行的程式;
  2. 推廣模組式體系結構;
  3. 簡化部署與安裝。

建立DLL

開啟Visual Studio 2012,建立如下圖的工程:


輸入工程名字,單擊[OK];

單擊[Finish],工程建立完畢了。現在,我們就可以在工程中加入我們的程式碼了。加入MyCode.h和MyCode.cpp兩個檔案;在MyCode.h中輸入以下程式碼:

[cpp]view plaincopy
  1. #ifndef_MYCODE_H_
  2. #define_MYCODE_H_
  3. #ifdefDLLDEMO1_EXPORTS
  4. #defineEXPORTS_DEMO_declspec(dllexport)
  5. #else
  6. #defineEXPORTS_DEMO_declspec(dllimport)
  7. #endif
  8. extern"C"EXPORTS_DEMOintAdd(inta,intb);
  9. #endif
在MyCode.cpp中輸入以下程式碼: [cpp]view plaincopy
  1. #include"stdafx.h"
  2. #include"MyCode.h"
  3. intAdd(inta,intb)
  4. {
  5. return(a+b);
  6. }
編譯工程,就會生成DLLDemo1.dll檔案。在程式碼中,很多細節的地方,我稍後進行詳細的講解。

使用DLL

當我們的程式需要使用DLL時,就需要去載入DLL,在程式中載入DLL有兩種方法,分別為載入時動態連結和執行時動態連結。

  1. 在載入時動態連結中,應用程式像呼叫本地函式一樣對匯出的DLL函式進行顯示呼叫。要使用載入時動態連結,需要在編譯和連結應用程式時提供標頭檔案和匯入庫檔案(.lib)。當這樣做的時候,連結器將向系統提供載入DLL所需的資訊,並在載入時解析匯出的DLL函式的位置;
  2. 在執行時動態連結中,應用程式呼叫LoadLibrary函式或LoadLibraryEx函式以在執行時載入DLL。成功載入DLL後,可以使用GetProcAddress函式獲得要呼叫的匯出的DLL函式的地址。在使用執行時動態連結時,不需要使用匯入庫檔案。

在實際程式設計時有兩種使用DLL的方法,那麼到底應該使用那一種呢?在實際開發時,是基於以下幾點進行考慮的:

  1. 啟動效能如果應用程式的初始啟動效能很重要,則應使用執行時動態連結;
  2. 易用性在載入時動態連結中,匯出的DLL函式類似於本地函式,我們可以方便地進行這些函式的呼叫;
  3. 應用程式邏輯在執行時動態連結中,應用程式可以分支,以便按照需要載入不同的模組。

下面,我將分別使用兩種方法呼叫DLL動態連結庫。

載入時動態連結:

[cpp]view plaincopy
  1. #include<windows.h>
  2. #include<iostream>
  3. //#include"..\\DLLDemo1\\MyCode.h"
  4. usingnamespacestd;
  5. #pragmacomment(lib,"..\\debug\\DLLDemo1.lib")
  6. extern"C"_declspec(dllimport)intAdd(inta,intb);
  7. intmain(intargc,char*argv[])
  8. {
  9. cout<<Add(2,3)<<endl;
  10. return0;
  11. }
執行時動態連結: [cpp]view plaincopy
  1. #include<windows.h>
  2. #include<iostream>
  3. usingnamespacestd;
  4. typedefint(*AddFunc)(inta,intb);
  5. intmain(intargc,char*argv[])
  6. {
  7. HMODULEhDll=LoadLibrary("DLLDemo1.dll");
  8. if(hDll!=NULL)
  9. {
  10. AddFuncadd=(AddFunc)GetProcAddress(hDll,"Add");
  11. if(add!=NULL)
  12. {
  13. cout<<add(2,3)<<endl;
  14. }
  15. FreeLibrary(hDll);
  16. }
  17. }

上述程式碼都在DLLDemo1工程中。(工程下載)。

DllMain函式

Windows在載入DLL時,需要一個入口函式,就像控制檯程式需要main函式一樣。有的時候,DLL並沒有提供DllMain函式,應用程式也能成功引用DLL,這是因為Windows在找不到DllMain的時候,系統會從其它執行庫中引入一個不做任何操作的預設DllMain函式版本,並不意味著DLL可以拋棄DllMain函式。

根據編寫規範,Windows必須查詢並執行DLL裡的DllMain函式作為載入DLL的依據,它使得DLL得以保留在記憶體裡。這個函式並不屬於匯出函式,而是DLL的內部函式,這就說明不能在客戶端直接呼叫DllMain函式,DllMain函式是自動被呼叫的。

DllMain函式在DLL被載入和解除安裝時被呼叫,在單個執行緒啟動和終止時,DllMain函式也被呼叫。引數ul_reason_for_call指明瞭呼叫DllMain的原因,有以下四種情況:

DLL_PROCESS_ATTACH:當一個DLL被首次載入程序地址空間時,系統會呼叫該DLL的DllMain函式,傳遞的ul_reason_for_call引數值為DLL_PROCESS_ATTACH。這種情況只有首次對映DLL時才發生;

DLL_THREAD_ATTACH:該通知告訴所有的DLL執行執行緒的初始化。當程序建立一個新的執行緒時,系統會檢視程序地址空間中所有的DLL檔案對映,之後用DLL_THREAD_ATTACH來呼叫DLL中的DllMain函式。要注意的是,系統不會為程序的主執行緒使用值DLL_THREAD_ATTACH來呼叫DLL中的DllMain函式;

DLL_PROCESS_DETACH:當DLL從程序的地址空間解除對映時,引數ul_reason_for_call引數值為DLL_PROCESS_DETACH。當DLL處理DLL_PROCESS_DETACH時,DLL應該處理與程序相關的清理操作。如果程序的終結是因為系統中有某個執行緒呼叫了TerminateProcess來終結的,那麼系統就不會用DLL_PROCESS_DETACH來呼叫DLL中的DllMain函式來執行程序的清理工作。這樣就會造成資料丟失;

DLL_THREAD_DETACH:該通知告訴所有的DLL執行執行緒的清理工作。注意的是如果執行緒的終結是使用TerminateThread來完成的,那麼系統將不會使用值DLL_THREAD_DETACH來執行執行緒的清理工作,這也就是說可能會造成資料丟失,所以不要使用TerminateThread來終結執行緒。以上所有講解在工程DLLMainDemo(工程下載)都有體現。

函式匯出方式

在DLL的建立過程中,我使用的是_declspec( dllexport )方式匯出函式的,其實還有另一種匯出函式的方式,那就是使用匯出檔案(.def)。你可以在DLL工程中,新增一個Module-Definition File(.def)檔案。.def檔案為連結器提供了有關被連結器程式的匯出、屬性及其它方面的資訊。

對於上面的例子,.def可以是這樣的:

[cpp]view plaincopy
  1. LIBRARY"DLLDemo2"
  2. EXPORTS
  3. Add@1;ExporttheAddfunction

Module-Definition File(.def)檔案的格式如下:

  1. LIBRARY語句說明.def檔案對應的DLL;
  2. EXPORTS語句後列出要匯出函式的名稱。可以在.def檔案中的匯出函式名後加@n,表示要匯出函式的序號為n(在進行函式呼叫時,這個序號有一定的作用)。

使用def檔案,生成了DLL,客戶端呼叫程式碼如下:

[cpp]view plaincopy
  1. #include<windows.h>
  2. #include<iostream>
  3. usingnamespacestd;
  4. typedefint(*AddFunc)(inta,intb);
  5. intmain(intargc,char*argv[])
  6. {
  7. HMODULEhDll=LoadLibrary("DLLDemo2.dll");
  8. if(hDll!=NULL)
  9. {
  10. AddFuncadd=(AddFunc)GetProcAddress(hDll,MAKEINTRESOURCE(1));
  11. if(add!=NULL)
  12. {
  13. cout<<add(2,3)<<endl;
  14. }
  15. FreeLibrary(hDll);
  16. }
  17. }
可以看到,在呼叫GetProcAddress函式時,傳入的第二個引數是MAKEINTRESOURCE(1),這裡面的1就是def檔案中對應函式的序號。(工程下載

extern “C”

為什麼要使用extern “C”呢?C++之父在設計C++時,考慮到當時已經存在了大量的C程式碼,為了支援原來的C程式碼和已經寫好的C庫,需要在C++中儘可能的支援C,而extern “C”就是其中的一個策略。在宣告函式時,注意到我也使用了extern “C”,這裡要詳細的說說extern “C”。

extern “C”包含兩層含義,首先是它修飾的目標是”extern”的;其次,被它修飾的目標才是”C”的。先來說說extern;在C/C++中,extern用來表明函式和變數作用範圍(可見性)的關鍵字,這個關鍵字告訴編譯器,它申明的函式和變數可以在本模組或其它模組中使用。extern的作用總結起來就是以下幾點:

  1. 在一個檔案內,如果外部變數不在檔案的開頭定義,其有效範圍只限定在從定義開始到檔案的結束處。如果在定義前需要引用該變數,則要在引用之前用關鍵字”extern”對該變數做”外部變數宣告”,表示該變數是一個已經定義的外部變數。有了這個宣告,就可以從宣告處起合理地使用該變量了,例如:
[cpp]view plaincopy
  1. /*
  2. **FileName:ExternDemo
  3. **Author:JellyYoung
  4. **Date:2013/11/18
  5. **Description:Moreinformation,pleasegotohttp://www.jellythink.com
  6. */
  7. #include<iostream>
  8. usingnamespacestd;
  9. intmain(intargc,char*argv[])
  10. {
  11. externinta;
  12. cout<<a<<endl;
  13. }
  14. inta=100;
  1. 在多檔案的程式中,如果多個檔案都要使用同一個外部變數,不能在各個檔案中各定義一個外部變數,否則會出現“重複定義”的錯誤。正確的做法是在任意一個檔案中定義外部變數,其它檔案用extern對變數做“外部變數宣告”。在編譯和連結時,系統會知道該變數是一個已經在別處定義的外部變數,並把另一檔案中外部變數的作用域擴充套件到本檔案,這樣在本檔案就可以合法地使用該外部變量了。寫過MFC程式的人都知道,在在CXXXApp類的標頭檔案中,就使用extern聲明瞭一個該類的變數,而該變數的實際定義是在CXXXApp類的實現檔案中完成的;
  2. 外部函式,在定義函式時,如果在最左端加關鍵字extern,表示此函式是外部函式。C語言規定,如果在定義時省略extern,則隱含為外部函式。而內部函式必須在前面加static關鍵字。在需要呼叫此函式的檔案中,用extern對函式作宣告,表明該函式是在其它檔案中定義的外部函式。

接著說”C”的含義。我們都知道C++通過函式引數的不同型別支援過載機制,編譯器根據引數為每個過載函式產生不同的內部識別符號;但是,如果遇到了C++程式要呼叫已經被編譯後的C函式,那該怎麼辦呢?比如上面的int Add ( int a , int b )函式。該函式被C編譯器後在庫中的名字為_Add,而C++編譯器則會生成像_Add_int_int之類的名字用來支援函式過載和型別安全。由於編譯後的名字不同,C++程式不能直接呼叫C函式,所以C++提供了一個C連線交換指定符號extern “C”來解決這個問題;所以,在上面的DLL中,Add函式的宣告格式為:extern “C” EXPORTS_DEMO int Add (int a , int b)。這樣就告訴了C++編譯器,函式Add是個C連線的函式,應該到庫中找名字_Add,而不是找_Add_int_int。當我們將上面DLL中的”C”去掉,編譯生成新的DLL,使用Dependency Walker工具檢視該DLL,如圖:


請注意匯出方式為C++,而且匯出的Add函式的名字添加了很多的東西,當使用這種方式匯出時,客戶端呼叫時,程式碼就是下面這樣:



[cpp]view plaincopy





  1. #include<windows.h>
  2. #include<iostream>
  3. usingnamespacestd;
  4. typedefint(*AddFunc)(inta,intb);
  5. intmain(intargc,char*argv[])
  6. {
  7. HMODULEhDll=LoadLibrary("DLLDemo1.dll");
  8. if(hDll!=NULL)
  9. {
  10. AddFuncadd=(AddFunc)GetProcAddress(hDll,"?Add@@YAHHH@Z");
  11. if(add!=NULL)
  12. {
  13. cout<<add(2,3)<<endl;
  14. }
  15. FreeLibrary(hDll);
  16. }
  17. }

請注意GetProcAddress函式的第二個引數,該引數名就是匯出的函式名,在編碼時,寫這樣一個名字是不是很奇怪啊。當我們使用extern “C”方式匯出時,截圖如下:

注意匯出方式為C,而且函式名現在就是普通的Add了。我們再使用GetProcAddress時,就可以直接指定Add了,而不用再加那一長串奇怪的名字了。

DLL匯出變數

DLL定義的全域性變數可以被呼叫程序訪問;DLL也可以訪問呼叫程序的全域性資料。

DLL匯出類

DLL中定義的類,也可以被匯出。詳細工程程式碼,請參見(工程下載

總結

對DLL的講解就到此結束,由於MFC在現在的環境下使用較少,此處不予講解,如果以後做專案遇到了MFC的DLL相關知識,我再做總結。最後,希望大家給我的部落格提出一些中肯的建議。

本文版權歸果凍說所有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連結,否則保留追究法律責任的權利。如果這篇文章對你有幫助,你可以請我喝杯咖啡。»本文連結:http://www.jellythink.com/archives/111»訂閱本站:http://www.jellythink.com/feed »轉載請註明來源:果凍想»<a rel="bookmark" title="在Visual Studio中使用C++建立和使用DLL"《在Visual Studio中使用C++建立和使用DLL》

******************************************************************************************************

參考上面的內容,自己在VS2010開發環境上測試了一遍,測試步驟如下:

1.將所需要的函式封裝成DLL.

首先建立DLL工程專案,命名為DllDemo,如下圖: 然後建立標頭檔案(MyCode.h)和.cpp檔案(MyCode.cpp),並分別新增程式碼: MyCode.h標頭檔案: [cpp]view plaincopy
  1. #ifndef_MYCODE_H_
  2. #define_MYCODE_H_
  3. #ifdefDLLDEMO1_EXPORTS
  4. #defineEXPORTS_DEMO_declspec(dllexport)
  5. #else
  6. #defineEXPORTS_DEMO_declspec(dllimport)
  7. #endif
  8. extern"C"EXPORTS_DEMOintAdd(inta,intb);
  9. #endif
MyCode.cpp檔案: [cpp]view plaincopy
  1. #include"MyCode.h"
  2. intAdd(inta,intb)
  3. {
  4. return(a+b);
  5. }
編譯工程,就會在Debug檔案下生成DllDemo.dll檔案。

2.載入時動態連結方式呼叫DLL.

首先建立控制檯應用程式,命名為DllTest,如下圖所示: `` 然後新增程式碼: [cpp]view plaincopy
  1. //DllTest.cpp:定義控制檯應用程式的入口點。
  2. #include"stdafx.h"
  3. #include<iostream>
  4. //#include"..\\DLLDemo1\\MyCode.h"
  5. usingnamespacestd;
  6. #pragmacomment(lib,"..\\debug\\DllDemo.lib")//***********************************************************************問題1
  7. extern"C"_declspec(dllimport)intAdd(inta,intb);
  8. int_tmain(intargc,_TCHAR*argv[])
  9. {
  10. cout<<Add(2,3)<<endl;
  11. while(1);//程式執行到這,方便看執行結果
  12. return0;
  13. }
執行結果如下圖: 注意:匯入庫檔案的目錄必須在本工程的目錄下,也就是說要把生成的dll和lib檔案都要拷貝到該工程的目錄下,因為不再該目錄下,儘管修改了路徑,仍然提示找不到DllDemo.dll,不知道為什麼?

3.執行時動態連結方式呼叫DLL.

和第二步一樣,建立控制檯應用程式,命名為DllTest1,新增程式碼如下: [cpp]view plaincopy
  1. //DllTest1.cpp:定義控制檯應用程式的入口點。
  2. //
  3. #include"stdafx.h"
  4. #include<iostream>
  5. #include<windows.h>
  6. usingnamespacestd;
  7. typedefint(*AddFunc)(inta,intb);
  8. int_tmain(intargc,_TCHAR*argv[])
  9. {
  10. HMODULEhDll=LoadLibrary(_T("DllDemo.dll"));
  11. if(hDll!=NULL)
  12. {
  13. AddFuncadd=(AddFunc)GetProcAddress(hDll,"Add");
  14. if(add!=NULL)
  15. {
  16. cout<<add(2,3)<<endl;
  17. }
  18. FreeLibrary(hDll);
  19. }
  20. while(1);
  21. }


執行結果如下圖:


4.以.def檔案(模組定義檔案)方式匯出函式(非_declspec(dllexport)方式匯出函式):

首先建立DLL工程專案,命名為DllDemo,如下圖: 然後建立標頭檔案(MyCode.h)和.cpp檔案(MyCode.cpp),並分別新增程式碼: MyCode.h標頭檔案: [cpp]view plaincopy
  1. #ifndef_MYCODE_H_
  2. #define_MYCODE_H_
  3. extern"C"intAdd(inta,intb);
  4. #endif
MyCode.cpp檔案: [cpp]view plaincopy
  1. #include"MyCode.h"
  2. intAdd(inta,intb)
  3. {
  4. return(a+b);
  5. }
然後新增模組定義檔案(.def檔案):

新增程式碼: [cpp]view plaincopy
  1. LIBRARY"DllDemo"//這裡的字串名和工程名要一致
  2. EXPORTS
  3. Add@1;ExporttheAddfunction
編譯工程,即刻生成DllDemo.dll檔案。 使用def檔案,生成了DLL,客戶端呼叫程式碼如下: [cpp]view plaincopy
  1. //DllTest2.cpp:定義控制檯應用程式的入口點。
  2. //
  3. #include"stdafx.h"
  4. #include<windows.h>
  5. #include<iostream>
  6. usingnamespacestd;
  7. typedefint(*AddFunc)(inta,intb);
  8. int_tmain(intargc,_TCHAR*argv[])
  9. {
  10. HMODULEhDll=LoadLibrary("DllDemo.dll");
  11. if(hDll!=NULL)
  12. {
  13. AddFuncadd=(AddFunc)GetProcAddress(hDll,MAKEINTRESOURCE(1));
  14. if(add!=NULL)
  15. {
  16. cout<<add(2,3)<<endl;
  17. }
  18. FreeLibrary(hDll);
  19. }
  20. while(1);
  21. }
工程程式碼下載: 1.生成動態連結庫(_declspec(dllexport)方式匯出函式) 2.生成動態連結庫(以.def檔案(模組定義檔案)方式匯出函式) 3.以載入時動態連結方式呼叫DLL 4.以執行時動態連結方式呼叫DLL 5.以模組定義方式(.def檔案)建立的動態連結庫的呼叫

遇到的問題:

1.庫匯入的時候目錄的問題。對應文中的問題1,後面有解釋。 2.字符集的問題(是Unicode字符集還是多位元組集),兩種方案,一種修改字符集為多位元組集,二是將字串前面加_T(""),文中問題2 3.不知道怎麼通過模組定義檔案方式生成DLL,通過看參考部落格的程式碼找到了答案,主要修改標頭檔案,和新增模組定義檔案。 4.模組定義檔案中的庫檔名應和工程名一致。