在Visual Studio中使用C++建立和使用DLL
DLL是一個包含可由多個程式同時使用的程式碼和資料的庫。例如:在Windows作業系統中,Comdlg32 DLL執行與對話方塊有關的常見函式。因此,每個程式都可以使用該DLL中包含的功能來實現“開啟”對話方塊。這有助於促進程式碼重用和記憶體的有效使用。這篇文章的目的就是讓你一次性就能瞭解和掌握DLL。
為什麼要使用DLL(動態連結庫)?
程式碼複用是提高軟體開發效率的重要途徑。一般而言,只要某部分程式碼具有通用性,就可以將它構造成相對獨立的功能模組並在之後的專案中重複使用。比較常見的例子是各種應用程式框架,它們都以原始碼的形式釋出。由於這種複用是原始碼級別的,原始碼完全暴露給了程式設計師,因而稱之為“白盒複用”。白盒複用有以下三個缺點:
- 暴露原始碼,多份拷貝,造成儲存浪費;
- 容易與程式設計師的原生代碼發生命名衝突;
- 更新模組功能比較困難,不利於問題的模組化實現;
為了彌補這些不足,就提出了“二進位制級別”的程式碼複用了。使用二進位制級別的程式碼複用一定程度上隱藏了原始碼,對於“黑盒複用”的途徑不只DLL一種,靜態連結庫,甚至更高階的COM元件都是。
使用DLL主要有以下優點:
- 使用較少的資源;當多個程式使用同一函式庫時,DLL可以減少在磁碟和實體記憶體中載入的程式碼的重複量。這不僅可以大大影響在前臺執行的程式,而且可以大大影響其它在Windows作業系統上執行的程式;
- 推廣模組式體系結構;
- 簡化部署與安裝。
建立DLL
開啟Visual Studio 2012,建立如下圖的工程:
輸入工程名字,單擊[OK];
單擊[Finish],工程建立完畢了。現在,我們就可以在工程中加入我們的程式碼了。加入MyCode.h和MyCode.cpp兩個檔案;在MyCode.h中輸入以下程式碼:
[cpp]view plaincopy- #ifndef_MYCODE_H_
- #define_MYCODE_H_
- #ifdefDLLDEMO1_EXPORTS
- #defineEXPORTS_DEMO_declspec(dllexport)
- #else
- #defineEXPORTS_DEMO_declspec(dllimport)
- #endif
- extern"C"EXPORTS_DEMOintAdd(inta,intb);
- #endif
- #include"stdafx.h"
- #include"MyCode.h"
- intAdd(inta,intb)
- {
- return(a+b);
- }
使用DLL
當我們的程式需要使用DLL時,就需要去載入DLL,在程式中載入DLL有兩種方法,分別為載入時動態連結和執行時動態連結。
- 在載入時動態連結中,應用程式像呼叫本地函式一樣對匯出的DLL函式進行顯示呼叫。要使用載入時動態連結,需要在編譯和連結應用程式時提供標頭檔案和匯入庫檔案(.lib)。當這樣做的時候,連結器將向系統提供載入DLL所需的資訊,並在載入時解析匯出的DLL函式的位置;
- 在執行時動態連結中,應用程式呼叫LoadLibrary函式或LoadLibraryEx函式以在執行時載入DLL。成功載入DLL後,可以使用GetProcAddress函式獲得要呼叫的匯出的DLL函式的地址。在使用執行時動態連結時,不需要使用匯入庫檔案。
在實際程式設計時有兩種使用DLL的方法,那麼到底應該使用那一種呢?在實際開發時,是基於以下幾點進行考慮的:
- 啟動效能如果應用程式的初始啟動效能很重要,則應使用執行時動態連結;
- 易用性在載入時動態連結中,匯出的DLL函式類似於本地函式,我們可以方便地進行這些函式的呼叫;
- 應用程式邏輯在執行時動態連結中,應用程式可以分支,以便按照需要載入不同的模組。
下面,我將分別使用兩種方法呼叫DLL動態連結庫。
載入時動態連結:
[cpp]view plaincopy- #include<windows.h>
- #include<iostream>
- //#include"..\\DLLDemo1\\MyCode.h"
- usingnamespacestd;
- #pragmacomment(lib,"..\\debug\\DLLDemo1.lib")
- extern"C"_declspec(dllimport)intAdd(inta,intb);
- intmain(intargc,char*argv[])
- {
- cout<<Add(2,3)<<endl;
- return0;
- }
- #include<windows.h>
- #include<iostream>
- usingnamespacestd;
- typedefint(*AddFunc)(inta,intb);
- intmain(intargc,char*argv[])
- {
- HMODULEhDll=LoadLibrary("DLLDemo1.dll");
- if(hDll!=NULL)
- {
- AddFuncadd=(AddFunc)GetProcAddress(hDll,"Add");
- if(add!=NULL)
- {
- cout<<add(2,3)<<endl;
- }
- FreeLibrary(hDll);
- }
- }
上述程式碼都在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- LIBRARY"DLLDemo2"
- EXPORTS
- Add@1;ExporttheAddfunction
Module-Definition File(.def)檔案的格式如下:
- LIBRARY語句說明.def檔案對應的DLL;
- EXPORTS語句後列出要匯出函式的名稱。可以在.def檔案中的匯出函式名後加@n,表示要匯出函式的序號為n(在進行函式呼叫時,這個序號有一定的作用)。
使用def檔案,生成了DLL,客戶端呼叫程式碼如下:
[cpp]view plaincopy- #include<windows.h>
- #include<iostream>
- usingnamespacestd;
- typedefint(*AddFunc)(inta,intb);
- intmain(intargc,char*argv[])
- {
- HMODULEhDll=LoadLibrary("DLLDemo2.dll");
- if(hDll!=NULL)
- {
- AddFuncadd=(AddFunc)GetProcAddress(hDll,MAKEINTRESOURCE(1));
- if(add!=NULL)
- {
- cout<<add(2,3)<<endl;
- }
- FreeLibrary(hDll);
- }
- }
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的作用總結起來就是以下幾點:
- 在一個檔案內,如果外部變數不在檔案的開頭定義,其有效範圍只限定在從定義開始到檔案的結束處。如果在定義前需要引用該變數,則要在引用之前用關鍵字”extern”對該變數做”外部變數宣告”,表示該變數是一個已經定義的外部變數。有了這個宣告,就可以從宣告處起合理地使用該變量了,例如:
- /*
- **FileName:ExternDemo
- **Author:JellyYoung
- **Date:2013/11/18
- **Description:Moreinformation,pleasegotohttp://www.jellythink.com
- */
- #include<iostream>
- usingnamespacestd;
- intmain(intargc,char*argv[])
- {
- externinta;
- cout<<a<<endl;
- }
- inta=100;
- 在多檔案的程式中,如果多個檔案都要使用同一個外部變數,不能在各個檔案中各定義一個外部變數,否則會出現“重複定義”的錯誤。正確的做法是在任意一個檔案中定義外部變數,其它檔案用extern對變數做“外部變數宣告”。在編譯和連結時,系統會知道該變數是一個已經在別處定義的外部變數,並把另一檔案中外部變數的作用域擴充套件到本檔案,這樣在本檔案就可以合法地使用該外部變量了。寫過MFC程式的人都知道,在在CXXXApp類的標頭檔案中,就使用extern聲明瞭一個該類的變數,而該變數的實際定義是在CXXXApp類的實現檔案中完成的;
- 外部函式,在定義函式時,如果在最左端加關鍵字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
- #include<windows.h>
- #include<iostream>
- usingnamespacestd;
- typedefint(*AddFunc)(inta,intb);
- intmain(intargc,char*argv[])
- {
- HMODULEhDll=LoadLibrary("DLLDemo1.dll");
- if(hDll!=NULL)
- {
- AddFuncadd=(AddFunc)GetProcAddress(hDll,"?Add@@YAHHH@Z");
- if(add!=NULL)
- {
- cout<<add(2,3)<<endl;
- }
- FreeLibrary(hDll);
- }
- }
請注意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- #ifndef_MYCODE_H_
- #define_MYCODE_H_
- #ifdefDLLDEMO1_EXPORTS
- #defineEXPORTS_DEMO_declspec(dllexport)
- #else
- #defineEXPORTS_DEMO_declspec(dllimport)
- #endif
- extern"C"EXPORTS_DEMOintAdd(inta,intb);
- #endif
- #include"MyCode.h"
- intAdd(inta,intb)
- {
- return(a+b);
- }
2.載入時動態連結方式呼叫DLL.
首先建立控制檯應用程式,命名為DllTest,如下圖所示: `` 然後新增程式碼: [cpp]view plaincopy- //DllTest.cpp:定義控制檯應用程式的入口點。
- #include"stdafx.h"
- #include<iostream>
- //#include"..\\DLLDemo1\\MyCode.h"
- usingnamespacestd;
- #pragmacomment(lib,"..\\debug\\DllDemo.lib")//***********************************************************************問題1
- extern"C"_declspec(dllimport)intAdd(inta,intb);
- int_tmain(intargc,_TCHAR*argv[])
- {
- cout<<Add(2,3)<<endl;
- while(1);//程式執行到這,方便看執行結果
- return0;
- }
3.執行時動態連結方式呼叫DLL.
和第二步一樣,建立控制檯應用程式,命名為DllTest1,新增程式碼如下: [cpp]view plaincopy- //DllTest1.cpp:定義控制檯應用程式的入口點。
- //
- #include"stdafx.h"
- #include<iostream>
- #include<windows.h>
- usingnamespacestd;
- typedefint(*AddFunc)(inta,intb);
- int_tmain(intargc,_TCHAR*argv[])
- {
- HMODULEhDll=LoadLibrary(_T("DllDemo.dll"));
- if(hDll!=NULL)
- {
- AddFuncadd=(AddFunc)GetProcAddress(hDll,"Add");
- if(add!=NULL)
- {
- cout<<add(2,3)<<endl;
- }
- FreeLibrary(hDll);
- }
- while(1);
- }
4.以.def檔案(模組定義檔案)方式匯出函式(非_declspec(dllexport)方式匯出函式):
首先建立DLL工程專案,命名為DllDemo,如下圖: 然後建立標頭檔案(MyCode.h)和.cpp檔案(MyCode.cpp),並分別新增程式碼: MyCode.h標頭檔案: [cpp]view plaincopy- #ifndef_MYCODE_H_
- #define_MYCODE_H_
- extern"C"intAdd(inta,intb);
- #endif
- #include"MyCode.h"
- intAdd(inta,intb)
- {
- return(a+b);
- }
新增程式碼: [cpp]view plaincopy
- LIBRARY"DllDemo"//這裡的字串名和工程名要一致
- EXPORTS
- Add@1;ExporttheAddfunction
- //DllTest2.cpp:定義控制檯應用程式的入口點。
- //
- #include"stdafx.h"
- #include<windows.h>
- #include<iostream>
- usingnamespacestd;
- typedefint(*AddFunc)(inta,intb);
- int_tmain(intargc,_TCHAR*argv[])
- {
- HMODULEhDll=LoadLibrary("DllDemo.dll");
- if(hDll!=NULL)
- {
- AddFuncadd=(AddFunc)GetProcAddress(hDll,MAKEINTRESOURCE(1));
- if(add!=NULL)
- {
- cout<<add(2,3)<<endl;
- }
- FreeLibrary(hDll);
- }
- while(1);
- }