1. 程式人生 > >C++程式設計知識分享:動態庫

C++程式設計知識分享:動態庫


動態庫

我們上一篇文章介紹了靜態庫,我們發現靜態庫,容易使用和理解,也達到了程式碼複用的目的,那為什麼還需要動態庫呢?

為什麼還需要動態庫?

為什麼需要動態庫,其實也是靜態庫的特點導致。

空間浪費是靜態庫的一個問題。


有興趣一起交流c/c++的小夥伴可以加群:941636044

另一個問題是靜態庫對程式的更新、部署和釋出頁會帶來麻煩。如果靜態庫liba.lib更新了,所以使用它的應用程式都需要重新編譯、釋出給使用者(對於玩家來說,可能是一個很小的改動,卻導致整個程式重新下載,全量更新)。

動態庫在程式編譯時並不會被連線到目的碼中,而是在程式執行是才被載入。不同的應用程式如果呼叫相同的庫,那麼在記憶體裡只需要有一份該共享庫的例項,規避了空間浪費問題。動態庫在程式執行是才被載入,也解決了靜態庫對程式的更新、部署和釋出頁會帶來麻煩。使用者只需要更新動態庫即可,增量更新。


 

動態庫特點總結:

 動態庫把對一些庫函式的連結載入推遲到程式執行的時期。

 可以實現程序之間的資源共享。(因此動態庫也稱為共享庫)

 將一些程序升級變得簡單。

 甚至可以真正做到連結載入完全由程式設計師在程式程式碼中控制(顯示呼叫)。

Window與Linux執行檔案格式不同,在建立動態庫的時候有一些差異。

 在Windows系統下的執行檔案格式是PE格式,動態庫需要一個DllMain函式做出初始化的入口,通常在匯出函式的宣告時需要有_declspec(dllexport)關鍵字。

 Linux下gcc編譯的執行檔案預設是ELF格式,不需要初始化入口,亦不需要函式做特別的宣告,編寫比較方便。

與建立靜態庫不同的是,不需要打包工具(ar、lib.exe),直接使用編譯器即可建立動態庫。

Linux下建立與使用動態庫

linux動態庫的命名規則

動態連結庫的名字形式為 libxxx.so,字首是lib,字尾名為“.so”。

 針對於實際庫檔案,每個共享庫都有個特殊的名字“soname”。在程式啟動後,程式通過這個名字來告訴動態載入器該載入哪個共享庫。

 在檔案系統中,soname僅是一個連結到實際動態庫的連結。對於動態庫而言,每個庫實際上都有另一個名字給編譯器來用。它是一個指向實際庫映象檔案的連結檔案(lib+soname+.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

Shell

1g++ -fPIC -c DynamicMath.cpp

-fPIC 建立與地址無關的編譯程式(pic,position independent code),是為了能夠在多個應用程式間共享。

然後,生成動態庫,此時要加連結器選項-shared

Shell

1g++ -shared -o libdynmath.so DynamicMath.o

-shared指定生成動態連結庫。


 

其實上面兩個步驟可以合併為一個命令:

Shell

1g++ -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 目錄找到庫檔案後將其載入記憶體。

如何讓系統能夠找到它:

 如果安裝在/lib或者/usr/lib下,那麼ld預設能夠找到,無需其他操作。

 如果安裝在其他目錄,需要將其新增到/etc/ld.so.cache檔案中,步驟如下:

        1. 編輯/etc/ld.so.conf檔案,加入庫檔案所在目錄的路徑

        2. 執行ldconfig ,該命令會重建/etc/ld.so.cache檔案

我們將建立的動態庫複製到/usr/lib下面,然後執行測試程式。


 

Windows下建立與使用動態庫

建立動態庫(.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;

}

方法一:

工程“屬性面板”→“通用屬性”→“框架和引用”→”新增引用”,將顯示“新增引用”對話方塊。“專案”選項卡列出了當前解決方案中的各個專案以及可以引用的所有庫。 在“專案”選項卡中,選擇 DynamicLibrary。 單擊“確定”。


 

新增DynamicMath.h 標頭檔案目錄,必須修改包含目錄路徑。開啟工程“屬性面板”→”配置屬性”→“C/C++”→” 常規”,在“附加包含目錄”屬性值中,鍵入DynamicMath.h 標頭檔案所在目錄的路徑或瀏覽至該目錄。


 

編譯執行OK。


 

方法二:

 “屬性面板”→”配置屬性”→“連結器”→”常規”,附加依賴庫目錄中輸入,動態庫所在目錄;


 

“屬性面板”→”配置屬性”→“連結器”→”輸入”,附加依賴庫中輸入動態庫編譯出來的DynamicLibrary.lib。


 

這裡可能大家有個疑問,動態庫怎麼還有一個DynamicLibrary.lib檔案?即無論是靜態連結庫還是動態連結庫,最後都有lib檔案,那麼兩者區別是什麼呢?其實,兩個是完全不一樣的東西。


 

StaticLibrary.lib的大小為190KB,DynamicLibrary.lib的大小為3KB,靜態庫對應的lib檔案叫靜態庫,動態庫對應的lib檔案叫【匯入庫】。實際上靜態庫本身就包含了實際執行程式碼、符號表等等,而對於匯入庫而言,其實際的執行程式碼位於動態庫中,匯入庫只包含了地址符號表等,確保程式找到對應函式的一些基本地址資訊。

 

動態庫的顯式呼叫

上面介紹的動態庫使用方法和靜態庫類似屬於隱式呼叫,編譯的時候指定相應的庫和查詢路徑。其實,動態庫還可以顯式呼叫。【在C語言中】,顯示呼叫一個動態庫輕而易舉!

在Linux下顯式呼叫動態庫

#include <dlfcn.h>,提供了下面幾個介面:

void * dlopen( const char * pathname, int mode ):函式以指定模式開啟指定的動態連線庫檔案,並返回一個控制代碼給呼叫程序。

void* dlsym(void* handle,const char* symbol):dlsym根據動態連結庫操作控制代碼(pHandle)與符號(symbol),返回符號對應的地址。使用這個函式不但可以獲取函式地址,也可以獲取變數地址。

int dlclose (void *handle):dlclose用於關閉指定控制代碼的動態連結庫,只有當此動態連結庫的使用計數為0時,才會真正被系統解除安裝。

const char *dlerror(void):當動態連結庫操作函式執行失敗時,dlerror可以返回出錯資訊,返回值為NULL時表示操作函式執行成功。

在Windows下顯式呼叫動態庫

應用程式必須進行函式呼叫以在執行時顯式載入 DLL。為顯式連結到 DLL,應用程式必須:

呼叫 LoadLibrary(或相似的函式)以載入 DLL 和獲取模組控制代碼。

呼叫 GetProcAddress,以獲取指向應用程式要呼叫的每個匯出函式的函式指標。由於應用程式是通過指標呼叫 DLL 的函式,編譯器不生成外部引用,故無需與匯入庫連結。

使用完 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++動態庫中獲取類,附上幾篇相關文章,但我並不建議這麼做:

《LoadLibrary呼叫DLL中的Class》:http://www.cppblog.com/codejie/archive/2009/09/24/97141.html

《C++ dlopen mini HOWTO》:http://blog.csdn.net/denny_233/article/details/7255673

“顯式”使用C++動態庫中的Class是非常繁瑣和危險的事情,因此能用“隱式”就不要用“顯式”,能靜態就不要用動態。

附件:Linux下庫相關命令

g++(gcc)編譯選項

-shared :指定生成動態連結庫。

-static :指定生成靜態連結庫。

-fPIC :表示編譯為位置獨立的程式碼,用於編譯共享庫。目標檔案需要建立成位置無關碼, 念上就是在可執行程式裝載它們的時候,它們可以放在可執行程式的記憶體裡的任何地方。

-L. :表示要連線的庫所在的目錄。

-l:指定連結時需要的動態庫。編譯器查詢動態連線庫時有隱含的命名規則,即在給出的名字前面加上lib,後面加上.a/.so來確定庫的名稱。

-Wall :生成所有警告資訊。

-ggdb :此選項將盡可能的生成gdb 的可以使用的除錯資訊。

-g :編譯器在編譯的時候產生除錯資訊。

-c :只啟用預處理、編譯和彙編,也就是把程式做成目標檔案(.o檔案) 。

-Wl,options :把引數(options)傳遞給連結器ld 。如果options 中間有逗號,就將options分成多個選項,然後傳遞給連結程式。

nm命令

有時候可能需要檢視一個庫中到底有哪些函式,nm命令可以打印出庫中的涉及到的所有符號。庫既可以是靜態的也可以是動態的。nm列出的符號有很多,常見的有三種:

一種是在庫中被呼叫,但並沒有在庫中定義(表明需要其他庫支援),用U表示;

一種是庫中定義的函式,用T表示,這是最常見的;

一種是所謂的弱態”符號,它們雖然在庫中被定義,但是可能被其他庫中的同名符號覆蓋,用W表示。

$nm libhello.h

ldd命令

ldd命令可以檢視一個可執行程式依賴的共享庫,例如我們編寫的四則運算動態庫依賴下面這些庫:


 

總結

大家可以對比上一篇文章的靜態庫。動態庫與靜態庫的不同點在於程式碼被載入的時刻不同。

 靜態庫在程式編譯時會被連線到目的碼中,程式執行時將不再需要該靜態庫,因此體積較大。

 動態庫在程式編譯時並不會被連線到目的碼中,而是在程式執行是才被載入,因此在程式執行時還需要動態庫存在,因此程式碼體積較小。

動態庫的好處是,不同的應用程式如果呼叫相同的庫,那麼在記憶體裡只需要有一份該共享庫的例項。帶來好處的同時,也會有問題!如經典的DLL Hell問題,關於如何規避動態庫管理問題,可以自行查詢相關資料。