1. 程式人生 > >6、C_宏定義與預處理、函數與函數庫

6、C_宏定義與預處理、函數與函數庫

a10 使用 不可 find 字符串比較 pos cde 文件包含 mnt

C語言預處理理論

由源碼到可執行程序的過程
  • 源碼.c->(編譯)->elf可執行程序
  • 源碼.c->(編譯)->目標文件.o->(鏈接)->elf可執行程序
  • 源碼.c->(編譯)->匯編文件.S->(匯編)->目標文件.o->(鏈接)->elf可執行程序
  • 源碼.c->(預處理)->預處理過的.i源文件->(編譯)->匯編文件.S->(匯編)->目標文件.o->(鏈接)->elf可執行程序
  • 預處理用預處理器,編譯用編譯器,匯編用匯編器,鏈接用鏈接器,這幾個工具再加上其他一些額外的會用到的可用工具,合起來叫編譯工具鏈。gcc就是一個編譯工具鏈。
預處理的意義
  • 編譯器本身的主要目的是編譯源代碼,將C的源代碼轉化成.S的匯編代碼。編譯器聚焦核心功能後,就剝離出了一些非核心的功能到預處理器去了。
  • 預處理器幫編譯器做一些編譯前的雜事。
編程中常見的預處理
  • #include(#include <>和#include ""的區別):#include< > 引用的是編譯器的類庫路徑裏面的頭文件; #include" " 引用的是你程序目錄的相對路徑中的頭文件。
  • 註釋;
  • #if #elif #endif #ifdef
  • 宏定義;
gcc中只預處理不編譯的方法
  • gcc編譯時可以給一些參數來做一些設置,譬如gcc xx.c -o xx可以指定可執行程序的名稱;譬如gcc xx.c -c -o xx.o可以指定只編譯不鏈接,也可以生成.o的目標文件。
  • gcc -E xx.c -o xx.i可以實現只預處理不編譯。一般情況下沒必要只預處理不編譯,但有時候這種技巧可以用來幫助我們研究預處理過程,幫助debug程序。
  • 總結:宏定義被預處理時的現象有:第一,宏定義語句本身不見了(可見編譯器根本就不認識#define,編譯器根本不知道還有個宏定義);第二,typedef重命名語言還在,說明它和宏定義是有本質區別的(說明typedef是由編譯器來處理而不是預處理器處理的);

技術分享圖片

技術分享圖片

技術分享圖片 技術分享圖片 C語言預處理代碼實戰 頭文件包含
  • #include <> 和 #include""的區別:<>專門用來包含系統提供的頭文件(就是系統自帶的,不是程序員自己寫的),""用來包含自己寫的頭文件;更深層次來說:<>的話C語言編譯器只會到系統指定目錄(編譯器中配置的或者操作系統配置的尋找目錄,譬如在ubuntu中是/usr/include目錄,編譯器還允許用-I來附加指定其他的包含路徑)去尋找這個頭文件(隱含意思就是不會找當前目錄下),如果找不到就會提示這個頭文件不存在。
  • ""包含的頭文件,編譯器默認會先在當前目錄下尋找相應的頭文件,如果沒找到然後再到系統指定目錄去尋找,如果還沒找到則提示文件不存在。
  • 總結+註意:規則雖然允許用雙引號來包含系統指定目錄,但是一般的使用原則是:如果是系統指定的自帶的用<>,如果是自己寫的在當前目錄下放著用"",如果是自己寫的但是集中放在了一起專門存放頭文件的目錄下將來在編譯器中用-I參數來尋找,這種情況下用<>。
  • 頭文件包含的真實含義就是:在#include<xx.h>的那一行,將xx.h這個頭文件的內容原地展開替換這一行#include語句。過程在預處理中進行。
註釋
  • 註釋是給人看的,不是給編譯器看的。
  • 編譯器既然不看註釋,那麽編譯時最好沒有註釋的。實際上在預處理階段,預處理器會拿掉程序中所有的註釋語句,到了編譯器編譯階段程序中其實已經沒有註釋了。
條件編譯
  • 有時候我們希望程序有多種配置,我們在源代碼編寫時寫好了各種配置的代碼,然後給個配置開關,在源代碼級別去修改配置開關來讓程序編譯出不同的效果。
  • 條件編譯中用的兩種條件判定方法分別是#ifdef 和 #if
  • 區別:#ifdef XXX判定條件成立與否時主要是看XXX這個符號在本語句之前有沒有被定義,只要定義了這個符號就是成立的(我們可以直接#define XXX或者#define XXX 12或者#define XXX YYY)。
  • 它的格式是:#if (條件表達式),它的判定標準是()中的表達式是否為true還是flase,跟C中的if語句有點像。

技術分享圖片

技術分享圖片

宏定義 宏定義的規則和使用解析
  • 宏定義的解析規則就是:在預處理階段由預處理器進行替換,這個替換是原封不動的替換。
  • 宏定義替換會遞歸進行,直到替換出來的值本身不再是一個宏為止。
  • 一個正確的宏定義式子本身分為3部分:第一部分是#dedine ,第二部分是宏名 ,剩下的所有為第三部分。
  • 宏可以帶參數,稱為帶參宏。帶參宏的使用和帶參函數非常像,但是使用上有一些差異。在定義帶參宏時,每一個參數在宏體中引用時都必須加括號,最後整體再加括號,括號缺一不可。
宏定義示例1:MAX宏,求2個數中較大的一個
  • #defineMax(a,b) (((a)>(b)) ? (a) : (b))
  • 關鍵:
  • 第一點:要想到使用三目運算符來完成。
  • 第二點:註意括號的使用
宏定義示例2:SEC_PER_YEAR,用宏定義表示一年中有多少秒
  • #defineyear (365UL*24*60*60)
  • 關鍵:
  • 第一點:當一個數字直接出現在程序中時,它的是類型默認是int
  • 第二點:一年有多少秒,這個數字不知道是否超過了int類型存儲的範圍
技術分享圖片 技術分享圖片

帶參宏和帶參函數的區別(宏定義的缺陷)
  • 宏定義是在預處理期間處理的,而函數是在編譯期間處理的。這個區別帶來的實質差異是:宏定義最終是在調用宏的地方把宏體原地展開,而函數是在調用函數處跳轉到函數中去執行,執行完後再跳轉回來。
  • 註:宏定義和函數的最大差別就是:宏定義是原地展開,因此沒有調用開銷;而函數是跳轉執行再返回,因此函數有比較大的調用開銷。所以宏定義和函數相比,優勢就是沒有調用開銷,沒有傳參開銷,所以當函數體很短(尤其是只有一句話時)可以用宏定義來替代,這樣效率高。
  • 帶參宏和帶參函數的一個重要差別就是:宏定義不會檢查參數的類型,返回值也不會附帶類型;而函數有明確的參數類型和返回值類型。當我們調用函數時編譯器會幫我們做參數的靜態類型檢查,如果編譯器發現我們實際傳參和參數聲明不同時會報警告或錯誤。
  • 註:用函數的時候程序員不太用操心類型不匹配因為編譯器會檢查,如果不匹配編譯器會叫;用宏的時候程序員必須很註意實際傳參和宏所希望的參數類型一致,否則可能編譯不報錯但是運行有誤。
  • 總結:宏和函數各有千秋,各有優劣。總的來說,如果代碼比較多用函數適合而且不影響效率;但是對於那些只有一兩句話的函數開銷就太大了,適合用帶參宏。但是用帶參宏又有缺點:不檢查參數類型。
內聯函數和inline關鍵字
  • 內聯函數通過在函數定義前加inline關鍵字實現,他既有函數的優點,又有帶參宏的優點。
  • 內聯函數本質上是函數,所以有函數的優點(內聯函數是編譯器負責處理的,編譯器可以幫我們做參數的靜態類型檢查);但是他同時也有帶參宏的優點(不用調用開銷,而是原地展開)。所以幾乎可以這樣認為:內聯函數就是帶了參數靜態類型檢查的宏。
  • 當我們的函數內函數體很短(譬如只有一兩句話)的時候,我們又希望利用編譯器的參數類型檢查來排錯,我還希望沒有調用開銷時,最適合使用內聯函數。

技術分享圖片

技術分享圖片 宏定義來實現條件編譯(#define #undef #ifdef)
  • 程序有DEBUG版本和RELEASE版本,區別就是編譯時有無定義DEBUG宏。

技術分享圖片

技術分享圖片
  • __attribute__ 總結 - 簡書 http://www.jianshu.com/p/29eb7b5c8b2d
函數的本質 C語言為什麽會有函數
  • 整個程序分成多個源文件,一個文件分成多個函數,一個函數分成多個語句,這就是整個程序的組織形式。這樣組織的好處在於:分化問題、便於編寫程序、便於分工。
  • 函數的出現是人(程序員和架構師)的需要,而不是機器(編譯器、CPU)的需要。
  • 函數的目的就是實現模塊化編程。說白了就是為了提供程序的可移植性。
函數書寫的一般原則:
  • 遵循一定的格式。函數的返回類型、函數名、參數列表等。
  • 一個函數只做一件事。函數不能太長也不宜太短,原則上一個函數只做一件事情。
  • 傳參不宜過多。在ARM體系下,傳參不宜超過4個,如果傳參需要多個返回值,則考慮結構體打包。
  • 盡量少碰全局變量。函數最好用傳參返回值和外部交換數據,不要用全局變量(不利於函數移植);
函數是動詞、變量是名詞(面相對象中分別叫方法和成員變量)
  • 函數將來被編譯成可執行代碼段,變量(主要指全局變量)經過編譯後變成數據或者在運行時變成數據。一個程序的運行需要代碼和數據兩方向的結合才能完成。
  • 代碼和數據需要彼此配合,代碼是為了加工數據,數據必須借助代碼來起作用。拿現實中的工廠來比喻:數據是原材料,代碼是加工流水線。名詞性的數據必須經過動詞性的加工才能變成最終我們需要的產出的數據。這個加工的過程就是程序的執行過程。
函數的實質是:數據處理器
  • 程序的主體是數據,也就是說程序運行的主要目標是生成目標數據,我們寫代碼也是為了目標數據。我們如何得到目標數據?必須2個因素:原材料+加工算法。原材料就是程序的輸入數據,加工算法就是程序。
  • 程序的編寫和運行就是為了把原數據加工成目標數據,所以程序的實質就是一個數據處理器。
  • 函數就是程序的一個縮影,函數的參數列表其實就是為了給函數輸入原材料數據,函數的返回值和輸出型參數就是為了向外部輸出目標數據,函數的函數體裏的那些代碼就是加工算法。
  • 函數在靜止沒有執行(乖乖的躺在硬盤裏)的時候就好象一臺沒有開動的機器,此時只占一些存儲空間但是並不占用資源(CPU+內存);函數的每一次運行就好象機器的每一次開機運行,運行時需要耗費資源(CPU+內存),運行時可以對數據加工生成目標數據;函數運行完畢會釋放占用的資源。
  • 整個程序的運行其實就是很多個函數相繼運行的連續過程。
函數的基本使用 函數三要素:定義、聲明、調用
  • 函數的定義就是函數體、函數聲明是函數原型、函數調用就是使用函數
  • 函數定義是函數的根本,函數定義中的函數名表示了這個函數在內存中的首地址,所以可以用函數名來調用執行這個函數(實質是指針解引用訪問);函數定義中的函數體是函數的執行關鍵,函數將來執行時主要就是執行函數體。所以一個函數沒有定義就是無基之塔。
  • 函數聲明的主要作用是告訴編譯器函數的原型
  • 函數調用就是調用執行一個函數。
函數原型和作用
  • 函數原型就是函數的聲明,說白了就是函數的函數名、返回值類型、參數列表。
  • 函數原型的主要作用就是給編譯器提供原型,讓編譯器在編譯程序時幫我們進行參數的靜態類型檢查
  • 必須明白:編譯器在編譯程序時是以單個源文件為單位的(所以一定要在哪裏調用在哪裏聲明),而且編譯器工作時已經經過預處理處理了,最最重要的是編譯器編譯文件時是按照文件中語句的先後順序執行的。
  • 編譯器從源文件的第一行開始編譯,遇到函數聲明時就會收到編譯器的函數聲明表中,然後繼續向後。當遇到一個函數調用時,就在我的本文件的函數聲明表中去查這個函數,看有沒有原型相對應的一個函數(這個相對應的函數有且只能有一個)。如果沒有或者只有部分匹配則會報錯或報警告;如果發現多個則會報錯或報警告(函數重復了,C語言中不允許2個函數原型完全一樣,這個過程其實是在編譯器遇到函數定義時完成的。所以函數可以重復聲明但是不能重復定義)
遞歸函數 什麽是遞歸函數
  • 遞歸函數就是函數中調用了自己本身這個函數的函數。
  • 遞歸函數和循環的區別。遞歸不等於循環
  • 遞歸函數解決問題的典型就是:求階乘、求斐波那契數列
技術分享圖片 技術分享圖片 技術分享圖片

技術分享圖片

的遞歸調用原理
  • 實際上遞歸函數是在棧內存上遞歸執行的,每次遞歸執行一次就需要耗費一些棧內存。
  • 棧內存的大小是限制遞歸深度的重要因素。
使用遞歸函數的原則:收斂性、棧溢出
  • 收斂性就是說:遞歸函數必須有一個終止遞歸的條件。當每次這個函數被執行時,我們判斷一個條件決定是否繼續遞歸,這個條件最終必須能夠被滿足。如果沒有遞歸終止條件或者這個條件永遠不能被滿足,則這個遞歸沒有收斂性,這個遞歸最終要失敗。
  • 因為遞歸是占用棧內存的,每次遞歸調用都會消耗一些棧內存。因此必須在棧內存耗盡之前遞歸收斂(終止),否則就會棧溢出。
  • 遞歸函數的使用是有一定風險的,必須把握好。
函數庫 什麽是函數庫?
  • 函數庫就是一些事先寫好的函數的集合,給別人復用。
  • 函數是模塊化的,因此可以被復用。我們寫好了一個函數,可以被反復使用。也可以A寫好了一個函數然後共享出來,當B有相同的需求時就不需自己寫直接用A寫好的這個函數即可。
函數庫的由來
  • 最開始是沒有函數庫,每個人寫程序都要從零開始自己寫。時間長了慢慢的早期的程序員就積累下來了一些有用的函數。
  • 早期的程序員經常參加行業聚會,在聚會上大家互相交換各自的函數庫。
  • 後來程序員中的一些大神就提出把大家各自的函數庫收攏在一起,然後經過校準和整理,最後形成了一份標準化的函數庫,就是現在的標準的函數庫,譬如說glibc。
函數庫的提供形式:動態鏈接庫與靜態鏈接庫
  • 早期的函數共享都是以源代碼的形式進行的。這種方式共享是最徹底的(後來這種源碼共享的方向就形成了我們現在的開源社區)。但是這種方式有它的缺點,缺點就是無法以商業化形式來發布函數庫。
  • 商業公司需要將自己的有用的函數庫共享給被人(當然是付費的),但是又不能給客戶源代碼。這時候的解決方案就是以庫(主要有2種:靜態庫和動態庫)的形式來提供。
  • 比較早出現的是靜態鏈接庫。靜態庫其實就是商業公司將自己的函數庫源代碼經過只編譯不連接形成.o的目標文件,然後用ar工具將.o文件歸檔成.a的歸檔文件(.a的歸檔文件又叫靜態鏈接庫文件)。商業公司通過發布.a庫文件和.h頭文件來提供靜態庫給客戶使用;客戶拿到.a和.h文件後,通過.h頭文件得知庫中的庫函數的原型,然後在自己的.c文件中直接調用這些庫文件,在連接的時候鏈接器會去.a文件中拿出被調用的那個函數的編譯後的.o二進制代碼段鏈接進去形成最終的可執行程序。
  • 動態鏈接庫比靜態鏈接庫出現的晚一些,效率更高一些,是改進型的。現在我們一般都是使用動態庫(GCC默認使用的是動態庫)。靜態庫在用戶鏈接自己的可執行程序時就已經把調用的庫中的函數的代碼段鏈接進最終可執行程序中了,這樣好處是可以執行,壞處是太占地方了。尤其是有多個應用程序都使用了這個庫函數時,實際上在多個應用程序最後生成的可執行程序中都各自有一份這個庫函數的代碼段。當這些應用程序同時在內存中運行時,實際上在內存中有多個這個庫函數的代碼段,這完全重復了。而動態鏈接庫本身不將庫函數的代碼段鏈接入可執行程序,只是做個標記。然後當應用程序在內存中執行時,運行時環境發現它調用了一個動態庫中的庫函數時,會去加載這個動態庫到內存中,然後以後不管有多少個應用程序去調用這個庫中的函數都會跳轉到第一次加載的地方去執行(不會重復加載)。
函數庫中庫函數的使用
  • gcc中編譯鏈接程序默認是使用動態庫的,要想靜態鏈接需要顯式用-static來強制靜態鏈接。
  • 庫函數的使用需要註意3點:
  • 第一,包含相應的頭文件;
  • 第二,調用庫函數時註意函數原型;
  • 第三,有些庫函數鏈接時需要額外用-lxxx來指定鏈接;
  • 第四,如果是動態庫,要註意-L指定動態庫的地址。
字符串函數 什麽是字符串
  • 字符串就是由多個字符在內存中連續分布組成的字符結構。字符串的特點是指定了開頭(字符串的指針)和結尾(結尾固定為字符‘\0‘),而沒有指定長度(長度由開頭地址和結尾地址相減得到)
為什麽要講字符串處理函數
  • 函數庫為什麽要包含字符串處理函數?因為字符串處理的需求是客觀的,所以從很早開始人們就在寫很多關於字符串處理的函數,然後逐漸形成了現在的字符串處理函數庫。
  • 面試筆試時,常用字符串處理函數也是經常考到的點。
常用字符串處理函數
  • C庫中字符串處理函數包含在string.h中,這個文件在ubuntu系統中在/usr/include中
  • 常見字符串處理函數及作用:
memcpy
  • 原型:void *memcpy(void *dest, const void *src, size_t n);
  • 功能:由src指向地址為起始地址的連續n個字節的數據復制到以destin指向地址為起始地址的空間內。
  • 說明:http://blog.csdn.net/goodwillyang/article/details/45559925
  • 例子:char a[100], b[50]; memcpy(b, a,sizeof(b));
memmove
  • 原型:void *memmove(void *dest, const void *src, size_t n);
  • 功能:由src指向地址為起始地址的連續n個字節的數據復制到以destin指向地址為起始地址的空間內。memcpy在內存沒有重復的情況下能夠正確復制,若有重疊情況則復制結果錯誤,但是它的效率比memmove高。所以,在確定沒有重復的情況下用memcpy,在不確定是否有內存重復的情況用memmove。
  • 說明:memcpy和memmove的區別:http://blog.csdn.net/z702143700/article/details/47107701
  • http://blog.chinaunix.net/uid-26495963-id-3080058.html
  • 例子:char* p1 = s; char* p2 = s+2; memcpy(p2, p1, 5)
memset
  • 原型:void *memset(void *s, int c, size_t n);
  • 功能:將一段內存空間全部初始化為某個字符,一般用在對定義的字符串進行初始化為‘ ’或‘/0’
  • 說明:http://blog.sina.com.cn/s/blog_715d0ae30100yj2d.html
  • 例子:char a[100]; memset(a, ‘/0‘, sizeof(a));
  • struct sample_strcut stTest; memset(&stTest,0,sizeof(struct sample_struct));
memcmp
  • 原型:int memcmp(const void *s1, const void *s2, size_t n);
  • 功能:比較buf1和buf2的前n個字節;s1 > s2 返回 >0 的值; s1 = s2 返回 =0 的值; s1 < s2 返回 <0 的值
  • 說明:http://blog.csdn.net/riyadh_linux/article/details/50211403
  • 例子:char a[] = "nihao jingliming"; char b[] = "nihao xiaoming";
  • int r=memcmp(a,b,strlen(a)); if(r>0){}
memchr
  • 原型:void *memchr(const void *s, int c, size_t n);
  • 功能:從s所指內存區的前n個字節查找字符c,當第一次遇到字符c時停止查找。如果成功,返回指向字符c的指針;否則返回null
  • 說明:http://www.cnblogs.com/jingliming/p/4737409.html
  • 例子:char a[] = "nihao jingliming"; void *p;
  • p = memchr(a,‘j‘,sizeof(a));if (p){}
strcpy
  • 原型:char *strcpy(char *dest, const char *src);
  • 功能:strcpy把從src地址開始且含有‘\0‘結束符的字符串復制到以dest開始的地址空間,返回值的類型為char*。
  • 說明:http://blog.csdn.net/callinglove/article/details/8597189
  • 例子:chara[20],*p1="linux";my_strcpy(a,p1);puts(a);
  • 實現:
技術分享圖片 strncpy
  • 原型:char *strncpy(char *dest, const char *src, size_t n);
  • 功能:把src所指向的字符串中以src地址開始的前n個字節復制到dest所指的數組中,並返回dest。
  • 說明:http://blog.csdn.net/lanzhihui_10086/article/details/39827729
  • http://blog.csdn.net/goodwillyang/article/details/45559925
  • 例子:char name[]={"Chinanet"}; dest[20]={}; strncpy(dest,name,3);
  • 實現:
strcat
  • 原型:char *strcat(char *dest, const char *src);
  • 功能:把src所指由NULL結束的字符串復制到dest所指的數組中。
  • 說明:http://blog.csdn.net/lonfee88/article/details/6402274
  • 例子:chara[20]="mark";charb[]="yyyyy";char*cat=my_strcat(a,b); printf("%s%d\n",cat,strlen(cat));
  • 實現:
技術分享圖片 strncat
  • 原型:char *strncat(char *dest, const char *src, size_t n);
  • 功能:把src所指字符串的前n個字符添加到dest結尾處,覆蓋dest結尾處的‘/0‘,實現字符串連接。
  • 說明:http://blog.csdn.net/sky2098/article/details/1530662
  • 例子:int n=15; char *strtemp;
  • strtemp=strncat(str1,str2,n); //將字符串str2中的前n個字符連接到str1的後面
strcmp
  • 原型:int strcmp(const char *s1, const char *s2);
  • 功能:strcmp函數實際上是對字符的ASCII碼進行比較,實現原理如下:首先比較兩個串的第一個字符,若不相等,則停止比較並得出兩個ASCII碼大小比較的結果;逐字符比較字符串s1(source)和字符串s2(dest);s1 > s2 返回 >0 的值; s1 = s2 返回 =0 的值; s1 < s2 返回 <0 的值
  • 說明:http://www.cnblogs.com/happyhorseji/p/4244068.html
  • 例子:char a = "linux"; char b = "Nlinux"; int r; r = srtcmp(a, b); if(r >0){}
strncmp
  • 原型:int strncmp(const char *s1, const char *s2, size_t n);
  • 功能:字符串比較函數,逐字符比較字符串s1(source)和字符串s2(dest)的前n個字符,n為0時,返回0;
  • 說明:http://blog.csdn.net/riyadh_linux/article/details/50021381
  • 例子:char *a = "linux"; char *b = "Nlinux"; int r;
  • r = srtcmp(a, b, 3); if(r == 0){}
strdup
  • 原型:char *strdup(const char *s);
  • 功能:strdup()函數計算出字符串的長度,然後調用malloc()函數在堆上分配相應的空間,然後strdup()函數把所有字符復制到堆上的新空間(使用之後需要使用free釋放內存,避免內存泄漏)。stdrup可以直接把要復制的內容復制給沒有初始化的指針,因為它會自動分配空間給目的指針。
  • 說明:http://blog.csdn.net/leichelle/article/details/7465769
  • 例子:char *dup_str, *string = "abcde"; dup_str =strdup(string);
strndup
  • 原型:char *strndup(const char *s, size_t n);
  • 功能:計算字符串S的長度,如果S大於n,則只復制前n個字符;原理同strdup相同;
  • 說明:http://blog.csdn.net/yaoliang11/article/details/4163982
  • 例子:char *dup_str, *string = "abcde"; dup_str =strndup(string,3);
strchr
  • 原型:char *strchr(const char *s, int c);
  • 功能:查找字符串s中首次出現字符c的位置,成功則返回要查找字符第一次出現的位置,失敗返回NULL。
  • 說明:http://blog.csdn.net/lanzhihui_10086/article/details/39831935
  • 例子:char string[] = "This is a string"; char *ptr; char c=‘h‘; ptr=strchr(string,c); puts(ptr);
strstr
  • 原型:char *strstr(const char *haystack, const char *needle);
  • 功能:搜索一個字符串 *needle 在 *haystack 字符串中的第一次出現。找到所搜索的字符串,則該函數返回第一次匹配的字符串的地址;如果未找到所搜索的字符串,則返回NULL。
  • 說明:http://www.cnblogs.com/balingybj/p/4783684.html
  • 例子:char *str1 = "Borland International", *str2 = "a", *ptr; ptr = strstr(str1, str2); puts(ptr);
strtok
  • 原型:char *strtok(char *str, const char *delim);
  • 功能:該函數用來將字符串分割成一個個片段。
  • 說明:http://blog.csdn.net/mormont/article/details/53677363
  • 例子:char str[]="ab,cd,ef"; char *ptr2; ptr2 = strtok(str, ","); while(ptr2 != NULL)
  • {printf("str=%s\n",str); printf("ptr=%s\n",ptr2); ptr2 = strtok(NULL, ",");}
  • 實現:
數學庫函數 math.h
  • 真正的數學運算的函數定義在:/usr/include/i386-linux-gnu/bits/mathcalls.h
  • 使用數學庫函數的時候,只需要包含math.h即可。
計算開平方
  • 庫函數: double sqrt(double x);
  • 註意區分編譯時警告/錯誤,和鏈接時的錯誤:
編譯時警告/錯誤:
  • math.c:9:13: warning: incompatible implicit declaration of builtin function ‘sqrt’ [enabled by default]
  • double b = sqrt(a);
鏈接時錯誤:
  • math.c:(.text+0x1b): undefined reference to `sqrt‘
  • collect2: error: ld returned 1 exit status
分析;
  • 這個鏈接錯誤的意思是:sqrt函數有聲明(聲明就在math.h中)有引用(在math.c)但是沒有定義,鏈接器找不到函數體。sqrt本來是庫函數,在編譯器庫中是有.a和.so鏈接庫的(函數體在鏈接庫中的)。
  • C鏈接器的工作特點:因為庫函數有很多,鏈接器去庫函數目錄搜索的時間比較久。為了提升速度想了一個折中的方案:鏈接器只是默認的尋找幾個最常用的庫,如果是一些不常用的庫中的函數被調用,需要程序員在鏈接時明確給出要擴展查找的庫的名字。鏈接時可以用-lxxx來指示鏈接器去到libxxx.so中去查找這個函數。
鏈接時加-lm
  • -lm就是告訴鏈接器到libm中去查找用到的函數。
  • 實戰中發現在高版本的gcc中,經常會出現沒加-lm也可以編譯鏈接的。
  • 使用ldd命令可以知道該程序鏈接了哪些庫:技術分享圖片
技術分享圖片

自己制作靜態鏈接庫並使用 第一步:自己制作靜態鏈接庫
  • 首先使用gcc -c只編譯不連接,生成.o文件;然後使用ar工具進行打包成.a歸檔文件
  • 庫名不能隨便亂起,一般是lib+庫名稱,後綴名是.a表示是一個歸檔文件
  • 註意:制作出來了靜態庫之後,發布時需要發布.a文件和.h文件。
技術分享圖片 技術分享圖片

第二步:使用靜態鏈接庫
  • 把.a和.h都放在引用的文件夾下,然後在.c文件中包含庫的.h,然後直接使用庫函數。
  • 第一次,編譯方法:gcc test.c -o test
  • 報錯信息:test.c:(.text+0xa): undefined reference to `func1‘
  • test.c:(.text+0x1e): undefined reference to `func2‘
  • 第二次,編譯方法:gcc test.c -o test -laston
  • 報錯信息:/usr/bin/ld: cannot find -laston
  • collect2: error: ld returned 1 exit status
  • 第三次,編譯方法:gcc test.c -o test -laston -L.(L表示路徑,. 表示當前路徑)
  • 無報錯,生成test,執行正確。

技術分享圖片

技術分享圖片

  • 除了ar名另外,還有個 nm 命令也很有用,nm可以用來查看一個.a文件中都有哪些符號(函數符號)
技術分享圖片 技術分享圖片

自己制作動態鏈接庫並使用
  • 動態鏈接庫的後綴名是.so(對應windows系統中的dll),靜態庫的擴展名是.a
創建一個動態鏈接庫。
  • gcc aston.c -o aston.o -c -fPIC
  • gcc -o libaston.so aston.o -shared
  • -fPIC是位置無關碼,-shared是按照共享庫的方式來鏈接。
  • 註意:做庫的人給用庫的人發布庫時,發布libxxx.so和xxx.h即可。
技術分享圖片 技術分享圖片

使用自己創建的共享庫。
  • 第一步,編譯方法:gcc test.c -o test
  • 報錯信息:test.c:(.text+0xa): undefined reference to `func1‘
  • test.c:(.text+0x1e): undefined reference to `func2‘
  • collect2: error: ld returned 1 exit status
  • 第二步,編譯方法:gcc test.c -o test -laston
  • 報錯信息:/usr/bin/ld: cannot find -laston
  • collect2: error: ld returned 1 exit status
  • 第三步,編譯方法:gcc test.c -o test -laston -L.
  • 編譯成功
  • 但是運行出錯,報錯信息:
  • error while loading shared libraries: libaston.so: cannot open shared object file: No such file or directory

技術分享圖片

技術分享圖片
  • 錯誤原因:動態鏈接庫運行時需要被加載(運行時環境在執行test程序的時候發現他動態鏈接了libaston.so,於是乎會去固定目錄嘗試加載libaston.so,如果加載失敗則會打印以上錯誤信息。)
解決方法一:
  • 將libaston.so放到固定目錄下就可以了,這個固定目錄一般是/usr/lib目錄。
  • cp libaston.so /usr/lib即可
解決方法二:
  • 使用環境變量LD_LIBRARY_PATH。操作系統在加載固定目錄/usr/lib之前,會先去LD_LIBRARY_PATH這個環境變量所指定的目錄下去尋找,如果找到就不用去/usr/lib下面找了,如果沒找到再去/usr/lib下面找。所以解決方案就是將libaston.so所在的目錄導出到環境變量LD_LIBRARY_PATH中即可。
  • export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/mnt/hgfs/winshare/courseware_code/c_test/4.6/4.6.12/testlib

技術分享圖片

技術分享圖片

  • 在ubuntu中還有個解決方案三,用ldconfig
ldd命令:
  • 作用是可以在一個使用了共享庫的程序執行之前解析出這個程序使用了哪些共享庫,並且查看這些共享庫是否能被找到,能被解析(決定這個程序是否能正確執行)。技術分享圖片
技術分享圖片

技術分享圖片

6、C_宏定義與預處理、函數與函數庫