C++ 靜態庫與動態庫以及在 Windows上 的創建、使用


一、什么是庫

庫是寫好的現有的,成熟的,可以復用的代碼。現實中每個程序都要依賴很多基礎的底層庫,不可能每個人的代碼都從零開始,因此庫的存在意義非同尋常。

本質上來說庫是一種可執行代碼的二進制形式,可以被操作系統載入內存執行。庫有兩種:靜態庫(.a 或 .lib)和動態庫(.so 或 .dll)。

所謂靜態、動態是指鏈接。回顧一下,將一個程序編譯成可執行程序的步驟:

img


二、什么是靜態庫

之所以成為【靜態庫】,是因為在鏈接階段,會將匯編生成的目標文件 .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();
};

包含pragma once語句的文件只會被編譯一次,和頭文件中用#ifndef... #define... #endif的效果類似。


三、Windows 下創建與使用靜態庫

3.1 創建靜態庫

如果是使用 VS 命令行生成靜態庫,也是分兩個步驟來生成程序:

  • 首先,通過使用帶編譯器選項 /c 的 Cl.exe 編譯代碼 (cl /c StaticMath.cpp),創建名為 “StaticMath.obj” 的目標文件。
  • 然后,使用庫管理器 Lib.exe 鏈接代碼 (lib StaticMath.obj),創建靜態庫 StaticMath.lib。

當然,我們一般不這么用,使用 VS 工程設置更方便。創建 Win32 控制台程序時,勾選靜態庫類型;打開工程 “屬性面板” → ”配置屬性” → ”常規”,配置類型選擇靜態庫。

img


編譯項目即可生成靜態庫。


3.2 使用靜態庫

創建 Win32 控制台測試程序:

#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;
}

有 3 種使用方法:

方法一:

這也是最常用的方法:

  • “屬性面板” → ”配置屬性” → “鏈接器” → ”常規”,附加依賴庫目錄中輸入,靜態庫所在目錄;
  • “屬性面板” → ”配置屬性” → “鏈接器” → ”輸入”,附加依賴庫中輸入靜態庫名 StaticLibrary.lib。

img


編譯運行 OK。

img


方法二:

打開工程 “屬性面板” → ”配置屬性” → “鏈接器” → ”命令行”,輸入靜態庫的完整路徑即可。

img


方法三:

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

img


添加 StaticMath.h 頭文件目錄,必須修改包含目錄路徑。打開工程 “屬性面板” → ”配置屬性” → “C/C++” → ” 常規”,在 “附加包含目錄” 屬性值中,鍵入 StaticMath.h 頭文件所在目錄的路徑或瀏覽至該目錄。

img


四、什么是動態庫

通過上面的介紹發現靜態庫,容易使用和理解,也達到了代碼復用的目的,那為什么還需要動態庫呢?

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

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

    img

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

動態庫在程序編譯時並不會被連接到目標代碼中,而是在程序運行是才被載入。不同的應用程序如果調用相同的庫,那么在內存里只需要有一份該共享庫的實例,規避了空間浪費問題。動態庫在程序運行是才被載入,也解決了靜態庫對程序的更新、部署和發布頁會帶來麻煩。用戶只需要更新動態庫即可,增量更新。

img


動態庫特點總結:

  • 動態庫把對一些庫函數的鏈接載入推遲到程序運行的時期。
  • 可以實現進程之間的資源共享。(因此動態庫也稱為共享庫)
  • 將一些程序升級變得簡單。
  • 甚至可以真正做到鏈接載入完全由程序員在程序代碼中控制(顯示調用)。

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

  • 在 Windows 系統下的執行文件格式是 PE 格式,動態庫需要一個 DllMain 函數做出初始化的入口,通常在導出函數的聲明時需要有 _declspec(dllexport)關 鍵字。
  • Linux 下 gcc 編譯的執行文件默認是 ELF 格式,不需要初始化入口,亦不需要函數做特別的聲明,編寫比較方便。

五、Windows 下創建與使用動態庫

5.1 創建動態庫

與 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();
};

生成動態庫需要設置工程屬性,打開工程 “屬性面板” → ”配置屬性” → ”常規”,配置類型選擇動態庫。

img

編譯項目即可生成動態庫。


5.2 使用動態庫

創建 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;
}

有 2 種使用方法:

方法一:

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

img


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

img


編譯運行 OK。

img


這里可能大家有個疑問,動態庫怎么還有一個 DynamicLibrary.lib 文件?即無論是靜態鏈接庫還是動態鏈接庫,最后都有 lib 文件,那么兩者區別是什么呢?其實,兩個是完全不一樣的東西。

img

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


方法二:

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

    img


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

    img


六、在 Windows 下顯式調用動態庫

應用程序必須進行函數調用以在運行時顯式加載 DLL。為顯式鏈接到 DLL,應用程序必須:

  • 調用 LoadLibrary(或相似的函數)以加載 DLL 和獲取模塊句柄。
  • 調用 GetProcAddress,以獲取指向應用程序要調用的每個導出函數的函數指針。由於應用程序是通過指針調用 DLL 的函數,編譯器不生成外部引用,故無需與導入庫鏈接。
  • 使用完 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++ 動態庫中獲取類,附上幾篇相關文章,但我並不建議這么做:

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


七、總結

二者的不同點在於代碼被載入的時刻不同。

  • 靜態庫在程序編譯時會被連接到目標代碼中,程序運行時將不再需要該靜態庫,因此體積較大。
  • 動態庫在程序編譯時並不會被連接到目標代碼中,而是在程序運行是才被載入,因此在程序運行時還需要動態庫存在,因此代碼體積較小。

靜態編譯:編譯器在編譯可執行文件的時候,將可執行文件需要調用的對應靜態庫(.a 或 .lib)中的部分提取出來,鏈接到可執行文件中去,使可執行文件在運行的時候不依賴於動態鏈接庫。

動態編譯:可執行文件需要附帶一個的動態鏈接庫(.so 或 .dll),在執行時,需要調用其對應動態鏈接庫中的命令。

  • 優點:一方面是縮小了執行文件本身的體積,另一方面是加快了編譯速度,節省了系統資源。
  • 缺點:一是哪怕是很簡單的程序,只用到了鏈接庫中的一兩條命令,也需要附帶一個相對龐大的鏈接庫;二是如果其他計算機上沒有安裝對應的運行庫,則用動態編譯的可執行文件就不能運行。

這里暫時只介紹 Windows 下庫的創建和使用,Linux 暫不涉及,以后有空再總結。


參考:

【C/C++開發】C++靜態庫與動態庫以及在Linux和Windows上的創建使用



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM