C++靜態庫與動態庫html
此次分享的宗旨是——讓你們學會建立與使用靜態庫、動態庫,知道靜態庫與動態庫的區別,知道使用的時候如何選擇。這裏不深刻介紹靜態庫、動態庫的底層格式,內存佈局等,有興趣的同窗,推薦一本書《程序員的自我修養——連接、裝載與庫》。linux
庫是寫好的現有的,成熟的,能夠複用的代碼。現實中每一個程序都要依賴不少基礎的底層庫,不可能每一個人的代碼都從零開始,所以庫的存在乎義非同尋常。ios
本質上來講庫是一種可執行代碼的二進制形式,能夠被操做系統載入內存執行。庫有兩種:靜態庫(.a、.lib)和動態庫(.so、.dll)。程序員
所謂靜態、動態是指連接。回顧一下,將一個程序編譯成可執行程序的步驟:app
圖:編譯過程框架
之因此成爲【靜態庫】,是由於在連接階段,會將彙編生成的目標文件.o與引用到的庫一塊兒連接打包到可執行文件中。所以對應的連接方式稱爲靜態連接。函數
試想一下,靜態庫與彙編生成的目標文件一塊兒連接爲可執行文件,那麼靜態庫一定跟.o文件格式類似。其實一個靜態庫能夠簡單當作是一組目標文件(.o/.obj文件)的集合,即不少目標文件通過壓縮打包後造成的一個文件。靜態庫特色總結:工具
l 靜態庫對函數庫的連接是放在編譯時期完成的。佈局
l 程序在運行時與函數庫再無瓜葛,移植方便。測試
l 浪費空間和資源,由於全部相關的目標文件與牽涉到的函數庫被連接合成一個可執行文件。
下面編寫一些簡單的四則運算C++類,將其編譯成靜態庫給他人用,頭文件以下所示:
StaticMath.h頭文件 |
#pragma once class StaticMath { public: StaticMath(void); ~StaticMath(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(); }; |
Linux下使用ar工具、Windows下vs使用lib.exe,將目標文件壓縮到一塊兒,而且對其進行編號和索引,以便於查找和檢索。通常建立靜態庫的步驟如圖所示:
圖:建立靜態庫過程
Linux靜態庫命名規範,必須是"lib[your_library_name].a":lib爲前綴,中間是靜態庫名,擴展名爲.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> using namespace std;
int main(int argc, 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來肯定庫的名稱。
若是是使用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),直接使用編譯器便可建立動態庫。
動態連接庫的名字形式爲 libxxx.so,前綴是lib,後綴名爲「.so」。
l 針對於實際庫文件,每一個共享庫都有個特殊的名字「soname」。在程序啓動後,程序經過這個名字來告訴動態加載器該載入哪一個共享庫。
l 在文件系統中,soname僅是一個連接到實際動態庫的連接。對於動態庫而言,每一個庫實際上都有另外一個名字給編譯器來用。它是一個指向實際庫鏡像文件的連接文件(lib+soname+.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下面,而後運行測試程序。
與Linux相比,在Windows系統下建立動態庫要稍微麻煩一些。首先,須要一個DllMain函數作出初始化的入口(建立win32控制檯程序時,勾選DLL類型會自動生成這個文件):
dllmain.cpp入口文件 |
// dllmain.cpp : Defines the entry point for the DLL application. #include "stdafx.h"
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; } |
一般在導出函數的聲明時須要有_declspec(dllexport)關鍵字:
DynamicMath.h頭文件 |
#pragma once class DynamicMath { 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> using namespace std;
int _tmain(int argc, _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語言中】,顯示調用一個動態庫垂手可得!
#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時表示操做函數執行成功。
應用程序必須進行函數調用以在運行時顯式加載 DLL。爲顯式連接到 DLL,應用程序必須:
l 調用 LoadLibrary(或類似的函數)以加載 DLL 和獲取模塊句柄。
l 調用 GetProcAddress,以獲取指向應用程序要調用的每一個導出函數的函數指針。因爲應用程序是經過指針調用 DLL 的函數,編譯器不生成外部引用,故無需與導入庫連接。
l 使用完 DLL 後調用 FreeLibrary。
對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是很是繁瑣和危險的事情,所以能用「隱式」就不要用「顯式」,能靜態就不要用動態。
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列出的符號有不少,常見的有三種:
l 一種是在庫中被調用,但並無在庫中定義(代表須要其餘庫支持),用U表示;
l 一種是庫中定義的函數,用T表示,這是最多見的;
l 一種是所謂的弱態」符號,它們雖然在庫中被定義,可是可能被其餘庫中的同名符號覆蓋,用W表示。
$nm libhello.h
ldd命令能夠查看一個可執行程序依賴的共享庫,例如咱們編寫的四則運算動態庫依賴下面這些庫:
兩者的不一樣點在於代碼被載入的時刻不一樣。
l 靜態庫在程序編譯時會被鏈接到目標代碼中,程序運行時將再也不須要該靜態庫,所以體積較大。
l 動態庫在程序編譯時並不會被鏈接到目標代碼中,而是在程序運行是才被載入,所以在程序運行時還須要動態庫存在,所以代碼體積較小。
動態庫的好處是,不一樣的應用程序若是調用相同的庫,那麼在內存裏只須要有一份該共享庫的實例。帶來好處的同時,也會有問題!如經典的DLL Hell問題,關於如何規避動態庫管理問題,能夠自行查找相關資料。