35.1.函數庫的前世今生
(1)函數庫就是一些事先寫好的函數的集合,因爲函數是模塊化的,因此可以被複用;我們寫好了某個函數,可以被反覆使用,譬如A寫好了某個函數然後共享出來,當B有相同的需求時就不需自己寫直接用A寫好的這個函數即可。
(2)最開始是沒有函數庫的,每個人寫程序都要從零開始自己寫,時間長了慢慢的早期的程序員就積累下來了一些有用的函數;早期的程序員經常參加行業聚會,在聚會上大家互相交換各自的函數庫;後來程序員中的一些大神就提出把大家各自的函數庫收攏在一起,然後經過校準和整理,最後形成了一份標準化的函數庫,就是現在的標準的函數庫,譬如說glibc。
(3)早期的函數共享都是以源代碼的形式進行的,該方式共享是最徹底的(後來這種源碼共享的方向就形成了我們現在的開源社區),但該方式的缺點就是無法以商業化形式來發布函數庫;商業公司需要將自己的有用的函數庫共享給別人(當然是付費的),但又不能給客戶源代碼,則解決方案就是以庫(靜態庫和動態庫)的形式來提供。
35.2.靜態庫和動態庫
(1)較早出現的是靜態鏈接庫,靜態庫其實就是商業公司將自己的函數庫源代碼經過只編譯不鏈接形成.o的目標文件,然後用ar工具將.o文件歸檔成.a的歸檔文件(.a的歸檔文件又叫靜態鏈接庫文件);商業公司通過發佈.a庫文件和.h頭文件來提供靜態庫給客戶使用;客戶拿到.a和.h文件後,通過.h頭文件得知庫中的庫函數的原型,然後在自己的.c文件中直接調用這些庫文件,在鏈接的時候鏈接器會去.a文件中拿出被調用的函數的編譯後的.o二進制代碼段鏈接進去形成最終的可執行程序;靜態庫在用戶鏈接自己的可執行程序時就已經把調用的庫中的函數的代碼段鏈接進最終可執行程序中了,該方式的優點是可以獨立執行,缺點是程序太過龐大,尤其是有多個應用程序都使用了該庫函數時,實際上在多個應用程序最後生成的可執行程序中都各自有1份該庫函數的代碼段,當這些應用程序同時在內存中運行時,實際上在內存中有多個該庫函數的代碼段,導致重複加載。
(2)動態鏈接庫出現較晚,效率更高一些,現在我們一般都是使用動態庫;動態鏈接庫本身不將庫函數的代碼段鏈接入可執行程序,只是做個標記,然後當應用程序在內存中執行時,運行時環境發現它調用了某個動態庫中的庫函數時,會去加載這個動態庫到內存中,以後不管有多少個應用程序去調用該庫中的函數都會跳轉到第1次加載的地方去執行,不會重複加載。
(3)gcc中編譯鏈接程序默認是使用動態庫的,要想使用靜態鏈接需要顯式用-static來強制靜態鏈接;調用庫函數時應包含相應的頭文件;調用庫函數時需注意函數原型;有些庫函數鏈接時需要額外用-lxxx來指定鏈接(譬如數學庫和線程庫);如果使用動態庫要注意-L指定動態庫的地址。
35.3.字符串庫函數
(1)字符串就是由多個字符在內存中連續分佈組成的字符結構,字符串的特點是指定了開頭(字符串的指針)和結尾(結尾固定爲字符’\0’),而沒有指定長度(長度由開頭地址和結尾地址相減得到)。
(2)因爲字符串處理的需求是客觀的,所以從很早開始人們就在寫很多關於字符串處理的函數,然後逐漸形成了現在的字符串處理函數庫;面試筆試時,常用字符串處理函數也是經常考到的點。
(3)C庫中字符串處理函數包含在string.h中,該文件在ubuntu系統位於/usr/include目錄中,在學習字符串處理函數時可參照string.h文件和man手冊及網上的博客針對常用的字符串口處理函數進行針對性的學習和總結,這樣能在很大程度上提高編程能力。
(4)常見字符串處理函數memcpy(確定src和dst不會overlap,則使用memcpy效率高)、memmove(確定會overlap或者不確定但是有可能overlap,則使用memove比較保險)、memset、memcmp、memchr、strcpy、strncpy、strcat、strncat、strcmp、strncmp、strdup、strndup、strchr、strstr、strtok。
35.4.數學庫函數
(1)使用數學庫函數的時候,只需要包含math.h即可;真正的數學運算的函數定義在/usr/include/i386-linux-gnu/bits/mathcalls.h中。
(2)計算開平方時使用庫函數double sqrt(double x);在編譯後需注意區分編譯時警告/錯誤及鏈接時的錯誤;4.6.10.math.c:9:13: warning: incompatible implicit declaration of built-in function ‘sqrt’ [enabled by default](編譯時警告/錯誤);double b = sqrt(a);;4.6.10.math.c:(.text+0x1b): undefined reference to `sqrt’ collect2: error: ld returned 1 exit status(鏈接時錯誤,sqrt函數有聲明(聲明就在math.h中)有引用(在math.c)但是沒有定義,鏈接器找不到函數體;sqrt本來是庫函數,在編譯器庫中是有.a和.so鏈接庫的(函數體在鏈接庫中的))。
(3)C鏈接器的工作特點,因爲庫函數有很多,鏈接器去庫函數目錄搜索的時間比較久,爲了提升速度採用了折中的方案;鏈接器只是默認的尋找幾個最常用的庫,如果是一些不常用的庫中的函數被調用,需要程序員在鏈接時明確給出要擴展查找的庫的名字;鏈接時可以用-lxxx來指示鏈接器去到libxxx.so中去查找這個函數。
(4)鏈接時加-lm即告訴鏈接器到libm中去查找用到的函數;實戰中發現在高版本的gcc中,經常會出現沒加-lm也可以編譯鏈接的;我們可使用ldd命令查看可執行程序鏈接使用到的庫的路徑(ldd a.out)。
35.5.自己製作靜態鏈接庫並使用
(1)自己製作靜態鏈接庫;首先使用gcc -c只編譯不連接(gcc rston.c -o rston.o -c),生成.o文件;然後使用ar工具打包成.a歸檔文件(ar -rc librston.a rston.o);庫名不能隨便亂起,一般是lib+庫名稱(librston);後綴名是.a表示是1個歸檔文件(librston.a);製作出靜態庫之後,發佈時需要發佈.a文件和.h文件。
(2)使用靜態鏈接庫;把.a和.h都放在某個目錄中,然後在.c文件中包含庫的.h,然後直接使用庫函數;第1次編譯方法(gcc testlib.c -o testlib)+報錯信息testlib.c:(.text+0x19): undefined reference to `add’+collect2: ld returned 1 exit status;第2次編譯方法(gcc testlib.c -o testlib -lrston)+報錯信息/usr/bin/ld: cannot find -lrston+collect2: ld returned 1 exit status;第3次編譯方法(gcc testlib.c -o testlib -lrston -L.)+無報錯,生成testlib,執行正確。
(3)ar命令和nm命令,除了ar歸檔命令外,還有個nm命令也很有用,它可以用來查看某個.a文件中都有哪些符號。
35.6.自己製作動態鏈接庫並使用
(1)自己製作動態鏈接庫動;動態鏈接庫的後綴名是.so(對應windows系統中的dll),靜態鏈接庫的擴展名是.a;首先使用gcc -c只編譯不連接(gcc rston.c -o rston.o -c -FPIC;-fPIC是位置無關碼),生成.o文件;然後使用gcc工具打包成.so文件(gcc rston.o -o librston.so -shared;-shared是按照共享庫的方式來鏈接);製作成動態庫後,發佈時需發佈.so文件和.h文件。
(2)使用動態鏈接庫;第1次編譯方法(gcc testlib.c -o testlib)+報錯信息testlib.c:(.text+0x19): undefined reference to `add’+collect2: ld returned 1 exit status;第2次編譯方法(gcc testlib.c -o testlib -lrston)+報錯信息/usr/bin/ld: cannot find -lrston+collect2: ld returned 1 exit status;第3次編譯方法(gcc testlib.c -o testlib -lrston -L.)+無報錯,生成testlib,執行錯誤。
(3)執行報錯信息(error while loading shared libraries: librston.so: cannot open shared object file: No such file or directory);錯誤原因是動態鏈接庫運行時需要被加載(運行時環境在執行testlib程序的時候發現其動態鏈接了librston.so,於是乎會去固定目錄嘗試加載libaston.so,如果加載失敗則會打印以上錯誤信息)。
(4)解決方法1是將librston.so放到固定目錄下即可,該固定目錄一般是/usr/lib目錄(sudo cp librston.so /usr/lib);解決方法2是將libaston.so所在的目錄導出到環境變量LD_LIBRARY_PATH中,操作系統在加載固定目錄/usr/lib之前,會先去LD_LIBRARY_PATH這個環境變量所指定的目錄下去尋找,如果找到就不用去/usr/lib下面找了,如果沒找到再去/usr/lib下面找(export LD_LIBRARY_PATH=dD_LIBRARY_PATH:d(pwd)注意此爲shell中運行該命令;若在Makefile中則需運行命令export LD_LIBRARY_PATH=dD_LIBRARY_PATH:d(shell pwd) && ./testlib);解決方法3是使用ldconfig(僅限於ubuntu中)。
(5)ldd命令的作用是可以在某個使用了共享庫(動態鏈接庫)的程序執行之前解析出該程序使用了哪些共享庫,並且查看這些共享庫是否能被找到並解析,即使用該命令可提前知道該程序能否正確執行。
35.library
/*
* 公司:XXXX
* 作者:Rston
* 博客:http://blog.csdn.net/rston
* GitHub:https://github.com/rston
* 項目:靜態鏈接庫和動態鏈接庫
* 功能:演示靜態庫和動態庫所佔用空間大小對比。
*/
#include <stdio.h>
int main(int argc, char **argv)
{
// 動態庫->gcc 35.library.c->ls -l a.out(大小:7164)
// 靜態庫->gcc 35.library.c -static->ls -l a.out(大小:751243)
printf("hello world.\n");
return 0;
}
35.mem_cpy_move_ccpy
/*
* 公司:XXXX
* 作者:Rston
* 博客:http://blog.csdn.net/rston
* GitHub:https://github.com/rston
* 項目:靜態鏈接庫和動態鏈接庫
* 功能:演示memcoy和memmove和memccoy函數的用法和區別。
*/
#include <stdio.h>
#include <string.h>
//#define NDEBUG
#include <assert.h>
// linux下memcpy源碼
// void *memcpy(void *dest, const void *src, size_t n);
// 從源src所指的內存地址的起始位置開始拷貝n個字節
// 到目標dest所指的內存地址的起始位置中,返回值爲指向dest的指針
void *memcpy1(void *dest, const void *src, size_t n)
{
assert((src != NULL) && (dest != NULL)); // 判斷參數合法性
void *rev = dest;
while (n--)
{
*(char *)dest = *(char *)src;
dest = (char *)dest + 1; // 單個字節進行copy
src = (char *)src + 1;
}
return rev; // 返回值爲指向dest的指針
}
// linux下memmove源碼
// void *memmove(void *dest, const void *src, size_t n);
// 從源src所指的內存地址的起始位置開始拷貝n個字節
// 到目標dest所指的內存地址的起始位置中,返回值爲指向dest的指針
void *memmove1(void *dest, const void *src, size_t n)
{
assert((src != NULL) && (dest != NULL)); // 判斷參數合法性
void *rev = dest;
// 原則:未拷貝的src數據與已拷貝的dest數據不能被改變
if ((src >= dest) || ((char *)dest >= ((char *)src + n)))
{// 若dst和src區域沒有重疊,則從起始處開始逐一拷貝
while (n--)
{
*(char *)dest = *(char *)src;
dest = (char *)dest + 1; // 正向拷貝
src = (char *)src + 1;
}
}
else
{// 若dst和src 區域交叉,則從尾部開始向起始位置拷貝,這樣可以避免數據衝突
dest = (char *)dest + (n - 1);
src = (char *)src + (n - 1);
while (n--)
{
*(char *)dest = *(char *)src;
dest = (char *)dest - 1; // 反向拷貝
src = (char *)src - 1;
}
}
return rev; // 返回值爲指向dest的指針
}
// linux下memmccpy源碼
// void *memccpy(void *dest, const void *src, int c, size_t n);
// 由src所指內存區域複製不多於n個字節到dest所指內存區域,如果遇到字符ch則停止複製。
// 返回指向dest中值爲c的下一個字節的指針,如果src前n個字節中不存在c則返回NULL。c被複制。
void *memccpy1(void *dest, const void *src, int c, size_t n)
{
assert((src != NULL) && (dest != NULL)); // 判斷參數合法性
while (n)
{
*(char *)dest = *(char *)src;
dest = (char *)dest + 1;
if (*(char *)src == (char)c) // 若發現指定的字符,則停止拷貝
{
break;
}
src = (char *)src + 1;
n--;
}
return (n ? dest : NULL); // 返回指向字符’c’後的第一個字符的指針;
// 否則返回NULL,c被複制
}
// 函數關鍵點:
// (1)以上函數不關心被複制的數據類型(因爲函數的參數類型是void*(未定義類型指針)),
// 只是逐字節地進行復制,這給函數的使用帶來了很大的靈活性,可以面向任何數據類型進行復制。
// (2)dest指針要分配足夠的空間,也即大於等於n字節的空間,
// 如果沒有分配空間,會出現段錯誤。
// (3)若能確定dest和src所指的內存空間不能重疊,則使用memcpy或memccpy,
// 如果發生了重疊,使用memmove會更加安全。
// (4)如果目標destin本身已有數據,執行memcpy後,將覆蓋原有數據(最多覆蓋n)。
// 如果要追加數據,則每次執行memcpy後,要將目標數組地址增加到你要追加數據的地址。
// (5)memccpy增加了指定字符判斷功能,使用時注意添加dest結束標誌‘\0’,
// 但是沒有考慮內存空間重疊問題。
// 備註:
// (1)如果通過長期的嚴格測試,能夠保證使用者不會使用零地址作爲參數調用函數,
// 則希望有簡單的方法關掉參數合法性檢查,可加#define NDEBUG取消assert()。
// (2)size_t是標準C庫中定義的數據類型,32位系統中爲unsigned int,
// 在64位系統中爲long unsigned int,估計stdio.h中有相應的typedef語句
int main(void)
{
char src1[] = "hello world"; // 12字節
char src2[1024] = "hello world";
char src3[1024] = "hello world";
char dest1[sizeof(src1)] = {0};
char dest2[sizeof(src1)] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
char dest3[20] = {0};
char c;
char *rev;
// sizeof(src1)=12.strlen(src1)=11.
printf("sizeof(src1)=%d.strlen(src1)=%d.\n", sizeof(src1), strlen(src1));
#if 0
// 測試memcpy字符串的'/0'問題
memcpy1(dest1, src1, sizeof(src1));
printf("after memcpy dest1 is:%s.\n", dest1); // after memcpy dest1 is:hello world.
memcpy1(dest2, src1, strlen(src1));
printf("after memcpy dest2 is:%s.\n", dest2); // after memcpy dest2 is:hello world.
#endif
#if 0
// 測試memcpy參數合法性檢驗
memcpy1(dest1, NULL, sizeof(src1)); // a.out: memcpy.c:67: memcpy1: Assertion
memcpy1(NULL, src1, sizeof(src1)); // `(src != ((void *)0)) && (dest != ((void *)0))' failed.
#endif // Aborted (core dumped)
#if 0
// 測試memcpy複製任意連續的字節長度(長度小於等於n),複製wor
memcpy1(dest1, (src1 + 6), 3);
printf("after copy dest1 is %s.\n", dest1); // after copy dest1 is wor.
#endif
#if 0
// 測試內存重疊對memcpy和memmove的影響
printf("before memcopy src2 is %s.\n", src2); // before memcopy src2 is hello world.
memcpy1((src2 + 6), src2, strlen(src2));
printf("after memcopy src2 is %s.\n", src2); // after memcopy src2 is hello hello hello.
printf("before memmove dest is %s.\n", src3); // before memmove dest is hello world.
memmove1((src3 + 6), src3, strlen(src3));
printf("after memmove dest is %s.\n", src3); // after memmove dest is hello hello world.
#endif
#if 0
// 測試memcppy找到/沒找到相應字符的情況
c = 'z';
rev = memccpy1(dest3, src2, c, strlen(src2));
if (NULL == rev)
{
printf("can not find character %c.\n", c); // can not find character z.
printf("after memccpy dest3 is %s.\n", dest3); // after memccpy dest3 is hello world.
}
else
{
printf("find character %c success.\n", c); // find character r success.
*rev = '\0';
printf("after memccpy dest3 is %s.\n", dest3); // after memccpy dest3 is hello wor.
}
#endif
return 0;
}
35.sqrt
/*
* 公司:XXXX
* 作者:Rston
* 博客:http://blog.csdn.net/rston
* GitHub:https://github.com/rston
* 項目:靜態鏈接庫和動態鏈接庫
* 功能:演示調用某個數學庫函數。
*/
#include <stdio.h>
#include <math.h>
// 編譯時需加上-lm鏈接命令“gcc 35.sqrt.c -lm”
// 執行“ldd a.out”命令後輸出
// linux-gate.so.1 => (0xb7727000)
// libm.so.6 => /lib/i386-linux-gnu/libm.so.6 (0xb76e7000)
// libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb753d000)
// /lib/ld-linux.so.2 (0xb7728000)
int main(int argc, char **argv)
{
double a = 16.0;
// sqrt(16.000000) = 4.000000.
printf("sqrt(%lf) = %lf.\n", a, sqrt(a));
return 0;
}
35.archive_static/lib_create/
rston.c
/*
* 公司:XXXX
* 作者:Rston
* 博客:http://blog.csdn.net/rston
* GitHub:https://github.com/rston
* 項目:靜態鏈接庫和動態鏈接庫
* 功能:自己製作靜態鏈接庫。
*/
#include <stdio.h>
// 簡單的加法函數
int add(int a, int b)
{
printf("func add in rston.c.\n");
return (a + b);
}
*********
rston.h
#ifndef _RSTON_H_
#define _RSTON_H_
int add(int a, int b);
#endif
*********
Makefile
all:
gcc rston.c -o rston.o -c
ar -rc librston.a rston.o
nm librston.a
35.archive_static/lib_use/
testlib.c
/*
* 公司:XXXX
* 作者:Rston
* 博客:http://blog.csdn.net/rston
* GitHub:https://github.com/rston
* 項目:靜態鏈接庫和動態鏈接庫
* 功能:自己使用靜態鏈接庫。
*/
#include "rston.h"
#include <stdio.h>
int main(int argc, char **argv)
{
printf("add(7, 8) = %d.\n", add(7, 8));
return 0;
}
*********
rston.h
#ifndef _RSTON_H_
#define _RSTON_H_
int add(int a, int b);
#endif
*********
Makefile
all:
gcc testlib.c -o testlib -lrston -L.
nm librston.a
35.shared_dynamic/lib_create/
rston.c
/*
* 公司:XXXX
* 作者:Rston
* 博客:http://blog.csdn.net/rston
* GitHub:https://github.com/rston
* 項目:靜態鏈接庫和動態鏈接庫
* 功能:自己製作動態鏈接庫。
*/
#include <stdio.h>
// 簡單的加法函數
int add(int a, int b)
{
printf("func add in rston.c.\n");
return (a + b);
}
*********
rston.h
#ifndef _RSTON_H_
#define _RSTON_H_
int add(int a, int b);
#endif
*********
Makefile
all:
gcc rston.c -o rston.o -c -FPIC
gcc rston.o -o librston.so -shared
nm librston.so
35.shared_dynamic/lib_use/
testlib.c
/*
* 公司:XXXX
* 作者:Rston
* 博客:http://blog.csdn.net/rston
* GitHub:https://github.com/rston
* 項目:靜態鏈接庫和動態鏈接庫
* 功能:自己使用動態鏈接庫。
*/
#include "rston.h"
#include <stdio.h>
int main(int argc, char **argv)
{
printf("add(7, 8) = %d.\n", add(7, 8));
return 0;
}
*********
rston.h
#ifndef _RSTON_H_
#define _RSTON_H_
int add(int a, int b);
#endif
*********
Makefile
all:
gcc testlib.c -o testlib -lrston -L.
#方法1使用動態鏈接庫
#sudo cp librston.so /usr/lib/
#sudo rm -f /usr/lib/librston.so
#方法2使用動態鏈接庫
#注意Makefile中的export和shell中的export作用不同
#參考鏈接http://blog.csdn.net/sdustliyang/article/details/6959715
#export LD_LIBRARY_PATH=$D_LIBRARY_PATH:$(shell pwd) && ./testlib
#export LD_LIBRARY_PATH= && ./testlib
ldd testlib