目錄
1. 庫的概念
2. 靜態庫(Static Libraries)
2.1 靜態庫的制作
2.2 靜態庫的使用
2.2.1 顯式指定庫文件及頭文件路徑
2.2.2?將庫文件安裝到系統目錄
2.2.3?將頭文件安裝到系統目錄
3. 動態庫
3.1 動態庫的制作
3.2 動態庫的使用
3.2.1 顯式指定庫文件路徑
2.2.2?將路徑加載到環境變量中?
2.2.3 配置文件
4. 總結與補充
1. 庫的概念
庫(Library) 是一組預先編譯好的代碼(函數、類、數據等)的集合,可以被多個程序共享和重復使用。庫的核心目的是代碼復用,避免開發者重復編寫相同的功能(如文件操作、數學計算等)。
本質上來說庫是一種可執行代碼的二進制形式,可以被操作系統載如內存執行。
按照代碼復用的形式,庫可以分為兩種:
- 靜態庫:.a?[Linux]、.lib?[Windows]
- 動態庫:.so?[Linux]、.dll?[Windows]
庫是在鏈接這一步被使用的,實際上就是一堆 .o 文件的集合,我們可以特定的工具來將這些 .o 文件進行打包,進而形成庫。?
為舉例方便,這里給出我們自己實現的簡單的C語言庫---myc:
// mystdio.h#pragma once
#include <stdio.h>
#define MAX 1024
#define NONE_FLUSH (1<<0)
#define LINE_FLUSH (1<<1)
#define FULL_FLUSH (1<<2)typedef struct IO_FILE
{int fileno;int flag;char outbuffer[MAX];int bufferlen;int flush_method;
}MyFile;MyFile *MyFopen(const char *path, const char *mode);
void MyFclose(MyFile *);
int MyFwrite(MyFile *, void *str, int len);
void MyFFlush(MyFile *);// mystdio.c#include "mystdio.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>static MyFile *BuyFile(int fd, int flag)
{MyFile *f = (MyFile*)malloc(sizeof(MyFile));if(f == NULL) return NULL;f->bufferlen = 0;f->fileno = fd;f->flag = flag;f->flush_method = LINE_FLUSH;memset(f->outbuffer, 0, sizeof(f->outbuffer));return f;
}MyFile *MyFopen(const char *path, const char *mode)
{int fd = -1;int flag = 0;if(strcmp(mode, "w") == 0){flag = O_CREAT | O_WRONLY | O_TRUNC;fd = open(path, flag, 0666);}else if(strcmp(mode, "a") == 0){flag = O_CREAT | O_WRONLY | O_APPEND;fd = open(path, flag, 0666);}else if(strcmp(mode, "r") == 0){flag = O_RDWR;fd = open(path, flag);}else{//TODO}if(fd < 0) return NULL;return BuyFile(fd, flag);
}
void MyFclose(MyFile *file)
{if(file->fileno < 0) return;MyFFlush(file);close(file->fileno);free(file);
}
int MyFwrite(MyFile *file, void *str, int len)
{// 1. 拷貝memcpy(file->outbuffer+file->bufferlen, str, len);file->bufferlen += len;// 2. 嘗試判斷是否滿足刷新條件!if((file->flush_method & LINE_FLUSH) && file->outbuffer[file->bufferlen-1] == '\n'){MyFFlush(file);}return 0;
}
void MyFFlush(MyFile *file)
{if(file->bufferlen <= 0) return;// 把數據從用戶拷貝到內核文件緩沖區中int n = write(file->fileno, file->outbuffer, file->bufferlen);(void)n;fsync(file->fileno);file->bufferlen = 0;
}// mystring.h#pragma once
int my_strlen(const char *s);// mystring.c#include "mystring.h"int my_strlen(const char *s)
{const char *start = s;while(*s){s++;}return s - start;
}
接下來,我們會介紹如何將上述的原文件打包成動靜態庫并使用。?
2. 靜態庫(Static Libraries)
文件擴展名:.a(Archive)
- 特點:
在編譯時,庫的代碼會被直接復制到最終的可執行文件中。
生成的可執行文件獨立,不依賴運行時環境中的庫文件。
缺點:文件體積較大,且更新庫時需要重新編譯程序。
創建工具:ar(歸檔工具)+ ranlib(生成索引)。
使用場景:適合對程序獨立性要求高的場景。
2.1 靜態庫的制作
靜態庫使用 ar 指令進行打包:
ar -rc lib[庫名].a [目標文件s]
lib[庫名].a 是靜態庫文件的命名規范,實際上的庫名需要去掉lib前綴以及.a擴展名。
通常來說,只有庫文件是不夠的,還需要將庫的頭文件交給用戶,所以我們可以使用如下的Makefile來將庫及其頭文件一起打包交給用戶:
SRC=$(wildcard *.c)
OBJ=$(SRC:.c=.o)libmyc.a:$(OBJ)ar -rc $@ $^$(OBJ):$(SRC)gcc -c $^.PHONY:output
output:mkdir -p lib/includemkdir -p lib/mylibcp -f *.h lib/includecp -f *.a lib/mylibtar czf lib.tgz lib.PHONY:clean
clean:rm -rf *.o libmyc.a lib lib.tgz
2.2 靜態庫的使用
gcc/g++會默認鏈接c標準庫,但是myc庫是我們自己制作的第三方庫,所以在編譯時需要指定鏈接myc庫。
假設用戶已經接收到了我們的 lib.tgz 包,并且用戶的代碼(usercode.c)調用了我們庫中的方法:
注:使用tar xzf lib.tgz進行解包得到lib目錄。?
// usercode.c#include "mystdio.h"
#include "mystring.h"
#include <string.h>
#include <unistd.h>int main()
{MyFile *filep = MyFopen("./log.txt", "a");if(!filep){printf("fopen error!\n");return 1;}int cnt = 10;while(cnt--){char *msg = (char*)"hello myfile!!!";MyFwrite(filep, msg, strlen(msg));MyFFlush(filep);printf("buffer: %s\n", filep->outbuffer);sleep(1);}MyFclose(filep); // FILE *fpconst char *str = "hello bit!\n";printf("strlen: %d\n",my_strlen(str));return 0;
}
2.2.1 顯式指定庫文件及頭文件路徑
在編譯時,需要指定頭文件所在路徑、要鏈接的庫文件路徑以及指定庫文件:
gcc -o [可執行程序] [目標文件s] -I [頭文件路徑] -L [庫路徑] -l [庫名]
2.2.2?將庫文件安裝到系統目錄
我們知道,所謂安裝,實際上就是把文件拷貝到指定的系統目錄下。這樣,在我們未顯式指定庫文件所在目錄時,系統就能夠在默認目錄中找到。
當然,除了拷貝,建立鏈接也是可以的。?
- /lib、/usr/lib:系統級庫
- /usr/local/lib:用戶安裝的第三方庫
我們將 libmyc.a 文件拷貝到三個庫中的一個即可完成安裝,此時不在需要指明庫所在路徑:
但是,不建議安裝到系統級庫,用戶自己要安裝的第三方庫最好安裝到 /usr/local/lib 中。?
2.2.3?將頭文件安裝到系統目錄
- /usr/include:系統級頭文件
- /usr/local/include:本地安裝的第三方庫頭文件
- /usr/include/<庫名> 或 /usr/local/include/<庫名>:特定軟件的子目錄
我們將自己的頭文件拷貝到上述目錄下即可完成安裝,此時不再需要指明頭文件所在路徑:
3. 動態庫
文件擴展名:.so(Shared Object)
- 特點:
在程序運行時被動態加載到內存,多個程序可共享同一份庫代碼。
可執行文件體積小,庫更新時無需重新編譯程序。
缺點:依賴運行時環境中的庫文件(若缺失會導致程序無法運行)。
創建工具:gcc/g++ 的 -shared 選項。
使用場景:大多數系統庫(如 glibc)和通用功能庫(如 OpenSSL)。
3.1 動態庫的制作
// 編譯目標文件時需要帶上-fPIC選項,fPIC:產生位置無關碼(position independent code)
gcc/g++ -c -fPIC [原文件s]// 生成庫文件時需要帶上-shared選項,shared: 表示生成共享庫格式
gcc/g++ -o lib[庫名].so [目標文件s] -shared
同樣的,lib[庫名].so 是命名規范,實際上的庫名需要去掉lib前綴和 .so擴展名。
我們可以使用如下的Makefile來對庫及其頭文件進行打包:
SRC=$(wildcard *.c)
OBJ=$(SRC:.c=.o)libmyc.so:$(OBJ)gcc -shared -o $@ $^$(OBJ):$(SRC)gcc -fPIC -c $^.PHONY:output
output:mkdir -p lib/includemkdir -p lib/mylibcp -f *.h lib/includecp -f *.so lib/mylibtar czf lib.tgz lib.PHONY:clean
clean:rm -rf *.o libmyc.so lib lib.tgz
3.2 動態庫的使用
我們以同樣的代碼作為示例,將庫及頭文件安裝到系統目錄的方式與靜態庫一樣,這里就不再重復,但是對于顯式給出庫文件路徑的方式,我們要多說兩句。
3.2.1 顯式指定庫文件路徑
假如我們未將庫文件安裝到系統目錄當中,并顯式指定某路徑下的庫文件:
我們會發現編譯通過了,但是:
當我們運行生成的可執行程序時,會發現系統顯式找不到對應的庫。
這是因為,我們僅僅告訴了編譯器:“這個庫是存在的”,所以編譯器完成了編譯。
但是動態鏈接是在程序運行時才將庫與可執行程序產生鏈接,負責鏈接的是系統,然而系統并不知道在哪里找到這個庫。?
要讓操作系統在運行我們的程序時找到對應的動態庫,我們可以選擇安裝的形式(與靜態庫的安裝完全一致),也可采取以下幾點中提到的措施。
?注意:與靜態鏈接不同,接下來的幾點措施(包括安裝),都不需要重新編譯可執行文件。
2.2.2?將路徑加載到環境變量中?
# LD_LIBRARY_PATH:臨時指定額外的庫搜索路徑。
export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/path/to/libs
使用 ldd 命令可以查看可執行程序鏈接的庫及其所在路徑:
但是,這種方式只是臨時的,重新登錄或更新環境變量時就會失效。?
2.2.3 配置文件
- /etc/ld.so.conf:系統級庫路徑配置文件
- /etc/ld.so.conf.d:用戶庫路徑配置文件目錄
我們可以直接在/etc/ld.so.conf中加入我們庫文件的路徑,但是我們依然更建議在用戶庫路徑配置文件目錄中添加自己的配置文件:
這里sudo echo創建文件的方式居然不行,只能用編輯器創建了。
然后加載配置文件:
sudo ldconfig
?結果與2.2.2相同,這里就不展示了。
4. 總結與補充
- gcc/g++編譯命令補充:
-
[-I] :指定頭文件所在目錄。
-
[-L]:指定庫文件所在路徑。
-
[-l]:指定要鏈接的庫。
-
[-shared]:生成動態庫。
-
[-fPIC]:產生位置無關碼。
-
[-static]:使用靜態鏈接。
-
- 靜態庫使用ar命令進行打包:
ar -rc lib[庫名].a [目標文件s]
- 將靜態庫與用戶目標文件一起編譯即可生成可執行程序。
- ?動態庫使用gcc/g++進行打包,且目標文件需要攜帶位置無關碼:
// 編譯目標文件時需要帶上-fPIC選項,fPIC:產生位置無關碼(position independent code) gcc/g++ -c -fPIC [原文件s]// 生成庫文件時需要帶上-shared選項,shared: 表示生成共享庫格式 gcc/g++ -o lib[庫名].so [目標文件s] -shared
- 動態庫在編譯時需要讓gcc/g++知道這個庫是存在的(給出路徑或安裝到系統,并指定庫名)。在運行時,系統需要能夠找到這個庫(需要安裝到系統)。
- 第三方庫在編譯時要指定鏈接這個庫。
- 在編譯時,我們的系統當中可能既安裝了某個庫的動態版本,又安裝了某個庫的靜態版本。此時,編譯器默認能采用動態鏈接則采用動態鏈接。如果要使用靜態鏈接則需要帶上 -static 選項,一旦帶上這個選項,就意味著動態鏈接被禁用,如果某個庫只有動態鏈接的版本,則會發生鏈接失敗。