庫是寫好的現有的,成熟的,可以復用的代碼。現實中每個程序都要依賴很多基礎的底層庫,不可能每個人的代碼都從零開始,因此庫的存在意義非同尋常。
本質上來說庫是一種可執行代碼的二進制形式,可以被操作系統載入內存執行。庫有兩種:靜態庫(.a、.lib)和動態庫(.so、.dll)。
所謂靜態、動態是指鏈接。回顧一下,將一個程序編譯成可執行程序的步驟:
之所以成為【靜態庫】,是因為在鏈接階段,會將匯編生成的目標文件.o與引用到的庫一起鏈接打包到可執行文件中。因此對應的鏈接方式稱為靜態鏈接。
試想一下,靜態庫與匯編生成的目標文件一起鏈接為可執行文件,那麼靜態庫必定跟.o文件格式相似。其實一個靜態庫可以簡單看成是一組目標文件(.o/.obj文件)的集合,即很多目標文件經過壓縮打包後形成的一個文件。靜態庫特點總結:
下面編寫一些簡單的四則運算C++類,將其編譯成靜態庫給他人用,頭文件如下所示:
#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(); };
inux下使用ar工具、Windows下vs使用lib.exe,將目標文件壓縮到一起,並且對其進行編號和索引,以便於查找和檢索。一般創建靜態庫的步驟如圖所示:
Linux下創建與使用靜態庫
Linux靜態庫命名規則
Linux靜態庫命名規范,必須是”lib[your_library_name].a”:lib為前綴,中間是靜態庫名,擴展名為.a。
創建靜態庫(.a)
通過上面的流程可以知道,Linux創建靜態庫過程如下:
g++ -c StaticMath.cpp
注意帶參數-c,否則直接編譯為可執行文件
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
創建靜態庫(.lib)
如果是使用VS命令行生成靜態庫,也是分兩個步驟來生成程序:
當然,我們一般不這麼用,使用VS工程設置更方便。創建win32控制台程序時,勾選靜態庫類型;打開工程“屬性面板”→”配置屬性”→”常規”,配置類型選擇靜態庫。
Build項目即可生成靜態庫。
使用靜態庫
測試代碼Linux下面的一樣。有3種使用方法:
方法一:
在VS中使用靜態庫方法:
編譯運行OK。
如果引用的靜態庫不是在同一解決方案下的子工程,而是使用第三方提供的靜態庫lib和頭文件,上面的方法設置不了。還有2中方法設置都可行。
方法二:
打開工程“屬性面板”→”配置屬性”→ “鏈接器”→ ”命令行”,輸入靜態庫的完整路徑即可。
方法三:
通過上面的介紹發現靜態庫,容易使用和理解,也達到了代碼復用的目的,那為什麼還需要動態庫呢?
為什麼還需要動態庫?
為什麼需要動態庫,其實也是靜態庫的特點導致。
動態庫在程序編譯時並不會被連接到目標代碼中,而是在程序運行是才被載入。不同的應用程序如果調用相同的庫,那麼在內存裡只需要有一份該共享庫的實例,規避了空間浪費問題。動態庫在程序運行是才被載入,也解決了靜態庫對程序的更新、部署和發布頁會帶來麻煩。用戶只需要更新動態庫即可,增量更新。
動態庫特點總結:
Window與Linux執行文件格式不同,在創建動態庫的時候有一些差異。
與創建靜態庫不同的是,不需要打包工具(ar、lib.exe),直接使用編譯器即可創建動態庫。
Linux下創建與使用動態庫
linux動態庫的命名規則
動態鏈接庫的名字形式為 libxxx.so,前綴是lib,後綴名為“.so”。
創建動態庫(.so)
編寫四則運算動態庫代碼:
#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(); };
首先,生成目標文件,此時要加編譯器選項-fpic
g++ -fPIC -c DynamicMath.cpp
-fPIC 創建與地址無關的編譯程序(pic,position independent code),是為了能夠在多個應用程序間共享。
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 目錄找到庫文件後將其載入內存。
如何讓系統能夠找到它:
我們將創建的動態庫復制到/usr/lib下面,然後運行測試程序。
創建動態庫(.dll)
與Linux相比,在Windows系統下創建動態庫要稍微麻煩一些。首先,需要一個DllMain函數做出初始化的入口(創建win32控制台程序時,勾選DLL類型會自動生成這個文件):
// 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)關鍵字:
#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(); };
生成動態庫需要設置工程屬性,打開工程“屬性面板”→”配置屬性”→”常規”,配置類型選擇動態庫。
Build項目即可生成動態庫。
使用動態庫
創建win32控制台測試程序:
#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; }
方法一:
編譯運行OK。
方法二:
這裡可能大家有個疑問,動態庫怎麼還有一個DynamicLibrary.lib文件?即無論是靜態鏈接庫還是動態鏈接庫,最後都有lib文件,那麼兩者區別是什麼呢?其實,兩個是完全不一樣的東西。
StaticLibrary.lib的大小為190KB,DynamicLibrary.lib的大小為3KB,靜態庫對應的lib文件叫靜態庫,動態庫對應的lib文件叫【導入庫】。實際上靜態庫本身就包含了實際執行代碼、符號表等等,而對於導入庫而言,其實際的執行代碼位於動態庫中,導入庫只包含了地址符號表等,確保程序找到對應函數的一些基本地址信息。
上面介紹的動態庫使用方法和靜態庫類似屬於隱式調用,編譯的時候指定相應的庫和查找路徑。其實,動態庫還可以顯式調用。【在C語言中】,顯示調用一個動態庫輕而易舉!
在Linux下顯式調用動態庫
#include <dlfcn.h>,提供了下面幾個接口:
在Windows下顯式調用動態庫
應用程序必須進行函數調用以在運行時顯式加載 DLL。為顯式鏈接到 DLL,應用程序必須:
顯式調用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++動態庫中獲取類,附上幾篇相關文章,但我並不建議這麼做:
“顯式”使用C++動態庫中的Class是非常繁瑣和危險的事情,因此能用“隱式”就不要用“顯式”,能靜態就不要用動態。
g++(gcc)編譯選項
nm命令
有時候可能需要查看一個庫中到底有哪些函數,nm命令可以打印出庫中的涉及到的所有符號。庫既可以是靜態的也可以是動態的。nm列出的符號有很多,常見的有三種:
$nm libhello.h
ldd命令
ldd命令可以查看一個可執行程序依賴的共享庫,例如我們編寫的四則運算動態庫依賴下面這些庫:
二者的不同點在於代碼被載入的時刻不同。
動態庫的好處是,不同的應用程序如果調用相同的庫,那麼在內存裡只需要有一份該共享庫的實例。帶來好處的同時,也會有問題!如經典的DLL Hell問題,關於如何規避動態庫管理問題,可以自行查找相關資料。