宏定义与预处理、函数和函数库
以下內容源于朱有鵬嵌入式課程的學習,如有侵權,請告知刪除。
一、C語言預處理理論
1、由源碼到可執行程序的過程
- 源碼.c->(編譯)->elf可執行程序
- 源碼.c->(編譯)->目標文件.o->(鏈接)->elf可執行程序
- 源碼.c->(編譯)->匯編文件.S->(匯編)->目標文件.o->(鏈接)->elf可執行程序
- 源碼.c->(預處理)->預處理過的.i源文件->(編譯)->匯編文件.S->(匯編)->目標文件.o->(鏈接)->elf可執行程序
- 預處理用預處理器,編譯用編譯器,匯編用匯編器,鏈接用鏈接器,這幾個工具再加上其他一些額外的會用到的可用工具,合起來叫編譯工具鏈。
- gcc就是一個編譯工具鏈。
2、預處理的意義
(1)編譯器本身的主要目的是編譯源代碼,將C的源代碼轉化成.S的匯編代碼。編譯器聚焦核心功能后,就剝離出了一些非核心的功能到預處理器去。
(2)預處理器幫編譯器做一些編譯前的雜事。
3、編程中常見的預處理
(1)頭文件包含:#include(#include <>和#include ""的區別);
(2)注釋;
(3)條件編譯:#if #elif #endif, #ifdef
(4)宏定義
4、gcc中只預處理不編譯的方法
- gcc編譯時可以給一些參數來做一些設置。
- 譬如gcc xx.c -o xx可以指定可執行程序的名稱;
- 譬如gcc xx.c -c -o xx.o可以指定只編譯不連接,也可以生成.o的目標文件。
- 譬如gcc -E xx.c -o xx.i可以實現只預處理不編譯。一般情況下沒必要只預處理不編譯,但有時候這種技巧可以用來幫助我們研究預處理過程,幫助debug程序。
5、總結
- 宏定義被預處理時的現象有:第一,宏定義語句本身不見了(可見編譯器根本就不認識#define,編譯器根本不知道還有個宏定義);第二,typedef重命名語言還在,說明它和宏定義是有本質區別的(說明typedef是由編譯器來處理而不是預處理器處理的);
二、C語言預處理代碼實戰
1、頭文件包含
(1)#include <> 和 #include""的區別
- <>專門用來包含系統提供的頭文件(就是系統自帶的,不是程序員自己寫的),""用來包含自己寫的頭文件;
- 更深層次來說:<>的話C語言編譯器只會到系統指定目錄(編譯器中配置的或者操作系統配置的尋找目錄,譬如在ubuntu中是/usr/include目錄,編譯器還允許用-I來附加指定其他的包含路徑)去尋找這個頭文件(隱含意思就是不會找當前目錄下),如果找不到就會提示這個頭文件不存在。
- ""包含的頭文件,編譯器默認會先在當前目錄下尋找相應的頭文件,如果沒找到然后再到系統指定目錄去尋找,如果還沒找到則提示文件不存在。
- 規則雖然允許用雙引號來包含系統指定目錄,但是一般的使用原則是:如果是系統指定的自帶的用<>,如果是自己寫的在當前目錄下放著用"",如果是自己寫的但是集中放在了一起專門存放頭文件的目錄下將來在編譯器中用-I參數來尋找,這種情況下用<>。
(2)頭文件包含的真實含義就是:在#include<xx.h>的那一行,將xx.h這個頭文件的內容原地展開替換這一行#include語句。過程在預處理中進行。
2、注釋
- 注釋是給人看的,不是給編譯器看的。
- 在預處理階段,預處理器會拿掉程序中所有的注釋語句,到了編譯器編譯階段程序中其實已經沒有注釋了。
3、條件編譯
- 條件編譯中用的兩種條件判定方法分別是#ifdef 和 #if
三、宏定義
1、宏定義的規則和使用解析
(1)宏定義的解析規則:在預處理階段由預處理器進行替換,這個替換是原封不動的替換。
(2)宏可以帶參數,稱為帶參宏。
- 在定義帶參宏時,每一個參數在宏體中引用時都必須加括號,最后整體再加括號,括號缺一不可。
2、宏定義示例1:MAX宏
- #define MAX(a, b) (((a)>(b)) ? (a) : (b))
(1)要想到使用三目運算符來完成。
(2)注意括號的使用
3、宏定義示例2:SEC_PER_YEAR
- #define SEC_PER_YEAR (365*24*60*60UL)
(1)當一個數字直接出現在程序中時,它的是類型默認是int;
(2)一年有多少秒,這個數字剛好超過了int類型存儲的范圍.
4、帶參宏和帶參函數的區別(宏定義的缺陷)
(1)宏定義是在預處理期間處理的,而函數是在編譯期間處理的。
- 這個區別帶來的實質差異是:宏定義最終是在調用宏的地方把宏體原地展開,而函數是在調用函數處跳轉到函數中去執行,執行完后再跳轉回來。
- 宏定義是原地展開,因此沒有調用開銷;而函數是跳轉執行再返回,因此函數有比較大的調用開銷。所以宏定義和函數相比,優勢就是沒有調用開銷,沒有傳參開銷,所以當函數體很短(尤其是只有一句話時)可以用宏定義來替代,這樣效率高。
(2)帶參宏和帶參函數的一個重要差別
- 宏定義不會檢查參數的類型,返回值也不會附帶類型。用宏的時候程序員必須很注意實際傳參和宏所希望的參數類型一致,否則可能編譯不報錯但是運行有誤。
- 函數有明確的參數類型和返回值類型。當我們調用函數時編譯器會幫我們做參數的靜態類型檢查,如果編譯器發現我們實際傳參和參數聲明不同時會報警告或錯誤。用函數的時候程序員不太用操心類型不匹配因為編譯器會檢查,如果不匹配編譯器會叫。
- 宏和函數各有千秋,各有優劣??偟膩碚f,如果代碼比較多用函數適合而且不影響效率;但是對于那些只有一兩句話的函數開銷就太大了,適合用帶參宏。但是用帶參宏又有缺點:不檢查參數類型。
5、內聯函數和inline關鍵字
(1)內聯函數通過在函數定義前加inline關鍵字實現。
(2)“內聯函數就是帶了參數靜態類型檢查的宏?!?/p>
- 內聯函數本質上是函數,所以有函數的優點(內聯函數是編譯器負責處理的,編譯器可以幫我們做參數的靜態類型檢查);
- 但是同時也有帶參宏的優點(不用調用開銷,而是原地展開)。
- 當我們的函數內函數體很短(譬如只有一兩句話)的時候,希望利用編譯器的參數類型檢查來排錯,又希望沒有調用開銷時,最適合使用內聯函數。
6、宏定義來實現條件編譯(#define #undef #ifdef)
- 程序有DEBUG版本和RELEASE版本,區別就是編譯時有無定義DEBUG宏。
三、函數庫
1、什么是函數庫?
- 函數庫就是一些事先寫好的函數的集合,給別人復用。
2、函數庫的提供形式:動態鏈接庫與靜態鏈接庫
(1)早期的函數共享都是以源代碼的形式進行的。
- 這種方式共享是最徹底的(后來這種源碼共享的方向就形成了我們現在的開源社區)。
- 缺點是無法以商業化形式來發布函數庫。
(2)以庫(主要有2種:靜態庫和動態庫)的形式來提供。
- 比較早出現的是靜態鏈接庫。靜態庫其實就是商業公司將自己的函數庫源代碼經過只編譯不連接形成.o的目標文件,然后用ar工具將.o文件歸檔成.a的歸檔文件(.a的歸檔文件又叫靜態鏈接庫文件)。商業公司通過發布.a庫文件和.h頭文件來提供靜態庫給客戶使用;客戶拿到.a和.h文件后,通過.h頭文件得知庫中的庫函數的原型,然后在自己的.c文件中直接調用這些庫文件,在連接的時候鏈接器會去.a文件中拿出被調用的那個函數的編譯后的.o二進制代碼段鏈接進去形成最終的可執行程序。
- 動態鏈接庫比靜態鏈接庫出現的晚一些,效率更高一些,是改進型的?,F在我們一般都是使用動態庫。靜態庫在用戶鏈接自己的可執行程序時就已經把調用的庫中的函數的代碼段鏈接進最終可執行程序中了,這樣好處是可以執行,壞處是太占地方了。尤其是有多個應用程序都使用了這個庫函數時,實際上在多個應用程序最后生成的可執行程序中都各自有一份這個庫函數的代碼段。當這些應用程序同時在內存中運行時,實際上在內存中有多個這個庫函數的代碼段,這完全重復了。而動態鏈接庫本身不將庫函數的代碼段鏈接入可執行程序,只是做個標記。然后當應用程序在內存中執行時,運行時環境發現它調用了一個動態庫中的庫函數時,會去加載這個動態庫到內存中,然后以后不管有多少個應用程序去調用這個庫中的函數都會跳轉到第一次加載的地方去執行(不會重復加載)。
3、函數庫中庫函數的使用
(1)gcc中編譯鏈接程序默認是使用動態庫的,要想靜態鏈接需要顯式用-static來強制靜態鏈接。
(2)庫函數的使用需要注意4點。
- 第一,包含相應的頭文件;
- 第二,調用庫函數時注意函數原型;
- 第三,有些庫函數鏈接時需要額外用-lxxx來指定鏈接;
- 第四,如果是動態庫,要注意-L指定動態庫的地址。
4、字符串函數
(1)C庫中字符串處理函數包含在string.h中,這個文件在ubuntu系統中在/usr/include中。
(2)常見字符串處理函數及作用:
memcpy 確定src和dst不會overlap,則使用memcpy效率高memmove 確定會overlap或者不確定但是有可能overlap,則使用memove比較保險
memset
memcmp
memchr
strcpy
strncpy
strcat
strncat
strcmp
strncmp
strdup
strndup
strchr
strstr
strtok
5、數學庫函數(math.h)
- 真正的數學運算的函數定義在:/usr/include/i386-linux-gnu/bits/mathcalls.h;
- 使用數學庫函數的時候,只需要包含math.h即可。
6、C鏈接器的工作特點
- 因為庫函數有很多,鏈接器去庫函數目錄搜索的時間比較久。為了提升速度想了一個折中的方案:鏈接器只是默認的尋找幾個最常用的庫,如果是一些不常用的庫中的函數被調用,需要程序員在鏈接時明確給出要擴展查找的庫的名字。鏈接時可以用-lxxx來指示鏈接器去到libxxx.so中去查找這個函數。
- 鏈接時加-lm,就是告訴鏈接器到libm中去查找用到的函數。
五、自己制作靜態鏈接庫并使用
(1)第一步:自己制作靜態鏈接庫首先使用gcc -c只編譯不連接,生成.o文件;然后使用ar工具進行打包成.a歸檔文件
庫名不能隨便亂起,一般是lib+庫名稱,后綴名是.a表示是一個歸檔文件
注意:制作出來了靜態庫之后,發布時需要發布.a文件和.h文件。
(2)第二步:使用靜態鏈接庫
把.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.
無報錯,生成test,執行正確。
除了ar名另外,還有個nm命令也很有用,它可以用來查看一個.a文件中都有哪些符號
六、自己制作動態鏈接庫并使用
(1)動態鏈接庫的后綴名是.so(對應windows系統中的dll),靜態庫的擴展名是.a
(2)第一步:創建一個動態鏈接庫。
gcc aston.c -o aston.o -c -fPICgcc -o libaston.so aston.o -shared?
-fPIC是位置無關碼,-shared是按照共享庫的方式來鏈接。
注意:做庫的人給用庫的人發布庫時,發布libxxx.so和xxx.h即可。
(3)第二步:使用自己創建的共享庫。
第一步,編譯方法: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/s5pv210/AdvancedC/4.6.PreprocessFunction/4.6.12.sharedobject.c/sotest
解決方案三:
ubuntu中用ldconfig
(4)ldd命令
- 作用是可以在一個使用了共享庫的程序執行之前解析出這個程序使用了哪些共享庫,并且查看這些共享庫是否能被找到,能被解析(決定這個程序是否能正確執行)。
總結
以上是生活随笔為你收集整理的宏定义与预处理、函数和函数库的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java数据库调用
- 下一篇: 字符串类型、结构体、共用体、枚举、con