? ? ? ? 不管是Linux還是Windows中的庫文件其本質和工作模式都是相同的, 只不過在不同的平臺上庫對應的文件格式和文件后綴不同。程序中調用的庫有兩種 靜態庫和動態庫,不管是哪種庫文件本質是還是源文件,只不過是二進制格式只有計算機能夠識別,作為一個普通人就無能為力了。
????????在項目中使用庫一般有兩個目的,一個是為了使程序更加簡潔不需要在項目中維護太多的源文件,另一方面是為了源代碼保密,畢竟不是所有人都想把自己編寫的程序開源出來。
????????當我們拿到了庫文件(動態庫、靜態庫)之后要想使用還必須有這些庫中提供的API函數的聲明,也就是頭文件,把這些都添加到項目中,就可以快樂的寫代碼了。
1. 靜態庫
????????在Linux中靜態庫由程序 ar 生成,現在靜態庫已經不像之前那么普遍了,這主要是由于程序都在使用動態庫。關于靜態庫的命名規則如下:
- 在Linux中靜態庫以lib作為前綴, 以.a作為后綴, 中間是庫的名字自己指定即可, 即: libxxx.a
- 在Windows中靜態庫一般以lib作為前綴, 以lib作為后綴, 中間是庫的名字需要自己指定, 即: libxxx.lib
1.1 生成靜態鏈接庫
????????生成靜態庫,需要先對源文件進行匯編操作 (使用參數 -c) 得到二進制格式的目標文件 (.o 格式), 然后在通過 ar工具將目標文件打包就可以得到靜態庫文件了 (libxxx.a)。
使用ar工具創建靜態庫的時候需要三個參數:
- 參數c:創建一個庫,不管庫是否存在,都將創建。
- 參數s:創建目標文件索引,這在創建較大的庫時能加快時間。
- 參數r:在庫中插入模塊(替換)。默認新的成員添加在庫的結尾處,如果模塊名已經在庫中存在,則替換同名的模塊。
生成靜態鏈接庫的具體步驟如下:
????????1.需要將源文件進行匯編, 得到 .o 文件, 需要使用參數 -c????????
# 執行如下操作, 默認生成二進制的 .o 文件
# -c 參數位置沒有要求
$ gcc 源文件(*.c) -c
????????2.將得到的 .o 進行打包, 得到靜態庫
$ ar rcs 靜態庫的名字(libxxx.a) 原材料(*.o)
? ? ? ? 3.發布靜態庫
# 發布靜態庫1. 提供頭文件 **.h2. 提供制作出來的靜態庫 libxxx.a
1.2 靜態庫制作舉例
1.2.1 準備測試程序
在某個目錄中有如下的源文件, 用來實現一個簡單的計算器:
# 目錄結構 add.c div.c mult.c sub.c -> 算法的源文件, 函數聲明在頭文件 head.h
# main.c中是對接口的測試程序, 制作庫的時候不需要將 main.c 算進去
.
├── add.c
├── div.c
├── include
│?? └── head.h
├── main.c
├── mult.c
└── sub.c
加法計算源文件 add.c:
#include <stdio.h>
#include "head.h"int add(int a, int b)
{return a+b;
}
減法計算源文件 sub.c:
#include <stdio.h>
#include "head.h"int subtract(int a, int b)
{return a-b;
}
乘法計算源文件 mult.c:
#include <stdio.h>
#include "head.h"int multiply(int a, int b)
{return a*b;
}
減法計算的源文件 div.c
#include <stdio.h>
#include "head.h"double divide(int a, int b)
{return (double)a/b;
}
頭文件 head.h
#ifndef _HEAD_H
#define _HEAD_H
// 加法
int add(int a, int b);
// 減法
int subtract(int a, int b);
// 乘法
int multiply(int a, int b);
// 除法
double divide(int a, int b);
#endif
測試文件main.c
#include <stdio.h>
#include "head.h"int main()
{int a = 20;int b = 12;printf("a = %d, b = %d\n", a, b);printf("a + b = %d\n", add(a, b));printf("a - b = %d\n", subtract(a, b));printf("a * b = %d\n", multiply(a, b));printf("a / b = %f\n", divide(a, b));return 0;
}
1.2.2 生成靜態庫
????????第一步: 將源文件add.c, div.c, mult.c, sub.c 進行匯編, 得到二進制目標文件 add.o, div.o, mult.o, sub.o
# 1. 生成.o
$ gcc add.c div.c mult.c sub.c -c
sub.c:2:18: fatal error: head.h: No such file or directory
compilation terminated.# 提示頭文件找不到, 添加參數 -I 重新頭文件路徑即可
$ gcc add.c div.c mult.c sub.c -c -I ./include/# 查看目標文件是否已經生成
$ tree
.
├── add.c
├── add.o # 目標文件
├── div.c
├── div.o # 目標文件
├── include
│?? └── head.h
├── main.c
├── mult.c
├── mult.o # 目標文件
├── sub.c
└── sub.o # 目標文件
第二步: 將生成的目標文件通過 ar工具打包生成靜態庫
# 2. 將生成的目標文件 .o 打包成靜態庫
$ ar rcs libcalc.a a.o b.o c.o # a.o b.o c.o在同一個目錄中可以寫成 *.o# 查看目錄中的文件
$ tree
.
├── add.c
├── add.o
├── div.c
├── div.o
├── include
│?? └── `head.h ===> 和靜態庫一并發布
├── `libcalc.a ===> 生成的靜態庫
├── main.c
├── mult.c
├── mult.o
├── sub.c
└── sub.o
第三步: 將生成的的靜態庫 libcalc.a和庫對應的頭文件head.h一并發布給使用者就可以了
# 3. 發布靜態庫1. head.h => 函數聲明2. libcalc.a => 函數定義(二進制格式)
1.3 靜態庫的使用
????????當我們得到了一個可用的靜態庫之后, 需要將其放到一個目錄中, 然后根據得到的頭文件編寫測試代碼, 對靜態庫中的函數進行調用。
# 1. 首先拿到了發布的靜態庫`head.h` 和 `libcalc.a`# 2. 將靜態庫, 頭文件, 測試程序放到一個目錄中準備進行測試
.
├── head.h # 函數聲明
├── libcalc.a # 函數定義(二進制格式)
└── main.c # 函數測試
編譯測試程序, 得到可執行文件。
# 3. 編譯測試程序 main.c
$ gcc main.c -o app
/tmp/ccR7Fk49.o: In function `main':
main.c:(.text+0x38): undefined reference to `add'
main.c:(.text+0x58): undefined reference to `subtract'
main.c:(.text+0x78): undefined reference to `multiply'
main.c:(.text+0x98): undefined reference to `divide'
collect2: error: ld returned 1 exit status
上述錯誤分析:
????????編譯的源文件中包含了頭文件 head.h, 這個頭文件中聲明的函數對應的定義(也就是函數體實現)在靜態庫中,程序在編譯的時候沒有找到函數實現,因此提示 undefined reference to xxxx。
解決方案:在編譯的時將靜態庫的路徑和名字都指定出來
- -L: 指定庫所在的目錄(相對或者絕對路徑)
- -l: 指定庫的名字, 需要掐頭(lib)去尾(.a) 剩下的才是需要的靜態庫的名字
# 4. 編譯的時候指定庫信息-L: 指定庫所在的目錄(相對或者絕對路徑)-l: 指定庫的名字, 掐頭(lib)去尾(.a) ==> calc
# -L -l, 參數和參數值之間可以有空格, 也可以沒有 -L./ -lcalc
$ gcc main.c -o app -L ./ -l calc# 查看目錄信息, 發現可執行程序已經生成了
$ tree
.
├── app # 生成的可執行程序
├── head.h
├── libcalc.a
└── main.c
2. 動態庫
????????動態鏈接庫是程序運行時加載的庫,當動態鏈接庫正確部署之后,運行的多個程序可以使用同一個加載到內存中的動態庫,因此在Linux中動態鏈接庫也可稱之為共享庫。
????????動態鏈接庫是目標文件的集合,目標文件在動態鏈接庫中的組織方式是按照特殊方式形成的。庫中函數和變量的地址使用的是相對地址(靜態庫中使用的是絕對地址),其真實地址是在應用程序加載動態庫時形成的。
關于動態庫的命名規則如下:
- 在Linux中動態庫以lib作為前綴, 以.so作為后綴, 中間是庫的名字自己指定即可, 即: libxxx.so
- 在Windows中動態庫一般以lib作為前綴, 以dll作為后綴, 中間是庫的名字需要自己指定, 即: libxxx.dll
2.1 生成動態鏈接庫
????????生成動態鏈接庫是直接使用gcc命令并且需要添加-fPIC(-fpic) 以及-shared 參數。
- -fPIC 或 -fpic 參數的作用是使得 gcc 生成的代碼是與位置無關的,也就是使用相對位置。
- -shared參數的作用是告訴編譯器生成一個動態鏈接庫。
生成動態鏈接庫的具體步驟如下:
? ? ? ? 1、將源文件進行匯編操作, 需要使用參數 -c, 還需要添加額外參數 -fpic / -fPIC
# 得到若干個 .o文件
$ gcc 源文件(*.c) -c -fpic
? ? ? ?2、 將得到的.o文件打包成動態庫, 還是使用gcc, 使用參數 -shared 指定生成動態庫(位置沒有要求)
$ gcc -shared 與位置無關的目標文件(*.o) -o 動態庫(libxxx.so)
? ? ? ? 3、發布動態庫和頭文件
# 發布1. 提供頭文件: xxx.h2. 提供動態庫: libxxx.so
2.2 動態庫制作舉例
????????在此還是以上面制作靜態庫使用的實例代碼為例來制作動態庫, 代碼目錄如下:
# 舉例, 示例目錄如下:
# 目錄結構 add.c div.c mult.c sub.c -> 算法的源文件, 函數聲明在頭文件 head.h
# main.c中是對接口的測試程序, 制作庫的時候不需要將 main.c 算進去
.
├── add.c
├── div.c
├── include
│?? └── head.h
├── main.c
├── mult.c
└── sub.c
第一步: 使用gcc將源文件進行匯編(參數-c), 生成與位置無關的目標文件, 需要使用參數 -fpic或者-fPIC
# 1. 將.c匯編得到.o, 需要額外的參數 -fpic/-fPIC
$ gcc add.c div.c mult.c sub.c -c -fpic -I ./include/# 查看目錄文件信息, 檢查是否生成了目標文件
$ tree
.
├── add.c
├── add.o # 生成的目標文件
├── div.c
├── div.o # 生成的目標文件
├── include
│?? └── head.h
├── main.c
├── mult.c
├── mult.o # 生成的目標文件
├── sub.c
└── sub.o # 生成的目標文件
第二步: 使用gcc將得到的目標文件打包生成動態庫, 需要使用參數 -shared
# 2. 將得到 .o 打包成動態庫, 使用gcc , 參數 -shared
$ gcc -shared add.o div.o mult.o sub.o -o libcalc.so # 檢查目錄中是否生成了動態庫
$ tree
.
├── add.c
├── add.o
├── div.c
├── div.o
├── include
│?? └── `head.h ===> 和動態庫一起發布
├── `libcalc.so ===> 生成的動態庫
├── main.c
├── mult.c
├── mult.o
├── sub.c
└── sub.o
第三步: 發布生成的動態庫和相關的頭文件
# 3. 發布庫文件和頭文件1. head.h2. libcalc.so
2.3 動態庫的使用
????????當我們得到了一個可用的動態庫之后, 需要將其放到一個目錄中, 然后根據得到的頭文件編寫測試代碼, 對動態庫中的函數進行調用。
# 1. 拿到發布的動態庫`head.h libcalc.so
# 2. 基于頭文件編寫測試程序, 測試動態庫中提供的接口是否可用`main.c`
# 示例目錄:
.
├── head.h ==> 函數聲明
├── libcalc.so ==> 函數定義
└── main.c ==> 函數測試
編譯測試程序
# 3. 編譯測試程序
$ gcc main.c -o app
/tmp/ccwlUpVy.o: In function `main':
main.c:(.text+0x38): undefined reference to `add'
main.c:(.text+0x58): undefined reference to `subtract'
main.c:(.text+0x78): undefined reference to `multiply'
main.c:(.text+0x98): undefined reference to `divide'
collect2: error: ld returned 1 exit status
錯誤原因:
- 和使用靜態庫一樣, 在編譯的時候需要指定庫相關的信息: 庫的路徑 -L和 庫的名字 -l
添加庫信息相關參數, 重新編譯測試代碼:
# 在編譯的時候指定動態庫相關的信息: 庫的路徑 -L, 庫的名字 -l
$ gcc main.c -o app -L./ -lcalc# 查看是否生成了可執行程序
$ tree
.
├── app # 生成的可執行程序
├── head.h
├── libcalc.so
└── main.c# 執行生成的可執行程序, 錯誤提示 ==> 可執行程序執行的時候找不到動態庫
$ ./app
./app: error while loading shared libraries: libcalc.so: cannot open shared object file: No such file or directory
3. 優缺點
3.1 靜態庫
優點:
- 靜態庫被打包到應用程序中加載速度快
- 發布程序無需提供靜態庫,移植方便
缺點:
- 相同的庫文件數據可能在內存中被加載多份, 消耗系統資源,浪費內存
- 庫文件更新需要重新編譯項目文件, 生成新的可執行程序, 浪費時間。
3.2 動態庫
優點:
- 可實現不同進程間的資源共享
- 動態庫升級簡單, 只需要替換庫文件, 無需重新編譯應用程序
- 程序猿可以控制何時加載動態庫, 不調用庫函數動態庫不會被加載
缺點:
- 加載速度比靜態庫慢, 以現在計算機的性能可以忽略
- 發 布程序需要提供依賴的動態庫