C語言中的程序環(huán)境與預(yù)處理詳情
1.程序的翻譯環(huán)境和執(zhí)行環(huán)境
在ANSI C的任何一種實現(xiàn)中,存在兩個不同的環(huán)境
- 第1種是翻譯環(huán)境,在這個環(huán)境中源代碼被轉(zhuǎn)換為可執(zhí)行的機器指令,也就是從,c文件到.exe文件;
- 第2種是執(zhí)行環(huán)境,它用于實際執(zhí)行代碼;
翻譯環(huán)境是由編譯器提供的,而執(zhí)行環(huán)境是由操作系統(tǒng)提供的。
如MSVC,DEV C++,Codeblocks這些編譯軟件都是集成開發(fā)環(huán)境,也就是集成了編輯,編譯,鏈接和調(diào)試等功能。
2.詳解編譯和鏈接
2.1程序翻譯環(huán)境下的編譯和鏈接
從源文件到可執(zhí)行程序可以分為編譯和鏈接兩步,在編譯階段源文件變成了目標文件,在鏈接階段目標文件變成了可執(zhí)行程序。
組成程序的每個源文件通過編譯過程分別轉(zhuǎn)化成目標文件;
每個目標文件由鏈接器捆綁在一起,形成一個單一而完整的可執(zhí)行程序;
鏈接器同時也會引入標準C函數(shù)庫中任何被該程序所用到的函數(shù),而且鏈接器也可以搜索程序員個人的程序庫,將其需要的函數(shù)也鏈接到程序中。
圖解:
2.2深入編譯和鏈接過程
編譯本身可以分為預(yù)編譯(預(yù)處理),編譯和匯編。
預(yù)編譯:在預(yù)編譯階段會將#include引用的頭文件給輸入到文件里面,進行#define定義的標識符的替換,以及將注釋給刪除,因為注釋是給程序員看的,不是給電腦看的;
編譯:在這個過程中會將C語言代碼翻譯成匯編代碼,編譯器會對代碼進行詞法分析,語法分析,語義分析,符號匯總;
匯編:會把在編譯階段形成的匯編代碼翻譯成二進制的指令,并將匯總的符號形成一個符號表;
在編譯完成之后,就會開始鏈接,鏈接過程會合成段表,也就是將目標文件捆綁在一起,以及將符號表合并并進行重定位,最后生成可執(zhí)行程序。
2.3運行環(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ū)⑹褂靡粋€運行時堆棧,存儲函數(shù)的局部變量和返回地址,程序同時也可以使用靜態(tài)內(nèi)存,存儲于靜態(tài)內(nèi)存中的變量在程序的整個執(zhí)行過程一種保留它們的值。
- 4.終止程序。正常終止main函數(shù),也有可能是意外終止。
3.預(yù)處理詳解
3.1預(yù)定義符號
預(yù)定義符號都是語言內(nèi)置的
__FILE__ //進行編譯的源文件
__LINE__ //當前代碼的行號
__DATE__ //文件被編譯時的日期
__TIME__ //文件被編譯時的時間
__STDC__ //如果編譯器遵循ANSI C,其值為1,否則未定義
預(yù)定義符號的使用:
int main() { printf("file:%s\nline:%d\ndata:%s\ntime:%s\n", __FILE__, __LINE__, __DATE__, __TIME__); return 0; }
3.2#define
3.2.1#define定義的標識符
#define name stuff
舉例:
#define MAX 1000 #define reg register ?????//為 register這個關(guān)鍵字,創(chuàng)建一個簡短的名字 #define do_forever for(;;) ??//用更形象的符號來替換一種實現(xiàn) #define CASE break;case ????//在寫case語句的時候自動把 break寫上。 // 如果定義的 stuff過長,可以分成幾行寫,除了最后一行外,每行的后面都加一個反斜杠(續(xù)行符)。 #define DEBUG_PRINT printf("file:%s\tline:%d\t \ ?????????????date:%s\ttime:%s\n" ,\ ?????????????__FILE__,__LINE__ , ???\ ?????????????__DATE__,__TIME__ )
在define定義標識符的時候,不要在最后加上;
如下面這種情況,會出現(xiàn)語法錯誤
#define NUM 100; int main() { int a = 0; if (1) a = NUM; else a = 0; return 0; }
3.2.2#define定義宏
#define機制包括了一個規(guī)定,允許把參數(shù)替換到文本中,這種實現(xiàn)通常稱為宏或宏定義
宏的聲明方式如下:
#define name(parament-list) stuff
其中的parament-list是一個由逗號隔開的符號表,它們可能出現(xiàn)在stuff中
把name(parament-list)這個整體稱為宏
注意:
參數(shù)列表的左括號必須與name緊貼,如果兩者之間存在空格,參數(shù)列表就會被解釋為stuff的一部分,語法就是這么規(guī)定的。
接下來是宏的使用:
比如用宏實現(xiàn)一個數(shù)的平方:
#define SQUARE(n) n * n int main() { SQUARE(6); return 0; }
語句SQUARE(6)就會替換成6 * 6;
解釋:宏先是接受一個參數(shù),SQRARE(n)中的n就變成了6,其后宏的內(nèi)容也就由n * n變成了6 * 6,再將6 * 6替換到程序中使用宏的位置。
但是,這個宏這么寫存在一個問題,如下代碼:
#define SQUARE(n) n * n int main() { printf("%d\n", SQUARE(1 + 3)); return 0; }
看上去似乎最后的結(jié)果是16,然而實際上參數(shù)n會被替換成1 + 3,這樣最終替換的內(nèi)容是1 + 3 * 1 + 3,這條表達式最終的結(jié)果是7.
所以需要在n的左右兩邊加上一對括號,如下:
#define SQUARE(n) (n) * (n)
再看另外一個宏定義:
#define DOUBLE(n) (n) + (n)
代碼:
#define DOUBLE(n) (n) + (n) int main() { printf("%d\n", 5 * DOUBLE(3)); return 0; }
看上去最終結(jié)果似乎是30,然而替換后語句實際上是
printf("%d\n", 5 * (3) + (3));
所以最終結(jié)果是18
所以為了保證獲得想要的結(jié)果,宏定義表達式兩邊還需要加上一對括號
#define DOUBLE ((n) + (n))
所以用于對數(shù)值表達式進行求值的宏定義都應(yīng)該用這種方式加上括號,避免在使用宏時由于參數(shù)中
的操作符或鄰近操作符之間產(chǎn)生不可預(yù)料的相互作用。
3.2.3#define替換規(guī)則
在程序中擴展#define定義符號和宏時,需要涉及幾個步驟
- 1.在調(diào)用宏時,首先對宏括號中的參數(shù)進行檢查,看看是否包含由#define定義的符號,如果有,這些符號首先被替換。
- 2.替換文本后,文本被插入到程序中原來文本的位置,對于宏,參數(shù)名被對應(yīng)的值所替換。
- 3.最后,再次對結(jié)果文件進行掃描,查看替換過后的內(nèi)容是否還有#define定義的符號,如果有,則重復上述處理過程
注意:
- 1.宏參數(shù)和#define定義中可以出現(xiàn)其他#define定義的符號,但是對于宏,不能實現(xiàn)遞歸。
- 2.當預(yù)處理器搜索#define定義的符號時,字符串常量的內(nèi)容并不被搜索。
比如:
#define a 123 int main() { printf("%s", "a"); return 0; }
語句printf("%s", "a");中的a并不會被替換成123
3.3.4#和##
如何把參數(shù)插入到字符串中?
如下代碼:
int main() { printf("abc""def"); return 0; }
輸出的結(jié)果是abcdef
發(fā)現(xiàn)字符串是有自動相連的特點的
看下面這個代碼:
#define PRINT(FORMAT, VALUE) printf("the value of "#VALUE " is "FORMAT"\n", VALUE) int main() { int a = 6; PRINT("%d", a); return 0; }
最終輸出的結(jié)果是:
the value of a is 6
所以#VALUE會被預(yù)處理器在預(yù)處理階段預(yù)處理為"VALUE"
接下來看看##的作用:
##可以把位于它兩邊的符號合成一個符號,并且允許宏定義從分離的文本片段創(chuàng)建標識符。
如下代碼:
#define MAXMIN 6 #define MIDDLE MAX##MIN int main() { printf("%d\n", MIDDLE); return 0; }
注意:連接之后產(chǎn)生的符號必須是已經(jīng)定義的,否則結(jié)果就是非法的。
3.2.5帶副作用的宏參數(shù)
當宏參數(shù)在宏的定義中出現(xiàn)超過一次的時候,如果參數(shù)帶有副作用,那么在使用這個宏的時候就可能會出現(xiàn)危險,導致不可預(yù)測的后果。副作用就是表達式求值的時候出現(xiàn)的永久性后果。
比如:
x + 1;//不帶有副作用
x++; //帶有副作用
如下代碼可以證明副作用的宏參數(shù)帶來的問題
#define MAX(a, b) ( (a) > (b) ? (a) : (b) ) int main() { int x = 5; int y = 8; int z = MAX(x++, y++); printf("x=%d y=%d z=%d\n", x, y, z); return 0; }
在代碼預(yù)處理之后
z = ( (x++) > (y++) ? (x++) : (y++));
所以最終結(jié)果是x=6 y=10 z=9
3.2.6宏和函數(shù)對比
宏通常被用于執(zhí)行簡單的運算,比如在兩個數(shù)中找出較大的一個
#define MAX(a, b) ((a)>(b)?(a):(b))
對于為什么不用函數(shù)來完成這個任務(wù),有兩個原因:
- 1.用于調(diào)用函數(shù)和從函數(shù)返回的代碼可能比實際執(zhí)行這個小型計算工作所需要的時間多,所以此時宏在程序的規(guī)模和運算方面更勝一籌。
- 2.函數(shù)的參數(shù)必須聲明為特定的類型,所以函數(shù)只能在類型合適的表達式上使用,而宏可以適用于整型,浮點型,長整型,宏是類型無關(guān)的。
當然宏也是有缺點的:
- 1.每次使用宏的時候,會將宏定義的代碼插入到程序中,除非宏比較短,否則可能大幅度增加程序的長度;
- 2.宏是沒法進行調(diào)試的,因為在預(yù)處理階段,宏定義的符號已經(jīng)發(fā)生了替換,此時調(diào)試看到的代碼和實際上運行時的代碼是有所差異的;
- 3.宏由于類型無關(guān),也就不夠嚴謹了;
- 4.宏可能會帶來運算級優(yōu)先的問題,導致程序容易出錯;
宏有時候也可以做到函數(shù)做不到的事情,比如宏的參數(shù)可以出現(xiàn)類型,但是函數(shù)不行
如下代碼:
#define MALLOC(num, type)\ (type *)malloc(num * sizeof(type)) ... //使用 MALLOC(10, int);//類型作為參數(shù)
預(yù)處理替換后:
(int *)malloc(10 * sizeof(int));
會節(jié)省部分代碼。
總的來對比一下宏和函數(shù)的區(qū)別:
函數(shù)和宏的使用語法很相似,所以語言本身沒法幫助區(qū)分二者,所以平時的命名習慣是:
宏名全部大寫
函數(shù)名不要全部大寫
3.3#undef
這條指令用于移除一個宏定義
語法:
#undef name
使用:
#define MAX 5 int main() { printf("%d\n", MAX); #undef MAX return 0; }
3.4命令行定義
許多C 的編譯器提供了一種能力,允許在命令行中定義符號。用于啟動編譯過程。
例如:當我們根據(jù)同一個源文件要編譯出不同的一個程序的不同版本的時候,這個特性有點用處。(假定某個程序中聲明了一個某個長度的數(shù)組,如果機器內(nèi)存有限,我們需要一個很小的數(shù)組,但是另外一個機器內(nèi)存大些,我們需要一個數(shù)組能夠大些。)
#include <stdio.h> int main() { int array [ARRAY_SIZE]; int i = 0; for(i = 0; i< ARRAY_SIZE; i ++) { array[i] = i; } for(i = 0; i< ARRAY_SIZE; i ++) { printf("%d " ,array[i]); } printf("\n" ); return 0; }
3.5條件編譯
在編譯一個程序的時候如果要將一條語句或者一組語句編譯或者放棄掉是很方便的,因為有一個叫條件編譯的東西。
對于調(diào)試性的代碼,刪除比較可惜,保留又會礙事,所以可以選擇性的去編譯。
如下代碼:
#define __DEBUG__ int main() { int arr[10] = { 0 }; int i = 0; for (i = 0; i < 10; i++) { arr[i] = i; #ifdef __DEBUG__ printf("%d ", arr[i]);//為了觀察數(shù)組是否被賦值成功 #endif } return 0; }
1.#if 常量表達式
//...
#endif
//常量表達式由預(yù)處理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif
2.多個分支的條件編譯
#if 常量表達式
//...
#elif 常量表達式
//...
#else
//...
#endif
3.判斷是否被定義
#if defined(symbol)
#ifdef symbol#if !defined(symbol)
#ifndef symbo
4.嵌套指令
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif
3.6文件包含
#include指令可以使另一個文件被編譯,會讓被包含的頭文件出現(xiàn)在#include指令的地方
這種替換的方式很簡單,預(yù)處理器會先刪除這條指令,并用包含文件里的內(nèi)容進行替換,如果這個文 件被包含了10次,那實際上就會被編譯10次
3.6.1頭文件被包含的方式
本地文件包含:
#include "fliename.h"
查找方法:先在源文件的目錄下去查找,如果該頭文件未被找到,編譯器就會像去查找?guī)旌瘮?shù)的頭文件一樣在標準位置去查找頭文件,如果還找不到就提示編譯錯誤。
庫文件包含:
#include <filename.h>
查找方法:直接在標準路徑下去查找,如果找不到就提示編譯錯誤。
雖然可以對庫文件也采用""的包含方式,但是當目錄下的文件非常多的時候,這樣查找起來的效率就會低一些了,而且也不容易去區(qū)分是庫文件還是頭文件了。
3.6.2嵌套文件包含
如圖:
- common.h和common.c是公共模塊
- test1.h和test1.c使用了公共模塊
- test2.h和test2.c使用了公共模塊
- test.h和test.c使用了test1模塊和test2模塊
這樣最終的程序中就會包含兩次common.h了,等于有2份common.h的內(nèi)容,會造成代碼的重復。
對此可以采用條件編譯的方式來解決這個問題
在引用每個頭文件時在開頭寫上這么一個內(nèi)容:
#ifndef __STDIO_H__ #define __STDIO_H__ #include <stdio.h> #endif
如果沒有定義標識符__STDIO_H__就定義__STDIO_H__并且去包含頭文件
如果下次還遇到包含頭文件的代碼,由于__STDUI_H__已經(jīng)被定義過,所以也就不會進行第二次包含了
或者對于在頭文件的開頭也可以這么寫:
#pragma once #include <stdio.h>
也可以避免頭文件的重復引入
4.其他預(yù)處理指令
比如:
#error
#pragma
#pragma
#line
到此這篇關(guān)于C語言中的程序環(huán)境與預(yù)處理詳情的文章就介紹到這了,更多相關(guān)C語言預(yù)處理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C語言實現(xiàn)大數(shù)值金額大寫轉(zhuǎn)換的方法詳解
這篇文章主要為大家詳細介紹了如何利用C語言實現(xiàn)大數(shù)值金額大寫轉(zhuǎn)換的功能,文中的示例代碼講解詳細,感興趣的小伙伴可以跟隨小編一起了解一下2023-03-03