本文屬轉載,但是寫的非常好。
原文位址:http://www.cnblogs.com/skynet/p/3372855.html
C++靜态庫與動态庫
這次分享的宗旨是——讓大家學會建立與使用靜态庫、動态庫,知道靜态庫與動态庫的差別,知道使用的時候如何選擇。這裡不深入介紹靜态庫、動态庫的底層格式,記憶體布局等,有興趣的同學,推薦一本書《程式員的自我修養——連結、裝載與庫》。
什麼是庫
庫是寫好的現有的,成熟的,可以複用的代碼。現實中每個程式都要依賴很多基礎的底層庫,不可能每個人的代碼都從零開始,是以庫的存在意義非同尋常。
本質上來說庫是一種可執行代碼的二進制形式,可以被作業系統載入記憶體執行。庫有兩種:靜态庫(.a、.lib)和動态庫(.so、.dll)。
所謂靜态、動态是指連結。回顧一下,将一個程式編譯成可執行程式的步驟:

圖:編譯過程
靜态庫
之是以成為【靜态庫】,是因為在連結階段,會将彙編生成的目标檔案.o與引用到的庫一起連結打包到可執行檔案中。是以對應的連結方式稱為靜态連結。
試想一下,靜态庫與彙編生成的目标檔案一起連結為可執行檔案,那麼靜态庫必定跟.o檔案格式相似。其實一個靜态庫可以簡單看成是一組目标檔案(.o/.obj檔案)的集合,即很多目标檔案經過壓縮打包後形成的一個檔案。靜态庫特點總結:
l 靜态庫對函數庫的連結是放在編譯時期完成的。
l 程式在運作時與函數庫再無瓜葛,移植友善。
l 浪費空間和資源,因為所有相關的目标檔案與牽涉到的函數庫被連結合成一個可執行檔案。
下面編寫一些簡單的四則運算C++類,将其編譯成靜态庫給他人用,頭檔案如下所示:
StaticMath.h頭檔案 |
#pragmaonce classStaticMath { public: StaticMath(void); ~StaticMath(void); staticdouble add(double a, double b);//加法 staticdouble sub(double a, double b);//減法 staticdouble mul(double a, double b);//乘法 staticdouble div(double a, double b);//除法 void print(); }; |
Linux下使用ar工具、Windows下vs使用lib.exe,将目标檔案壓縮到一起,并且對其進行編号和索引,以便于查找和檢索。一般建立靜态庫的步驟如圖所示:
圖:建立靜态庫過程
Linux下建立與使用靜态庫
Linux靜态庫命名規則
Linux靜态庫命名規範,必須是"lib[your_library_name].a":lib為字首,中間是靜态庫名,擴充名為.a。
建立靜态庫(.a)
通過上面的流程可以知道,Linux建立靜态庫過程如下:
l 首先,将代碼檔案編譯成目标檔案.o(StaticMath.o)
g++ -c StaticMath.cpp |
注意帶參數-c,否則直接編譯為可執行檔案
l 然後,通過ar工具将目标檔案打包成.a靜态庫檔案
ar -crv libstaticmath.a StaticMath.o |
生成靜态庫libstaticmath.a。
大一點的項目會編寫makefile檔案(CMake等等工程管理工具)來生成靜态庫,輸入多個指令太麻煩了。
使用靜态庫
編寫使用上面建立的靜态庫的測試代碼:
測試代碼: |
#include"StaticMath.h" #include<iostream> usingnamespace std; int main(intargc, char* argv[]) { double a = 10; double b = 2; cout << "a + b = " << StaticMath::add(a, b) << endl; cout << "a - b = " << StaticMath::sub(a, b) << endl; cout << "a * b = " << StaticMath::mul(a, b) << endl; cout << "a / b = " << StaticMath::div(a, b) << endl; StaticMath sm; sm.print(); system("pause"); return 0; } |
Linux下使用靜态庫,隻需要在編譯的時候,指定靜态庫的搜尋路徑(-L選項)、指定靜态庫名(不需要lib字首和.a字尾,-l選項)。
# g++ TestStaticLibrary.cpp -L../StaticLibrary -lstaticmath
l -L:表示要連接配接的庫所在目錄
l -l:指定連結時需要的動态庫,編譯器查找動态連接配接庫時有隐含的命名規則,即在給出的名字前面加上lib,後面加上.a或.so來确定庫的名稱。
Windows下建立與使用靜态庫
建立靜态庫(.lib)
如果是使用VS指令行生成靜态庫,也是分兩個步驟來生成程式:
l 首先,通過使用帶編譯器選項 /c 的 Cl.exe 編譯代碼 (cl /c StaticMath.cpp),建立名為“StaticMath.obj”的目标檔案。
l 然後,使用庫管理器 Lib.exe 連結代碼 (lib StaticMath.obj),建立靜态庫StaticMath.lib。
當然,我們一般不這麼用,使用VS工程設定更友善。建立win32控制台程式時,勾選靜态庫類型;打開工程“屬性面闆”è”配置屬性”è”正常”,配置類型選擇靜态庫。
圖:vs靜态庫項目屬性設定
Build項目即可生成靜态庫。
使用靜态庫
測試代碼Linux下面的一樣。有3種使用方法:
方法一:
在VS中使用靜态庫方法:
l 工程“屬性面闆”è“通用屬性”è “架構和引用”è”添加引用”,将顯示“添加引用”對話框。 “項目”頁籤列出了目前解決方案中的各個項目以及可以引用的所有庫。在“項目”頁籤中,選擇 StaticLibrary。單擊“确定”。
l 添加StaticMath.h 頭檔案目錄,必須修改包含目錄路徑。打開工程“屬性面闆”è”配置屬性”è “C/C++”è” 正常”,在“附加包含目錄”屬性值中,鍵入StaticMath.h 頭檔案所在目錄的路徑或浏覽至該目錄。
編譯運作OK。
圖:靜态庫測試結果(vs)
如果引用的靜态庫不是在同一解決方案下的子工程,而是使用第三方提供的靜态庫lib和頭檔案,上面的方法設定不了。還有2中方法設定都可行。
方法二:
打開工程“屬性面闆”è”配置屬性”è “連結器”è”指令行”,輸入靜态庫的完整路徑即可。
方法三:
l “屬性面闆”è”配置屬性”è “連結器”è”正常”,附加依賴庫目錄中輸入,靜态庫所在目錄;
l “屬性面闆”è”配置屬性”è “連結器”è”輸入”,附加依賴庫中輸入靜态庫名StaticLibrary.lib。
動态庫
通過上面的介紹發現靜态庫,容易使用和了解,也達到了代碼複用的目的,那為什麼還需要動态庫呢?
為什麼還需要動态庫?
為什麼需要動态庫,其實也是靜态庫的特點導緻。
l 空間浪費是靜态庫的一個問題。
l 另一個問題是靜态庫對程式的更新、部署和釋出頁會帶來麻煩。如果靜态庫liba.lib更新了,是以使用它的應用程式都需要重新編譯、釋出給使用者(對于玩家來說,可能是一個很小的改動,卻導緻整個程式重新下載下傳,全量更新)。
動态庫在程式編譯時并不會被連接配接到目标代碼中,而是在程式運作是才被載入。不同的應用程式如果調用相同的庫,那麼在記憶體裡隻需要有一份該共享庫的執行個體,規避了空間浪費問題。動态庫在程式運作是才被載入,也解決了靜态庫對程式的更新、部署和釋出頁會帶來麻煩。使用者隻需要更新動态庫即可,增量更新。
動态庫特點總結:
l 動态庫把對一些庫函數的連結載入推遲到程式運作的時期。
l 可以實作程序之間的資源共享。(是以動态庫也稱為共享庫)
l 将一些程式更新變得簡單。
l 甚至可以真正做到連結載入完全由程式員在程式代碼中控制(顯示調用)。
Window與Linux執行檔案格式不同,在建立動态庫的時候有一些差異。
l 在Windows系統下的執行檔案格式是PE格式,動态庫需要一個DllMain函數做出初始化的入口,通常在導出函數的聲明時需要有_declspec(dllexport)關鍵字。
l Linux下gcc編譯的執行檔案預設是ELF格式,不需要初始化入口,亦不需要函數做特别的聲明,編寫比較友善。
與建立靜态庫不同的是,不需要打包工具(ar、lib.exe),直接使用編譯器即可建立動态庫。
Linux下建立與使用動态庫
linux動态庫的命名規則
動态連結庫的名字形式為 libxxx.so,字首是lib,字尾名為“.so”。
l 針對于實際庫檔案,每個共享庫都有個特殊的名字“soname”。在程式啟動後,程式通過這個名字來告訴動态加載器該載入哪個共享庫。
l 在檔案系統中,soname僅是一個連結到實際動态庫的連結。對于動态庫而言,每個庫實際上都有另一個名字給編譯器來用。它是一個指向實際庫鏡像檔案的連結檔案(lib+soname+.so)。
建立動态庫(.so)
編寫四則運算動态庫代碼:
DynamicMath.h頭檔案 |
#pragma once class DynamicMath { public: DynamicMath(void); ~DynamicMath(void); static double add(double a, double b);//¼Ó·¨ static double sub(double a, double b);//¼õ·¨ static double mul(double a, double b);//³Ë·¨ static double div(double a, double b);//³ý·¨ void print(); }; |
l 首先,生成目标檔案,此時要加編譯器選項-fpic
g++ -fPIC -c DynamicMath.cpp |
-fPIC 建立與位址無關的編譯程式(pic,position independent code),是為了能夠在多個應用程式間共享。
l 然後,生成動态庫,此時要加連結器選項-shared
g++ -shared -o libdynmath.so DynamicMath.o |
-shared指定生成動态連結庫。
其實上面兩個步驟可以合并為一個指令:
g++ -fPIC -shared -o libdynmath.so DynamicMath.cpp |
使用動态庫
編寫使用動态庫的測試代碼:
測試代碼: |
#include "../DynamicLibrary/DynamicMath.h" #include <iostream> using namespace std; int main(int argc, char* argv[]) { double a = 10; double b = 2; cout << "a + b = " << DynamicMath::add(a, b) << endl; cout << "a - b = " << DynamicMath::sub(a, b) << endl; cout << "a * b = " << DynamicMath::mul(a, b) << endl; cout << "a / b = " << DynamicMath::div(a, b) << endl; DynamicMath dyn; dyn.print(); return 0; } |
引用動态庫編譯成可執行檔案(跟靜态庫方式一樣):
g++ TestDynamicLibrary.cpp -L../DynamicLibrary -ldynmath |
然後運作:./a.out,發現竟然報錯了!!!
可能大家會猜測,是因為動态庫跟測試程式不是一個目錄,那我們驗證下是否如此:
發現還是報錯!!!那麼,在執行的時候是如何定位共享庫檔案的呢?
1) 當系統加載可執行代碼時候,能夠知道其所依賴的庫的名字,但是還需要知道絕對路徑。此時就需要系統動态載入器(dynamic linker/loader)。
2) 對于elf格式的可執行程式,是由ld-linux.so*來完成的,它先後搜尋elf檔案的 DT_RPATH段—環境變量LD_LIBRARY_PATH—/etc/ld.so.cache檔案清單—/lib/,/usr/lib 目錄找到庫檔案後将其載入記憶體。
如何讓系統能夠找到它:
l 如果安裝在/lib或者/usr/lib下,那麼ld預設能夠找到,無需其他操作。
l 如果安裝在其他目錄,需要将其添加到/etc/ld.so.cache檔案中,步驟如下:
n 編輯/etc/ld.so.conf檔案,加入庫檔案所在目錄的路徑
n 運作ldconfig ,該指令會重建/etc/ld.so.cache檔案
我們将建立的動态庫複制到/usr/lib下面,然後運作測試程式。
Windows下建立與使用動态庫
建立動态庫(.dll)
與Linux相比,在Windows系統下建立動态庫要稍微麻煩一些。首先,需要一個DllMain函數做出初始化的入口(建立win32控制台程式時,勾選DLL類型會自動生成這個檔案):
dllmain.cpp入口檔案 |
// dllmain.cpp : Defines the entry point for the DLL application. #include"stdafx.h" BOOLAPIENTRY DllMain( HMODULEhModule, DWORD ul_reason_for_call, LPVOIDlpReserved ) { switch (ul_reason_for_call) { caseDLL_PROCESS_ATTACH: caseDLL_THREAD_ATTACH: caseDLL_THREAD_DETACH: caseDLL_PROCESS_DETACH: break; } returnTRUE; } |
通常在導出函數的聲明時需要有_declspec(dllexport)關鍵字:
DynamicMath.h頭檔案 |
#pragmaonce classDynamicMath { public: __declspec(dllexport) DynamicMath(void); __declspec(dllexport) ~DynamicMath(void); static__declspec(dllexport) double add(double a, double b);//加法 static__declspec(dllexport) double sub(double a, double b);//減法 static__declspec(dllexport) double mul(double a, double b);//乘法 static__declspec(dllexport) double div(double a, double b);//除法 __declspec(dllexport) void print(); }; |
生成動态庫需要設定工程屬性,打開工程“屬性面闆”è”配置屬性”è”正常”,配置類型選擇動态庫。
圖:v動态庫項目屬性設定
Build項目即可生成動态庫。
使用動态庫
建立win32控制台測試程式:
TestDynamicLibrary.cpp測試程式 |
#include"stdafx.h" #include"DynamicMath.h" #include<iostream> usingnamespace std; int_tmain(intargc, _TCHAR* argv[]) { double a = 10; double b = 2; cout << "a + b = " << DynamicMath::add(a, b) << endl; cout << "a - b = " << DynamicMath::sub(a, b) << endl; cout << "a * b = " << DynamicMath::mul(a, b) << endl; cout << "a / b = " << DynamicMath::div(a, b) << endl; DynamicMath dyn; dyn.print(); system("pause"); return 0; } |
方法一:
l 工程“屬性面闆”è“通用屬性”è “架構和引用”è”添加引用”,将顯示“添加引用”對話框。“項目”頁籤列出了目前解決方案中的各個項目以及可以引用的所有庫。在“項目”頁籤中,選擇 DynamicLibrary。單擊“确定”。
l 添加DynamicMath.h 頭檔案目錄,必須修改包含目錄路徑。打開工程“屬性面闆”è”配置屬性”è “C/C++”è” 正常”,在“附加包含目錄”屬性值中,鍵入DynamicMath.h 頭檔案所在目錄的路徑或浏覽至該目錄。
編譯運作OK。
圖:動态庫測試結果(vs)
方法二:
l “屬性面闆”è”配置屬性”è “連結器”è”正常”,附加依賴庫目錄中輸入,動态庫所在目錄;
l “屬性面闆”è”配置屬性”è “連結器”è”輸入”,附加依賴庫中輸入動态庫編譯出來的DynamicLibrary.lib。
這裡可能大家有個疑問,動态庫怎麼還有一個DynamicLibrary.lib檔案?即無論是靜态連結庫還是動态連結庫,最後都有lib檔案,那麼兩者差別是什麼呢?其實,兩個是完全不一樣的東西。
StaticLibrary.lib的大小為190KB,DynamicLibrary.lib的大小為3KB,靜态庫對應的lib檔案叫靜态庫,動态庫對應的lib檔案叫【導入庫】。實際上靜态庫本身就包含了實際執行代碼、符号表等等,而對于導入庫而言,其實際的執行代碼位于動态庫中,導入庫隻包含了位址符号表等,確定程式找到對應函數的一些基本位址資訊。
動态庫的顯式調用
上面介紹的動态庫使用方法和靜态庫類似屬于隐式調用,編譯的時候指定相應的庫和查找路徑。其實,動态庫還可以顯式調用。【在C語言中】,顯示調用一個動态庫輕而易舉!
在Linux下顯式調用動态庫
#include <dlfcn.h>,提供了下面幾個接口:
l void * dlopen( const char * pathname, int mode ):函數以指定模式打開指定的動态連接配接庫檔案,并傳回一個句柄給調用程序。
l void* dlsym(void* handle,const char* symbol):dlsym根據動态連結庫操作句柄(pHandle)與符号(symbol),傳回符号對應的位址。使用這個函數不但可以擷取函數位址,也可以擷取變量位址。
l int dlclose (void *handle):dlclose用于關閉指定句柄的動态連結庫,隻有當此動态連結庫的使用計數為0時,才會真正被系統解除安裝。
l const char *dlerror(void):當動态連結庫操作函數執行失敗時,dlerror可以傳回出錯資訊,傳回值為NULL時表示操作函數執行成功。
在Windows下顯式調用動态庫
應用程式必須進行函數調用以在運作時顯式加載 DLL。為顯式連結到 DLL,應用程式必須:
l 調用 LoadLibrary(或相似的函數)以加載 DLL 和擷取子產品句柄。
l 調用 GetProcAddress,以擷取指向應用程式要調用的每個導出函數的函數指針。由于應用程式是通過指針調用 DLL 的函數,編譯器不生成外部引用,故無需與導入庫連結。
l 使用完 DLL 後調用 FreeLibrary。
顯式調用C++動态庫注意點
對C++來說,情況稍微複雜。顯式加載一個C++動态庫的困難一部分是因為C++的name mangling;另一部分是因為沒有提供一個合适的API來裝載類,在C++中,您可能要用到庫中的一個類,而這需要建立該類的一個執行個體,這不容易做到。
name mangling可以通過extern "C"解決。C++有個特定的關鍵字用來聲明采用C binding的函數:extern "C" 。用 extern "C"聲明的函數将使用函數名作符号名,就像C函數一樣。是以,隻有非成員函數才能被聲明為extern "C",并且不能被重載。盡管限制多多,extern "C"函數還是非常有用,因為它們可以象C函數一樣被dlopen動态加載。冠以extern "C"限定符後,并不意味着函數中無法使用C++代碼了,相反,它仍然是一個完全的C++函數,可以使用任何C++特性和各種類型的參數。
另外如何從C++動态庫中擷取類,附上幾篇相關文章,但我并不建議這麼做:
l 《LoadLibrary調用DLL中的Class》:http://www.cppblog.com/codejie/archive/2009/09/24/97141.html
l 《C++ dlopen mini HOWTO》:http://blog.csdn.net/denny_233/article/details/7255673
“顯式”使用C++動态庫中的Class是非常繁瑣和危險的事情,是以能用“隐式”就不要用“顯式”,能靜态就不要用動态。
附件:Linux下庫相關指令
g++(gcc)編譯選項
l -shared :指定生成動态連結庫。
l -static :指定生成靜态連結庫。
l -fPIC :表示編譯為位置獨立的代碼,用于編譯共享庫。目标檔案需要建立成位置無關碼,念上就是在可執行程式裝載它們的時候,它們可以放在可執行程式的記憶體裡的任何地方。
l -L. :表示要連接配接的庫所在的目錄。
l -l:指定連結時需要的動态庫。編譯器查找動态連接配接庫時有隐含的命名規則,即在給出的名字前面加上lib,後面加上.a/.so來确定庫的名稱。
l -Wall :生成所有警告資訊。
l -ggdb :此選項将盡可能的生成gdb 的可以使用的調試資訊。
l -g :編譯器在編譯的時候産生調試資訊。
l -c :隻激活預處理、編譯和彙編,也就是把程式做成目标檔案(.o檔案) 。
l -Wl,options :把參數(options)傳遞給連結器ld 。如果options 中間有逗号,就将options分成多個選項,然後傳遞給連結程式。
nm指令
有時候可能需要檢視一個庫中到底有哪些函數,nm指令可以列印出庫中的涉及到的所有符号。庫既可以是靜态的也可以是動态的。nm列出的符号有很多,常見的有三種:
l 一種是在庫中被調用,但并沒有在庫中定義(表明需要其他庫支援),用U表示;
l 一種是庫中定義的函數,用T表示,這是最常見的;
l 一種是所謂的弱态”符号,它們雖然在庫中被定義,但是可能被其他庫中的同名符号覆寫,用W表示。
$nm libhello.h
ldd指令
ldd指令可以檢視一個可執行程式依賴的共享庫,例如我們編寫的四則運算動态庫依賴下面這些庫:
總結
二者的不同點在于代碼被載入的時刻不同。
l 靜态庫在程式編譯時會被連接配接到目标代碼中,程式運作時将不再需要該靜态庫,是以體積較大。
l 動态庫在程式編譯時并不會被連接配接到目标代碼中,而是在程式運作是才被載入,是以在程式運作時還需要動态庫存在,是以代碼體積較小。
動态庫的好處是,不同的應用程式如果調用相同的庫,那麼在記憶體裡隻需要有一份該共享庫的執行個體。帶來好處的同時,也會有問題!如經典的DLL Hell問題,關于如何規避動态庫管理問題,可以自行查找相關資料。