1、一個程序從源文件編譯生成可執行文件的步驟:
預編譯 --> 編譯 --> 匯編 --> 鏈接
(1)預編譯主要處理在源代碼文件中以“#”開始的預編譯指令,如宏展開、處理條件編譯指令、處理#include指令等。
(2)編譯過程就是把預處理完的文件進行一系列詞法分析、語法分析、語義分析以及優化后生成相應的匯編代碼文件。
(3)匯編是將匯編代碼轉變成機器指令。
(4)鏈接主要是把分散的數據和代碼收集並合成一個單一的可加載並可執行的的文件。鏈接可以發生在代碼靜態編譯、程序被加載時以及程序執行時。
鏈接過程的主要工作是符號解析和重定位。
2、庫
庫是一組目標文件的包,就是一些最常用的代碼編譯成目標文件后打包存放。而最常見的庫就是運行時庫(Runtime Library),如C運行庫CRT.
庫一般分為兩種:
靜態庫(.a 、.lib)
動態庫(.so 、.dll )
所謂靜態、動態是指鏈接過程。
3、靜態鏈接與靜態鏈接庫
靜態鏈接:源程序經過編譯器生成目標文件,目標文件和庫一起生成最終的可執行文件。鏈接的過程就是把分布在各個可重定位的目標文件中相應的節合並起來,同時完成符號解析和重定位。
靜態庫:一組目標文件的集合,即很多目標文件經過壓縮打包后形成的一個文件。
靜態庫可以作為鏈接器的輸入,如果程序需要引用靜態庫提供的某個函數,鏈接時只需要在命令行中輸入該庫。連接器只拷貝被程序引用的目標模塊,以及該目標模塊索依賴的那些目標模塊。
特點:
(1)靜態鏈接是在編譯時期完成的
(2)靜態鏈接形成可執行文件后,運行時與靜態庫再無關系,方便移植。
(3)浪費內存和磁盤空間。通過靜態鏈接產生可執行程序時,會復制所依賴的靜態庫中所有數據和代碼到該可執行程序中。
(4)更新困難。當程序所依賴的靜態庫有任何更新,整個程序就要重新鏈接。
Windows下創建與使用靜態庫:
以VS2010為例
編寫一個簡單的計算器類,編成靜態庫供其他程序使用,代碼如下:
ifndef CALCULATOR_H_ #define CALCULATOR_H_ extern double g_max_number; double GetMinNumber(); class Calculator { public: double Add(double a, double b) const; double Sub(double num1, double num2) const; double Mul(double num1, double num2) const; double Div(double num1, double num2) const; }; #endif
cpp文件
#include "calculator.h" double g_max_number = 999; double Calculator::Add( double a, double b ) const { return (a + b); } double Calculator::Sub( double num1, double num2 ) const { return (num1 - num2); } double Calculator::Mul( double num1, double num2 ) const { return (num1 * num2); } double Calculator::Div( double num1, double num2 ) const { if (num2 != 0) { return (num1 / num2); } return 0; } double GetMinNumber() { return (-999); }
在創建VS工程時,在設置工程屬性時,配置類型選擇靜態庫即可,build即可生成靜態庫。
使用靜態庫:
需要.h .lib文件
(1)添加頭文件包含目錄
(2)設置所依賴的庫目錄
(3)添加所依賴的Lib文件,在Additional Dependencies中輸入庫名稱即可。
注:直接在源代碼中加入代碼 #pragma comment(lib,"mylib.lib") 也可以。
設置完這三步后,就可以正常使用靜態庫中的類、函數、變量等。
4、動態鏈接與動態庫
引入動態庫的原因:
如上所述靜態庫有兩個重大缺點:
1)空間浪費
2)靜態鏈接對程序的更新、部署和發布會帶來很多麻煩。一旦程序中有任何模塊更新,整個程序就要重新鏈接,發布給用戶。
動態鏈接的基本思想:把程序按照模塊拆分成各個相對獨立的部分,在程序運行時才將它們鏈接在一起形成一個完整的程序,而不是想靜態鏈接一樣把所有的程序模塊都鏈接成一個單獨的可執行文件。
特點:
1)代碼共享,所有引用該動態庫的可執行目標文件共享一份相同的代碼與數據。
2)程序升級方便,應用程序不需要重新鏈接新版本的動態庫來升級,理論上只要簡單地將舊的目標文件覆蓋掉。
3)在運行時可以動態地選擇加載各種應用程序模塊
下面重點介紹Windows下動態鏈接庫DLL.
DLL即動態鏈接庫(Dynamic-Link Libaray)的縮寫,相當於Linux下的共享對象。Windows系統中大量采用了DLL機制,甚至內核的結構很大程度依賴與DLL機制。Windows下的DLL文件和EXE文件實際上是一個概念,都是PE格式的二進制文件。
為了更好的理解DLL,首先介紹一下導出和導入的概念。
(1)導出與導入
在ELF(Linux下動態庫的格式),共享庫中所有的全局函數和變量在默認情況下都可以被其他模塊使用,即ELF默認導出所有的全局符號。DLL不同,需要顯式地“告訴”編譯器需要導出某個符號,否則編譯器默認所有的符號都不導出。
程序使用DLL的過程其實是引用DLL中導出函數和符號的過程,即導入過程。對於從其他DLL導入的符號,需要使用“__declspec(dllimport)”顯式聲明某個符號為導入符號。在ELF中,使用外部符號時,不需要額外聲明該符號是從其他共享對象導入的。
指定符號的導入導出一般有如下兩種方法:
1)MSVC編譯器提供了一系列C/C++的擴展來指定符號的導入導出,即__declspec屬性關鍵字。
__declspec(dllexport) 表示該符號是從本DLL導出的符號
__declspec(dllimport) 表示該符號是從別的DLL中導入的
2)使用“.def”文件來聲明導入到導出符號,詳細參考《程序員的自我修養--鏈接、裝載與庫》。
(2)創建動態庫
還是上面的那個簡單計算器程序的例子,創建一個動態庫供其他程序使用。
建立一個VS Win32工程,在創建向導中選擇DLL ,如下:
可以看到生成了一個dllmain.cpp文件,里面有一個DllMain函數,是DLL的入口函數,一般不需要做修改。入口函數如下:
BOOL APIENTRY DllMain( HMODULE hModule, 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; }
添加自己的代碼CalculatorDll.h
#ifdef CALCULATORDLL_EXPORTS #define CALCULATORDLL_API __declspec(dllexport) #else #define CALCULATORDLL_API __declspec(dllimport) #endif // This class is exported from the CalculatorDll.dll class CALCULATORDLL_API CCalculatorDll { public: CCalculatorDll(void); // TODO: add your methods here. double Add(double a, double b) const; double Sub(double num1, double num2) const; double Mul(double num1, double num2) const; double Div(double num1, double num2) const; }; extern CALCULATORDLL_API int nCalculatorDll; CALCULATORDLL_API int fnCalculatorDll(void);
cpp文件
#include "CalculatorDll.h" // This is an example of an exported variable CALCULATORDLL_API int nCalculatorDll=0; // This is an example of an exported function. CALCULATORDLL_API int fnCalculatorDll(void) { return 42; } // This is the constructor of a class that has been exported. // see CalculatorDll.h for the class definition CCalculatorDll::CCalculatorDll() { return; } double CCalculatorDll::Add( double a, double b ) const { return (a + b); } double CCalculatorDll::Sub( double num1, double num2 ) const { return (num1 - num2); } double CCalculatorDll::Mul( double num1, double num2 ) const { return (num1 * num2); } double CCalculatorDll::Div( double num1, double num2 ) const { if (num2 != 0) { return (num1 / num2); } return 0; }
1)在這里使用__declspec擴展來聲明的類、函數和變量的導入導出。
#ifdef CALCULATORDLL_EXPORTS
#define CALCULATORDLL_API __declspec(dllexport)
#else
#define CALCULATORDLL_API __declspec(dllimport)
#endif
在DLL工程中,默認定義了宏CALCULATORDLL_EXPORTS ,CALCULATORDLL_API 為__declspec(dllexport),用來聲明導出。
在使用該DLL的其他工程中,沒有定義CALCULATORDLL_EXPORTS宏,CALCULATORDLL_API為 __declspec(dllimport),用來聲明導入。
2)動態鏈接庫的生成文件
動態鏈接一般會生成兩個文件 .dll 和 .lib
.dll : 動態鏈接庫
.lib: 導入庫
靜態鏈接的.lib : 一組目標文件的集合,包含代碼和數據。
動態鏈接的.lib : 並不包含代碼與數據,它包含了.dll中的導出符號以及一部分樁代碼。
該LIB包含了函數所在的DLL文件和文件中函數位置的信息(索引),函數實現代碼由運行時加載在進程空間中的DLL提供。
一句話總結:.lib是編譯鏈接時使用的,dll是運行時使用的。
(3)使用動態庫
1)隱式的調用
隱式的調用是指可執行程序的源代碼通過函數名直接調用DLL的導出函數,調用方法和調用程序內部的其他函數是一樣的。
隱式調用須用到 .h .lib(編譯時) .dll(運行時)
當建立DLL時,會自動生成一個與之對對應的lib導入庫。該庫文件包含了DLL的導出函數的符號名和可選的序列號,但不包含實際的代碼。在構建可執行應用時,需要把該導入庫與構建可執行程序的目標文件進行靜態鏈接。通過鏈接該導入庫,鏈接器可以解析可執行程序的所有外部符號的引用,並且把相應的DLL文件名(不是完整路徑)存儲在可執行應用文件內部。
隱式調用時,被應用程序使用的DLL都會在可執行應用EXE被加載時加載到內存中。
隱式鏈接的使用方式與靜態庫的使用類似:
a)添加頭文件包含目錄
b)設置導入庫目錄
c)添加導入庫Lib文件或在程序中使用 #pragma comment(lib,"CalculatorDll.lib")
2)顯式調用
在可執行應用運行時,顯示加載需要的DLL並且顯式連接到需要的輸出符號。
Windows提供了3各API來完成運行時加載:
LoadLibrary(LoadLibraryEx) :裝載一個DLL到進程的地址空間,返回HINSTANCE值用於標識DLL文件映像映射到的虛擬地址,如果加載失敗返回NULL.
GetProcAddress : 用來查找某個符號的地址,該函數將符號名或序列號轉換為DLL內部的地址。
FreeLibrary: 用來卸載已加載的DLL.
顯示調用一般只需要使用.dll即可。
顯式調用DLL示例:
顯式加載CalculatorDll.dll,並調用fnCalculatorDll函數。
注意:顯式調用DLL時,fnCalculatorDll要聲明為
extern "C" CALCULATORDLL_API int fnCalculatorDll(void);
//加extern "C",是為了使C++的名稱修飾機制將不起作用,保證編譯時生成的函數名不變,這樣動態調用dll時才能正確獲取函數的地址
如下:
#ifdef CALCULATORDLL_EXPORTS #define CALCULATORDLL_API __declspec(dllexport) #else #define CALCULATORDLL_API __declspec(dllimport) #endif // This class is exported from the CalculatorDll.dll class CALCULATORDLL_API CCalculatorDll { public: CCalculatorDll(void); // TODO: add your methods here. double Add(double a, double b) const; double Sub(double num1, double num2) const; double Mul(double num1, double num2) const; double Div(double num1, double num2) const; }; extern CALCULATORDLL_API int nCalculatorDll; //加extern "C",是為了使C++的名稱修飾機制將不起作用,保證編譯時生成的函數名不變,這樣動態調用dll時才能正確獲取函數的地址 extern "C" CALCULATORDLL_API int fnCalculatorDll(void);
顯示加載Dll:
typedef int (*DLLFunc)(void); int _tmain(int argc, _TCHAR* argv[]) { //加載DLL HINSTANCE dllHandle = LoadLibrary(_T("CalculatorDll.dll")); if (NULL == dllHandle) { cout << "Load dll failed!" << endl; return; } //根據符號名得到函數地址 DLLFunc f = (DLLFunc)GetProcAddress(dllHandle,"fnCalculatorDll"); if (NULL == f) { cout << "Unable find dll function!" << endl; } else { cout << "Result:" << f() << endl; } //卸載DLL FreeLibrary(dllHandle); return 0; }
3)顯式調用C++動態庫
一個DLL可以導出C++類,聲明導出類很簡單在該類前加上__declspec(dllexport),使用導出類的程序須聲明該類為__declspec(dllimport).
隱式調用很簡單,包含頭文件、設置導入庫。然后跟使用普通類一樣就可以了。
通過LoadLibrary顯式調用函數,如上所述方法很簡單。通過GetProcAddress獲得函數的在DLL的地址就可以訪問了。但DLL中的Class訪問就相對很復雜了。
在目前的項目中常用的一種做法是:
繼承接口基類,將需要在外部使用的類成員函數在基類中聲明為虛,子類實現這些函數,通過導出函數返回基類指針。利用運行時多態就可以通過該指針來訪問這些函數。
該方法的具體原理了解的不是很透徹,猜想應該是通過虛表來訪問成員函數,而不是直接通過函數符號,所以不會出現鏈接錯誤。
實現如下:
創建一個DLL:
//接口 class ICalculator { public: ICalculator() { m_Number = 1; } virtual ~ICalculator() { } virtual double Add(double a, double b) const = 0; virtual double Sub(double num1,double num2) const = 0; virtual double Mul(double num1, double num2) const = 0; virtual double Div(double num1, double num2) const = 0; double GetNumber() { return 99999; } int m_Number; }; //具體實現類 class Calculator2 : public ICalculator { public: double Add(double a, double b) const; double Sub(double num1, double num2) const; double Mul(double num1, double num2) const; double Div(double num1, double num2) const; }; //全局函數 返回基類指針 extern "C" MYDLL_API ICalculator * CreateCalculator(); extern "C" MYDLL_API void DestroyCalculator(ICalculator * &p);
cpp文件:
#include "calculator2.h" extern "C" MYDLL_API ICalculator * CreateCalculator() { return new Calculator2(); } extern "C" MYDLL_API void DestroyCalculator( ICalculator * &p ) { delete p; p = NULL; } double Calculator2::Add( double a, double b ) const { return (a + b); } double Calculator2::Sub( double num1, double num2 ) const { return (num1 - num2); } double Calculator2::Mul( double num1, double num2 ) const { return (num1 * num2); } double Calculator2::Div( double num1, double num2 ) const { if (num2 != 0) { return (num1 / num2); } return 0; }
顯式調用:
typedef ICalculator * (*CreateFunc)(void); typedef void (*FreeFunc)(ICalculator * &p); void LoadDllTest() { HINSTANCE dllHandle = LoadLibrary(_T("mydll.dll")); if (NULL == dllHandle) { cout << "Load dll failed!" << endl; return; } CreateFunc cfunc = (CreateFunc)GetProcAddress(dllHandle,"CreateCalculator"); FreeFunc freeFunc = (FreeFunc)GetProcAddress(dllHandle,"DestroyCalculator"); if (cfunc == NULL || freeFunc == NULL) { cout << "Unable find dll function CreateCalculator!" << endl; } else { ICalculator * pCalculator = cfunc(); cout << "Add:" << pCalculator->Add(199,1) << endl; cout << "Sub:" << pCalculator->Sub(1,2) << endl; cout << "Not Virtaul Method:" << pCalculator->GetNumber() << endl; cout << "Var:" << pCalculator->m_Number << endl; freeFunc(pCalculator); if (NULL == pCalculator) { cout << "already free" << endl; } } FreeLibrary(dllHandle); }
最后附上Windows定位DLL的搜索順序:
1.包含EXE文件的目錄
2.進程的當前工作目錄
3.Windows系統目錄
4.Windows目錄
5.列在Path環境變量中的一系列目錄