一起來(lái)學(xué)習(xí)C語(yǔ)言的程序環(huán)境與預(yù)處理
1.程序的翻譯環(huán)境和執(zhí)行環(huán)境
要支持c語(yǔ)言的實(shí)現(xiàn),會(huì)有不同的編譯器出現(xiàn),而這些編譯器都要遵循ANSI C,都存在兩種環(huán)境
第1種是翻譯環(huán)境,在這個(gè)環(huán)境中源代碼被轉(zhuǎn)換為可執(zhí)行的機(jī)器指令。 第2種是執(zhí)行環(huán)境,它用于實(shí)際執(zhí)行代碼。
.obj為后綴的就是目標(biāo)文件
而一個(gè)項(xiàng)目中可能會(huì)有很多.c后綴的源文件,分別處理后每經(jīng)過(guò)編譯器單獨(dú)處理,然后會(huì)生成對(duì)應(yīng)的目標(biāo)文件(.obj),然后總體經(jīng)過(guò)連接器處理,最終變成可執(zhí)行程序。
目標(biāo)文件最后還要加上鏈接庫(kù)整體一起通過(guò)鏈接器鏈接,變成可執(zhí)行程序.
鏈接庫(kù):在編寫(xiě)代碼的時(shí)候,會(huì)有一些不屬于我們自己寫(xiě)的函數(shù)(如printf),這些函數(shù)是自帶的庫(kù)里面包含的,這些庫(kù)就叫鏈接庫(kù)
(補(bǔ)函數(shù)的聲明與定義里面的靜態(tài)庫(kù))
從源文件生成可執(zhí)行程序的這一個(gè)過(guò)程就叫做翻譯環(huán)境
2.gcc C語(yǔ)言編譯器來(lái)演示編譯過(guò)程
2.1編譯
預(yù)編譯→編譯→匯編
預(yù)編譯(預(yù)處理):
文本操作:
1.頭文件的包含,#include——預(yù)編譯指令,將包含的頭文件給展開(kāi)
2.刪除注釋?zhuān)ㄗ⑨尡豢崭裉鎿Q)
3.#define定義符號(hào)的替換
2.2編譯:
生成.s的文件
把c語(yǔ)言代碼轉(zhuǎn)換成匯編代碼
1.語(yǔ)法分析
2.詞法分析
3.語(yǔ)義分析
4.符號(hào)匯總——匯總的是全局符號(hào)
《程序員的自我修養(yǎng)》——通俗地講解代碼編譯過(guò)程的細(xì)節(jié)
匯編:
生成了test.o
把匯編代碼轉(zhuǎn)換成二進(jìn)制指令
形成符號(hào)表:
框內(nèi)是十六進(jìn)制是地址
鏈接:
最終將.o文件鏈接成.exe可執(zhí)行程序
1.合并段表
2.符號(hào)表的合并和重定位(像Add一開(kāi)始地址為默認(rèn)0和另一個(gè).c文件內(nèi)的Add地址的為0x200,會(huì)重新定位)
符號(hào)表的意義:多個(gè)目標(biāo)文件進(jìn)行鏈接的時(shí)候會(huì)通過(guò)符號(hào)表查看來(lái)自外部的符號(hào)是否真實(shí)存在
2.3運(yùn)行環(huán)境
1.程序必須載入內(nèi)存中。在有操作系統(tǒng)的環(huán)境中:一般這個(gè)由操作系統(tǒng)完成。在獨(dú)立的環(huán)境中,程序的載入必須由手工安排(電焊好伐),也可能是通過(guò)可執(zhí)行代碼置入只讀內(nèi)存來(lái)完成。
2.程序的執(zhí)行便開(kāi)始。接著便調(diào)用main函數(shù)。
3.開(kāi)始執(zhí)行程序代碼。這個(gè)時(shí)候程序?qū)⑹褂靡粋€(gè)運(yùn)行時(shí)堆棧(stack)(也就是之前博客中寫(xiě)到的函數(shù)棧幀的創(chuàng)建與銷(xiāo)毀),存儲(chǔ)函數(shù)的局部變量和返回地址。程序同時(shí)也可以使用靜態(tài)(static)內(nèi)存,存儲(chǔ)于靜態(tài)內(nèi)存中的變量在程序的整個(gè)執(zhí)行過(guò)程一直保留他們的值。
4.終止程序。正常終止main函數(shù);也有可能是意外終止。
3詳解預(yù)處理
3.1預(yù)定義符號(hào)
- __DATE__
- __FILE__
- __LINE__
- __TIME__
- __STDC__ //如果編譯器遵循ANSI C,其值為1,否則未定義
作用:記錄日志:可記錄在哪個(gè)文件在哪個(gè)日期在什么時(shí)候在哪個(gè)文件在哪一行
#define _CRT_sECURE_NO_WARNINGS 1 #include <stdio.h> int main() { printf("%s\n", __FILE__); printf("%s\n", __TIME__); printf("%d\n", __LINE__); printf("%s\n", __DATE__); return 0; }
預(yù)處理符號(hào)的應(yīng)用:
//預(yù)處理符號(hào)的應(yīng)用——寫(xiě)日志 #define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> #include <string.h> #include <errno.h> int main() { int i = 0; FILE* pf = fopen("test.txt", "w"); if (NULL == pf) { printf("error is%s\n", strerror(errno)); return 0; } for (i = 0; i < 5; i++) { fprintf(pf, "%s\t%s\t%s\t%d\ti=%d\n", __FILE__, __DATE__, __TIME__, __LINE__, i); } fclose(pf); pf = NULL; return 0; }
3.2#define
3.2.1#define定義標(biāo)識(shí)符
兩種用法:
#define MM 100 #define reg register——關(guān)鍵字替換
#define末尾有時(shí)候可以加分號(hào)有時(shí)又不可以加上分號(hào),
不可以加上分號(hào)的情況:
//不可以加上分號(hào)的情況 #define _CRT_SECURE_NO_WARNINGS 1 #define MAX(x,y) ((x)>(y)?x:y); #include <stdio.h> int main() { int a = 5; int b = 3; printf("%d\n", MAX(a, b)); return 0; }
因?yàn)榧由戏痔?hào)會(huì)使得宏在替換的時(shí)候也帶上分號(hào),所以在調(diào)用在一些函數(shù)內(nèi)部的時(shí)候會(huì)出現(xiàn)錯(cuò)誤。
綜上,當(dāng)我們定義宏的時(shí)候,最好不要加分號(hào)在末尾。
3.2.2 #define定義宏
這里也是將全部參數(shù)給替換掉,在預(yù)處理的時(shí)候就替換掉了,不信的話(huà)可以在解決方案處右擊,點(diǎn)擊屬性后選擇預(yù)處理,然后就可以在debug里面發(fā)現(xiàn)又應(yīng)該.i文件,點(diǎn)開(kāi)后就可以發(fā)現(xiàn)這里已經(jīng)被替換掉了。
#define Max(x,y) ((x)>(y)?(x):(y)) // Max->宏的名字 // x和y->宏的參數(shù) // ((x)>(y)?(x):(y))->宏的內(nèi)容
ps:在定義宏的內(nèi)容的時(shí)候,最好每個(gè)參數(shù)都要加上小括號(hào),然后最后整體加上小括號(hào),否則如果傳入?yún)?shù)不是單獨(dú)一個(gè)值而是表達(dá)式的時(shí)候,會(huì)產(chǎn)生一些沒(méi)有意料到的優(yōu)先級(jí)計(jì)算改變
Tips:宏后面的參數(shù)的小括號(hào)一定要緊挨著宏的名
3.2.3 #define替換規(guī)則
1.先看宏的參數(shù)內(nèi)是不是有define的符號(hào),優(yōu)先替換掉define符號(hào)
2.對(duì)于宏,參數(shù)名被他們的值替換
注意?。?/strong>
1.宏的參數(shù)里可以出現(xiàn)其他#define定義的符號(hào),但不可以遞歸
2.當(dāng)define掃描預(yù)處理時(shí),字符串常量的內(nèi)容并不被搜索(也就是說(shuō)字符串里面的東西是不會(huì)被宏預(yù)處理的)
3.2.4 #和##
#
相當(dāng)于把宏的參數(shù)放進(jìn)字符串中變成所對(duì)應(yīng)的字符串
// #的用法 #define _CRT_SECURE_NO_WARNINGS 1 #define print(x) printf("the value of " #x " is %d\n",x) #include <stdio.h> int main() { int a = 5; int b = 4; print(a); print(b); return 0; }
##
可以把兩邊分離片段合成一個(gè)符號(hào)
#define CAT(C,num) C##num int main() { int Class104=10000; printf("%d\n",CAT(Class,104)); return 0; }
3.2.5帶副作用的宏參數(shù)
#define MAX(x,y) ((x)>(y)?(x):(y)) int main() { int a=3; int b=5; int m=MAX(a++,b++);//宏的參數(shù)是直接替換進(jìn)去 所以替換完之后為: int m=((a++)>(b++)?(a++):(b++));//會(huì)出現(xiàn)錯(cuò)誤 printf("%d\n",m); printf("%d %d\n",a,b); return 0; }
3.2.6宏和函數(shù)對(duì)比
宏的優(yōu)點(diǎn):
1.用于調(diào)用函數(shù)和從函數(shù)返回的代碼可能比實(shí)際執(zhí)行這個(gè)小型計(jì)算工作所需要的時(shí)間更多。 所以宏比函數(shù)在程序的規(guī)模和速度方面更勝一籌
2.更為重要的是函數(shù)的參數(shù)必須聲明為特定的類(lèi)型。 所以函數(shù)只能在類(lèi)型合適的表達(dá)式上使用。反之這個(gè)宏怎可以適用于整形、長(zhǎng)整型、浮點(diǎn)型等可以 用于>來(lái)比較的類(lèi)型。
宏的缺點(diǎn):
1.每次使用宏的時(shí)候,一份宏定義的代碼將插入到程序中。除非宏比較短,否則可能大幅度增加程序的長(zhǎng)度。
2.宏是沒(méi)法調(diào)試的。
3.宏由于類(lèi)型無(wú)關(guān),也就不夠嚴(yán)謹(jǐn)。
4.宏可能會(huì)帶來(lái)運(yùn)算符優(yōu)先級(jí)的問(wèn)題,導(dǎo)致程容易出現(xiàn)錯(cuò)
可從以下方面比較宏與函數(shù)的區(qū)別:
- 代碼長(zhǎng)度——宏如果不是特別小的話(huà),每一次使用的時(shí)候都要替換成宏的定義,可能會(huì)導(dǎo)致最終代碼特別長(zhǎng),大幅增長(zhǎng)程序長(zhǎng)度,而函數(shù)每次都只調(diào)用那一段代碼
- 執(zhí)行速度——宏只需要執(zhí)行一行的代碼,而函數(shù)擁有調(diào)用函數(shù),執(zhí)行代碼,返回參數(shù)這三步操作,所以相對(duì)來(lái)說(shuō)會(huì)慢一些
- 操作符優(yōu)先級(jí)——由于宏是不經(jīng)過(guò)計(jì)算直接將參數(shù)傳進(jìn)去的,所以在傳參后可能會(huì)有優(yōu)先級(jí)的不同導(dǎo)致結(jié)果與我們想要的最終結(jié)果有出入,除非加上括號(hào)。相對(duì)的函數(shù)參數(shù)只在函數(shù)調(diào)用的時(shí)候求值一次,會(huì)比較容易猜測(cè)結(jié)果。
- 帶有副作用的參數(shù)——參數(shù)可能被替換到宏體中的多個(gè)位置,所以帶有副作用的參數(shù)求值可能會(huì)產(chǎn)生不可預(yù)料的結(jié)果。函數(shù)參數(shù)只在傳參的時(shí)候求值一次,結(jié)果更容易控制。
- 參數(shù)類(lèi)型——宏的參數(shù)類(lèi)型相對(duì)自由,只要對(duì)參數(shù)的操作合法,就可以任何只要符合規(guī)定的參數(shù)類(lèi)型。函數(shù)的參數(shù)類(lèi)型是固定死的。
- 調(diào)試——宏不方便調(diào)試。函數(shù)能夠調(diào)試。
- 遞歸——宏不能夠遞歸,而函數(shù)可以遞歸
3.2.7 命名的約定
一般來(lái)講宏與函數(shù)的使用語(yǔ)法很類(lèi)似,所以以后使用這種方法區(qū)分宏與函數(shù):
- 宏名全部大寫(xiě)
- 函數(shù)名不要全部大寫(xiě)(可開(kāi)頭或部分大寫(xiě))
3.3 undef
去除一個(gè)宏定義
#undef 宏名
3.4命令行定義
許多C 的編譯器提供了一種能力,允許在命令行中定義符號(hào)。用于啟動(dòng)編譯過(guò)程。 例如:當(dāng)我們根據(jù)同一個(gè)源文件要編譯出不同的一個(gè)程序的不同版本的時(shí)候,這個(gè)特性有點(diǎn)用處。(假定某個(gè)程序中聲明了一個(gè)某個(gè)長(zhǎng)度的數(shù)組,如果機(jī)器內(nèi)存有限,我們需要一個(gè)很小的數(shù)組,但是另外一個(gè)機(jī)器內(nèi)存大些,我們需要一個(gè)數(shù)組能夠大些。)
#include <stdio.h> int main() { int array [SZ]; int i = 0; for(i = 0; i< SZ; i ++) { array[i] = i; } for(i = 0; i< SZ; i ++) { printf("%d " ,array[i]); } printf("\n" ); return 0; }
在這里我們可以知道SZ這個(gè)符號(hào)始終沒(méi)有被定義,到這里為止我們的程序還是無(wú)法運(yùn)行的,會(huì)報(bào)錯(cuò),但是:
編譯指令:
//linux 環(huán)境演示 gcc -D SZ=10 programe.c
在編譯完這一行以后程序就能夠執(zhí)行了,這是因?yàn)槲覀冊(cè)诿钚兄幸呀?jīng)將SZ這個(gè)符號(hào)定義好了。
3.5 條件編譯
有一段代碼,編譯了麻煩,刪去了可惜,這時(shí)可以選擇是否編譯,這時(shí)候就要用到條件編譯。
應(yīng)用場(chǎng)景:當(dāng)我們使用在不同系統(tǒng)時(shí),比如在用到windows系統(tǒng)時(shí)我們需要用到這一段代碼,而在Linus系統(tǒng)上又要用到另一段代碼而不能用windows那段代碼的時(shí)候,不可以刪除,因?yàn)橐獙?shí)現(xiàn)一個(gè)程序的跨平臺(tái)使用,這時(shí)候就需要用到條件編譯來(lái)選擇什么時(shí)候使用哪段代碼。
Tips:我們要明確條件編譯指令也是預(yù)處理指令
//條件編譯 #define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> int main() { int i = 0; for (i = 0; i < 10; i++) { #if 1 //這里的常量非0,于是執(zhí)行,如果為0,則不執(zhí)行 printf("%d\n", i); #endif return 0; } }
不能放變量,因?yàn)槭穷A(yù)處理階段執(zhí)行的,而變量在預(yù)處理中還沒(méi)有出現(xiàn),所以我們只能放常量進(jìn)去,否則放變量進(jìn)去的話(huà)只能判定為0。
常見(jiàn)的條件編譯指令:
1.#if 常量/常量表達(dá)式 #endif
2.#if 常量表達(dá)式 #elif 常量表達(dá)式 #else 常量表達(dá)式 #endif
3.判斷是否被定義:只要你定義了宏就為真
- #if defined(宏名) ...#endif #ifdef 宏名 ... #endif
- 如果要做到的是沒(méi)有定義,則在前面加上!
#if !defined(宏名) ...#endif #ifdef !宏名 ... #endif
4.嵌套指令
//全部類(lèi)型的條件編譯 #define _CRT_SECURE_NO_WARNINGS 1 #define VALUE 200 #define TEST 20 #include <stdio.h> int main() { int i = 0; for (i = 0; i < 10; i++) { #if 1 printf("%d\n", i); #endif } #if VALUE<100 printf("value<100\n"); #elif VALUE>=100&&VALUE<=150 printf("value>=100且value<=150\n"); #else printf("value>150\n"); #endif #ifdef VALUE printf("VALUE已定義\n"); #else printf("VALUE未定義\n"); #endif #if defined(VALUES) printf("VALUES已定義\n"); #else printf("VALUES未定義\n"); #endif //嵌套指令 #if VALUE<=150 #if defined(VALUE) printf("VALUE小于或等于150,VALUE已定義\n"); #else printf("VALUE小于或等于150,VALUE未定義\n"); #endif #elif VALUE>150&&VALUE<=200 #ifdef TEST printf("VALUE大于150且小于等于200,TEST已定義\n"); #else printf("VALUE大于150且小于等于200,TEST未定義\n"); #endif #endif return 0; }
3.6文件包含
我們知道在預(yù)編譯時(shí)會(huì)包含頭文件,而頭文件例如<stdio.h>從預(yù)編譯.i文件上看我們可以知道有2000多行代碼,若真的重復(fù)包含了五六次,則代碼量直接上升到了一萬(wàn)多行,這時(shí)候就會(huì)使得代碼過(guò)于冗長(zhǎng),同時(shí)也占用很多內(nèi)存,這個(gè)時(shí)候我們就需要文件包含來(lái)確認(rèn)是否重復(fù)包含了同一個(gè)頭文件。
方法一:
//在頭文件中 #ifndef __TEST_H__ #define __TEST_H__ int Add(int x,int y); #endif
方法二:
//在頭文件中 #pragma once int Add(int x,int y);
ps:這個(gè)#pragma once是比較高級(jí)的寫(xiě)法,在遠(yuǎn)古編譯器里面是無(wú)法使用的(如vc)
3.6.1頭文件被包含的方式
在小綠本人之前的三子棋以及掃雷的博客中都有自己創(chuàng)造頭文件而我們?cè)谝米约簞?chuàng)造的頭文件時(shí)是以#include "fun.h" 這樣的形式引用的,但是在引用庫(kù)函數(shù)時(shí)確實(shí)以#include <stdio.h>的方式引用,那么用不同的符號(hào)引用頭文件有什么不一樣呢?
""的查找策略
""的查找策略是:先在源文件所在的目錄下查找,如果沒(méi)有找到,編譯器就像查找?guī)旌瘮?shù)頭文件一樣在標(biāo)準(zhǔn)位置查找頭文件。再如果找不到就提示編譯錯(cuò)誤。
<>的查找策略
查找頭文件直接去標(biāo)準(zhǔn)位置下查找,如果找不到就直接提示編譯錯(cuò)誤。
總結(jié)
本篇文章就到這里了,希望能夠給你帶來(lái)幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
C++使用文件實(shí)現(xiàn)學(xué)生信息管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C++使用文件實(shí)現(xiàn)學(xué)生信息管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-01-01C語(yǔ)言實(shí)現(xiàn)簡(jiǎn)單的聊天室功能
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)簡(jiǎn)單的聊天室功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-06-06libevent庫(kù)的使用--定時(shí)器的使用實(shí)例
這篇文章主要介紹了libevent庫(kù)的使用--定時(shí)器的使用實(shí)例,有需要的朋友可以參考一下2013-12-12C語(yǔ)言行優(yōu)先和列優(yōu)先的問(wèn)題深入分析
這篇文章主要介紹了C語(yǔ)言行優(yōu)先和列優(yōu)先的問(wèn)題深入分析的相關(guān)資料,需要的朋友可以參考下2017-01-01C/C++ 公有繼承、保護(hù)繼承和私有繼承的對(duì)比詳解
這篇文章主要介紹了C/C++ 公有繼承、保護(hù)繼承和私有繼承的區(qū)別的相關(guān)資料,需要的朋友可以參考下2017-02-02