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