C語言程序環(huán)境中的預處理詳解
一、翻譯環(huán)境
整個翻譯環(huán)境大致就可以畫成這樣一張圖。
下列有幾點需要說明:
1. 組成一個程序的每一個源文件通過編譯過程分別轉(zhuǎn)換成目標文件(在Linux中目標文件的后綴為.o;而在Windows中目標文件后綴為.obj)
2. 每個目標文件由鏈接器(linker)捆綁在一起,形成一個單一而完整的可執(zhí)行程序
3. 鏈接器同時也會引入標準C函數(shù)庫(鏈接庫)中任何被該程序所用到的函數(shù),而且它可以搜索程序員個人的程序庫,將其需要的函數(shù)也鏈接到程序中
接下來介紹每一步在Linux系統(tǒng)下整個翻譯環(huán)境的實現(xiàn)方法,以及每一個步驟的作用。
編譯可分為三個部分:
(1)預處理:輸入指令gcc -E test.c -o,就會將test.c文件變?yōu)閠est.i文件。這一步的作用是是對頭文件(#include)的包含、刪除注釋、#define定義符號的替換等文本操作(下文會對預處理這一個步驟展開詳細的介紹)
(2)編譯:輸入指令gcc -S test.i,就會將test.i文件變?yōu)閠est.s文件,這一步主要作用是把C語言代碼轉(zhuǎn)換成匯編代碼,其中包含4步:1. 語法分析;2. 詞法分析;3. 語義分析;4. 符號匯總
(3)匯編:輸入指令gcc -c test.s,就會將test.s文件變?yōu)閠est.o文件,這一步是把匯編代碼轉(zhuǎn)換成二進制的指令,這一步是會形成符號表,此時的符號表為接下來的鏈接操作做出了準備
多個.c文件通過編譯過程后形成.o目標文件,在要執(zhí)行鏈接的時候,輸入指令gcc test.o add.o -o test,就會將.o文件變成可執(zhí)行文件,這其中的操作包括合并段表和符號表的合并和重定位,這一步主要就是將多個目標文件進行連接的時候通過符號表查看來自外部的符號是否真實存在,這樣就完成了整個翻譯環(huán)境的操作。
二、執(zhí)行環(huán)境
對于程序的執(zhí)行過程可分為以下幾個步驟:
1. 程序必須載入內(nèi)存中。在有操作系統(tǒng)的環(huán)境中:一般由操作系統(tǒng)完成。在獨立的環(huán)境中,程序的載入必須由手工安排,也可能是通過可執(zhí)行代碼置入只讀內(nèi)存來完成
2. 程序的執(zhí)行開始。之后就會調(diào)用main函數(shù)
3. 開始執(zhí)行程序代碼。這個時候程序?qū)⑹褂靡粋€運行時堆棧(stack),存儲函數(shù)的局部變量和返回地址;程序同時也可以使用靜態(tài)(static)內(nèi)存,存儲于靜態(tài)內(nèi)存中的變量在程序的整個執(zhí)行過程一直保留他們的值
4. 終止程序。正在終止main函數(shù),也有可能是意外終止的情況
三、預處理
1. 預處理符號
在C語言中,有些預處理符號是語言內(nèi)置的,就比如:
__FILE__ //進行編譯的源文件 __LINE__ //文件當前的行號 __DATE__ //文件被編譯的日期 __TIME__ //文件被編譯的時間 __STDC__ //如果編譯器遵循ANSI C,其值為1,否則未定義
2. #define定義標識符
#define定義的標識符可以是常量、簡化關鍵字、一些符號等,例如:
#define M 10 //定義常量 #define reg register //將關鍵字簡化 #define do_forever for(;;) //用形象的符號來替換一種實現(xiàn) #define CASE break;case //在寫case語句的時候會自動地把break寫上
對于#define定義標識符來說,如果定義的東西過長,還可以分幾行來寫,除最后一行外,其他每行都加上'\',例如:
#define DEBUG_PRINT printf("file:%s\tline:%d\t \ date:%s\ttime:%s\n",\ __FILE__,__LINE__, \ __DATE__,__TIME__)
3. #define定義宏
在#define定義標識符外,#define還有一個規(guī)定,就是允許把參數(shù)替換到文本中,進而就形成了#define定義宏。聲明的方式如下:
#define name(parament-list) stuff
這里的parament-list是由一個逗號隔開的符號表,在實際的代碼中他們也會存在于stuff中。
其中值得注意的是:
1. 參數(shù)列表的左括號必須與name相鄰
2. 如果parament-list與stuff兩者之間有任何空白存在,參數(shù)列表就會被注釋為stuff的一部分
了解了#define定義宏是如何寫后,接下來就是#define定義宏的替換規(guī)則:
1. 在調(diào)用宏的時候,首先對參數(shù)進行檢查,看看是否包含任何由#define定義的符號。如果是,它們首先被替換
2. 替換文本隨后被插入到程序中原來的文本位置,參數(shù)名被它們的值所替換
3. 最后,再次對結(jié)果文件進行掃描,看看它是否包含任何由#define定義的符號。如果是,就重復上述處理過程
所以,總結(jié)以上規(guī)則后得出的結(jié)論就是:如果是#define定義宏用于對數(shù)值表達式進行求值的宏定義都應該加上括號,避免在使用宏時由于參數(shù)中的操作符或者鄰近操作符之間不可預料的相互作用。
當然,對于#define的使用還有幾個注意的點:
1. 宏參數(shù)和#define定義中可以出現(xiàn)其他#define定義的符號,但是對于宏,不能出現(xiàn)遞歸
2. 當預處理器搜索#define定義的符號的時候,字符串常量的內(nèi)容并不被搜索
4. #和##
對于一些想要把參數(shù)插入到字符串中的情況,我們會使用#來把一個宏參數(shù)變成對應的字符串,下面舉個例子:
如果是直接打印出來的話,因為字符串是可以拼接的,所以就如這樣:
#include <stdio.h> int main() { int a = 10; printf("the value of ""a"" is %d\n", a); return 0; }
那么,對于定義宏參數(shù)來說,就應該這樣:
#include <stdio.h> #define PRINT(n) printf("the value of "#n" is %d\n", n) int main() { int a = 10; PRINT(a); return 0; }
這樣字符串中的n才會根據(jù)跟著宏參數(shù)的值變化而變化。
而##的作用是可以把位于它兩邊的符號合成一個符號。它允許宏定義從分離的文本段創(chuàng)建標識符。但是這樣連接必須產(chǎn)生一個合法的標識符,否則會報錯說未定義標識符。
5. 宏和函數(shù)的對比
宏的優(yōu)勢:1. 在執(zhí)行一些小型計算工作的時候,定義宏比調(diào)用函數(shù)和從函數(shù)返回的代碼執(zhí)行所需要的時間會更短;2. 函數(shù)的參數(shù)必須聲明為特定的類型,二宏參數(shù)不用
宏的劣勢:1. 每次使用宏的時候,一份宏定義的代碼將插入到程序中。除非宏比較短,否則可能大幅度增加程序的長度;2. 宏是無法進行調(diào)試的,而函數(shù)可以;3. 宏由于沒有進行類型定義,所以有時候就會不夠嚴謹;4. 宏可能會帶來運算符的優(yōu)先級的問題,導致程序容易出錯
屬性 | #define定義宏 | 函數(shù) |
代碼長度 | 每次使用時,宏代碼都會被插入到程序中,除了非常小的宏以外,程序的長度會大幅度增長 | 函數(shù)代碼只出現(xiàn)于一個地方;每次使用這個函數(shù)時,都調(diào)用那個地方的同一份代碼 |
執(zhí)行速度 | 更快 | 存在函數(shù)的使用和返回的額外開銷,所以相對慢一些 |
操作符優(yōu)先級 | 宏參數(shù)的求值是在所有周圍表達式上下文環(huán)境里,除非加上括號,否則鄰近操作符的優(yōu)先級可能會產(chǎn)生不可預料的后果,所以建議宏在書寫的時候多些括號 | 函數(shù)參數(shù)只在函數(shù)調(diào)用的時候求值一次,它的結(jié)果值傳給函數(shù)。表達式的求值結(jié)果更容易預測 |
帶有副作用的參數(shù) | 參數(shù)可能被替換到宏體中的多個位置,所以帶有副作用的參數(shù)求值可能會產(chǎn)生不可預料的結(jié)果 | 函數(shù)參數(shù)只在傳參的時候求值一次,結(jié)果更容易控制 |
參數(shù)類型 | 宏的參數(shù)與類型無關,只要參數(shù)的操作是合法的,它就可以使用于任何參數(shù)類型 | 函數(shù)的參數(shù)是與類型有關的,如果參數(shù)類型不同,就需要不同的參數(shù),即使他們執(zhí)行的任務的不同的 |
調(diào)試 | 宏是不方便調(diào)試的 | 函數(shù)是可以逐語句調(diào)試的 |
遞歸 | 宏是不能遞歸的 | 函數(shù)是可以遞歸的 |
6. 條件編譯
下面列舉一些編譯指令:
1. #undef 該指令用于移除一個宏定義
2. 該指令是判斷應該執(zhí)行哪一個語句塊
#if 常量表達式 執(zhí)行語塊 #elif 常量表達式 執(zhí)行語塊 #else 執(zhí)行語塊 #endif
3. 該指令是判斷是否被定義
#if define(symbol) 如果有定義,執(zhí)行此語句塊 or #ifdef symbol 如果有定義,執(zhí)行此語句塊 or #if !define(symbol) 如果沒有定義,執(zhí)行此語句塊 or #ifndef symbol 如果沒有定義,執(zhí)行此語句塊
4. 對于條件編譯指令來說,其實還可以對其進行嵌套,稱為嵌套指令
7. 文件包含
我們在一些較大工程進行編譯的時候、在多人合作同一塊項目工程的時候,可能會出現(xiàn)頭文件重復包含的情況,如果真是這樣,則會導致整個代碼運行時的效率大大降低,所以對頭文件避免重復包含就顯得十分重要了。那么,如何避免呢?下面就有一段代碼可以用來避免這種情況:
#ifndef __TEST_H__ #define __TEST_H__ 寫頭文件內(nèi)容 #endif
這段代碼就可以很好地解決了頭文件重復包含的問題,但是實際上,如果是在VS的環(huán)境下進行編譯,會自動在最開始的地方寫上:#pragma once,這句代碼一樣也是可以解決重復包含的問題。
那么,解決完頭文件重復包含的問題后,就來介紹兩種頭文件包含的方式:
1. 用引號包含的頭文件,例如:#include "test.h"
。這種包含方式頭文件的查找策略是先在源文件所在的目錄下查找,如果該頭文件未被找到,編譯器就像查找?guī)旌瘮?shù)頭文件一樣在標準位置查找頭文件,如果還找不到,則會直接報錯。
2. 用尖括號包含頭文件,例如:#include
。這種包含方式則是未有第一步,直接進行第二步。
但是不能說為了保證萬無一失,直接把全部頭文件的包含都用引號進行包含,這樣的話有些時候其實是用尖括號的情況而錯用引號導致程序的執(zhí)行速度下降、效率下降等。
總結(jié)
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關注腳本之家的更多內(nèi)容!
相關文章
用c語言根據(jù)可變參數(shù)合成字符串的實現(xiàn)代碼
本篇文章是對用c語言根據(jù)可變參數(shù)合成字符串的方法進行了詳細的分析介紹,需要的朋友參考下2013-05-05C++實現(xiàn)將數(shù)組中的值反轉(zhuǎn)
這里給大家分享的事一則C++實現(xiàn)將數(shù)組中的值反轉(zhuǎn)的代碼,取材自《C++程序設計》(梁勇著第三版367頁),有需要的小伙伴可以參考下2016-05-05