C++基礎知識之動態庫靜態庫
一、 靜態庫與動態庫
庫(library),一般是一種可執行的二進制格式,被操作系統載入內存執行。
我們通常把一些公用函數制作成函數庫,供其它程序使用。
函數庫分為靜態庫和動態庫
靜態庫和動態庫區別:
靜態庫在程序編譯時會被連接到目標代碼中,程序運行時將不再需要該靜態庫。因此,使用了靜態庫的可執行程序存儲在磁盤上的空間就比較大。
動態庫在程序編譯時並不會被連接到目標代碼中,而是在程序運行是才被載入,因此在程序運行時還需要動態庫存在。
靜態庫和動態庫命名規範:
linux:
靜態庫文件名的命名規範是以lib為前綴,緊接著跟靜態庫名,擴展名為.a。
動態庫文件名的命名規範是以以lib為前綴
window:
靜態庫是.lib文件(但和dll文件的.lib文件是不同的,下面會有闡述)。
動態庫是dll文件(Dynamic Linked Library)。
在Windows操作系統中,Visual Studio使用lib.exe作為庫的管理工具,負責創建靜態庫和動態庫。
二、在Windows下創建和使用靜態庫
創建靜態庫有3個方法:
創建靜態庫方法一
1. 通過使用帶編譯器選項 /c 的 Cl.exe 編譯代碼 (cl/c StaticMath.cpp),創建名為“StaticMath.obj”的目標文件。
2. 然後,使用庫管理器 Lib.exe 鏈接代碼 (lib StaticMath.obj),創建靜態庫StaticMath.lib。
創建靜態庫方法二
創建Win32控制臺程序時,勾選靜態庫類型;
創建靜態庫方法三
工程的“Properties” -> "Configuration Properties” -> ”General”,將ConfigurationType選為Static Library(.lib)。
使用靜態庫有2個方法:
使用靜態庫方法一
在 Project’s Properties -> Linker-> Command Line -> Additional Options 添加靜態庫的完整路徑。
使用靜態庫方法二
在 Project‘s Properties -> Linker -> General-> Additional Library Directories,添加靜態庫所在目錄;
在 Project’s Properties -> Linker -> Input ->Additional Dependencies,添加靜態庫的文件名。
註意,靜態庫的文件名叫xxx.lib,和動態庫的導入庫文件的文件後綴名相同。二者內容當然是完全不同的:靜態庫文件當然包含了執行代碼和符號表,而動態庫的導入庫文件則只包含了地址符號表。
三、動態庫的調用方式
動態庫的調用方式有2種:隱式調用 和 顯式調用。
1. 隱式調用
和調用靜態庫方法二類似,除了需要頭文件之外,還要:
在 Project’s Properties -> Linker-> General -> Additional Library Directories,添加動態庫的導入庫文件(即.lib文件)的所在目錄;
在Project’s Properties -> Linker -> Input ->Additional Dependencies,添加動態庫的導入庫的文件名。
隱式調用的優點是,既可以使用導出函數,也可以使用導出類。
2. 顯式調用
應用程序在運行時顯式加載 DLL:
a. 調用 LoadLibrary(或相似的函數)以加載 DLL 和獲取模塊句柄。
b. 調用 GetProcAddress,以獲取指向應用程序要調用的每個導出函數的函數指針。由於應用程序是通過指針調用 DLL的函數,編譯器不生成外部引用,故無需與導入庫鏈接。
c. 使用完 DLL 後調用 FreeLibrary。
顯式調用有一個比較大的問題,就是很難使用導出類。
四、如何找到動態庫
在上一節中,細心的讀者會發現,我們只是在VS中設置和.lib相關的選項,就可以使用動態庫了。但是可執行文件又是如何找到dll文件的呢?畢竟.lib文件未必需要和dll文件放在一起。
其實,搜索動態庫文件dll的順序是這樣的:
1. 包含EXE文件的目錄,
2. 進程的當前工作目錄,
3. Windows系統目錄,
4. Windows目錄,
5. 列在Path環境變量中的一系列目錄。
五、__declspec(dllexport) 和 __declspec(dllimport)
這2個宏是Windows對動態庫(dll)進行編程和使用的時候所特有的,在Linux系統上則不需要這2個宏。
先來看看它們的作用:
1. __declspec(dllexport)可以被用來修飾一個函數或一個類,以表明這是一個導出函數或導出類。所以,這個宏是用在為dll做編程實現的時候的。
當修飾類的時候,該宏要寫在class關鍵字的後面、類名的前面;
當修飾函數的時候,要寫在函數聲明的前面,而因為name mangling的問題,在此宏的前面還要寫上extern“C”. 比如:
extern "C" MYDLL_DECL_EXPORT void say_hello();
2. __declspec(dllimport) 被用來在調用dll的程序裏,表明該程序要調用的某個函數是import自某動態庫的。所以,該宏的具體位置是在對dll進行描述的頭文件中的。
3. 從以上可以看出,在dll的實現中,我們需要__declspec(dllexport)來表明這些函數和類是導出函數和導出類,而在使用dll的程序中,又要用__declspec(dllimport)來表明它所描述的函數或類是來自於某dll。那麽這樣的話,豈不是需要2個不同但又很相近的頭文件來做這些函數和類的聲明了嗎?能否將這2個函數合並成一個呢?答案是可以的 – 使用宏進行判斷:當宏A存在時,就認為宏B是__declspec(dllexport),否則就認為宏B是__declspec(dllimport)。具體實例如下:
#ifdef MYDLL_EXPORTS #define MYDLL_DECL_EXPORT __declspec(dllexport) #else #define MYDLL_DECL_EXPORT __declspec(dllimport) #endif
舉例:
在創建函數庫前,我們先來準備舉例用的源程序,並將函數庫的源程序編譯成.o文件。
無論靜態庫,還是動態庫,都是由.o文件創建的。因此,我們必須將源程序hello.c通過gcc先編譯成hello.o文件。
第1步:編輯得到舉例的程序--hello.h、hello.c和main.c;
hello.h(見程序1)為該函數庫的頭文件。
hello.c(見程序2)是函數庫的源程序,其中包含公用函數hello,該函數將在屏幕上輸出"Hello World!"。
main.c(見程序3)為測試庫文件的主程序,在主程序中調用了公用函數hello。
程序1: hello.h
#ifndef HELLO_H //防止該頭文件被重復引用 #define HELLO_H void hello(const char *name); #endif // !HELLO_H
程序2: hello.c
#include "hello.h" #include<stdio.h> void hello(const char *name) { printf("Hello %s!", name); }
程序3: main.c
#include "hello.h" int main() { hello(" everyone"); return 0; }
第2步:將hello.c編譯成.o文件;
在系統提示符下鍵入以下命令得到hello.o文件。
# gcc -c hello.c
(註2:首字符"#"是系統提示符,不需要鍵入,下文相同。)
下面我們先來看看如何創建靜態庫,以及使用它。
第3步:由.o文件創建靜態庫;
靜態庫文件名的命名規範是以lib為前綴,緊接著跟靜態庫名,擴展名為.a。
例如:我們將創建的靜態庫名為hello,則靜態庫文件名就是libhello.a。在創建和使用靜態庫時,需要註意這點。創建靜態庫用ar命令。
在系統提示符下鍵入以下命令將創建靜態庫文件libmyhello.a。
# ar cr libhello.a hello.o
# ls
hello.c hello.h hello.o libhello.a main.c
第4步:在程序中使用靜態庫;
靜態庫制作完了,如何使用它內部的函數呢?只需要在使用到這些公用函數的源程序中包含這些公用函數的原型聲明,然後在用gcc命令生成目標文件時指明靜態庫名,gcc將會從靜態庫中將公用函數連接到目標文件中。註意,gcc會在靜態庫名前加上前綴lib,然後追加擴展名.a得到的靜態庫文件名來查找靜態庫文件。
在程序3:main.c中,我們包含了靜態庫的頭文件hello.h,然後在主程序main中直接調用公用函數hello。下面先生成目標程序hello,然後運行hello程序看看結果如何。
# gcc -o hello main.c -L. -lmyhello
# ./hello
Hello everyone!
#
我們刪除靜態庫文件試試公用函數hello是否真的連接到目標文件 hello中了。
# rm libmyhello.a
rm: remove regular file `libmyhello.a‘? y
# ./hello
Hello everyone!
#
程序照常運行,靜態庫中的公用函數已經連接到目標文件中了。
我們繼續看看如何在Linux中創建動態庫。我們還是從.o文件開始。
第5步:由.o文件創建動態庫文件;
動態庫文件名命名規範和靜態庫文件名命名規範類似,也是在動態庫名增加前綴lib,但其文件擴展名為.so。例如:我們將創建的動態庫名為hello,則動態庫文件名就是libhello.so。用gcc來創建動態庫。
在系統提示符下鍵入以下命令得到動態庫文件libhello.so。
# gcc -shared -fPCI -o libhello.so hello.o
# ls
hello.c hello.h hello.o libhello.so main.c
第6步:在程序中使用動態庫;
在程序中使用動態庫和使用靜態庫完全一樣,也是在使用到這些公用函數的源程序中包含這些公用函數的原型聲明,然後在用gcc命令生成目標文件時指明動態庫名進行編譯。我們先運行gcc命令生成目標文件,再運行它看看結果。
# gcc -o hello main.c -L. -lhello
# ./hello
./hello: error while loading shared libraries: libhello.so: cannot open shared object file: No such file or directory
#
哦!出錯了。快看看錯誤提示,原來是找不到動態庫文件libhello.so。程序在運行時,會在/usr/lib和/lib等目錄中查找需要的動態庫文件。若找到,則載入動態庫,否則將提示類似上述錯誤而終止程序運行。我們將文件 libhello.so復制到目錄/usr/lib中,再試試。
# mv libhello.so /usr/lib
# ./hello
Hello everyone!
#
成功了。這也進一步說明了動態庫在程序運行時是需要的。
我們回過頭看看,發現使用靜態庫和使用動態庫編譯成目標程序使用的gcc命令完全一樣,那當靜態庫和動態庫同名時,gcc命令會使用哪個庫文件呢?抱著對問題必究到底的心情,來試試看。
先刪除 除.c和.h外的 所有文件,恢復成我們剛剛編輯完舉例程序狀態。
# rm -f hello hello.o /usr/lib/libhello.so
# ls
hello.c hello.h main.c
#
在來創建靜態庫文件libhello.a和動態庫文件libhello.so。
# gcc -c hello.c
# ar cr libhello.a hello.o
# gcc -shared -fPCI -o libhello.so hello.o
# ls
hello.c hello.h hello.o libhello.a libhello.so main.c
#
通過上述最後一條ls命令,可以發現靜態庫文件libhello.a和動態庫文件libhello.so都已經生成,並都在當前目錄中。然後,我們運行gcc命令來使用函數庫hello生成目標文件hello,並運行程序 hello。
# gcc -o hello main.c -L. -lhello
# ./hello
./hello: error while loading shared libraries: libhello.so: cannot open shared object file: No such file or directory
#
從程序hello運行的結果中很容易知道,當靜態庫和動態庫同名時, gcc命令將優先使用動態庫。
C++基礎知識之動態庫靜態庫