網站首頁 編程語言 正文
1.程序的翻譯環境和執行環境
要支持c語言的實現,會有不同的編譯器出現,而這些編譯器都要遵循ANSI C,都存在兩種環境
第1種是翻譯環境,在這個環境中源代碼被轉換為可執行的機器指令。 第2種是執行環境,它用于實際執行代碼。
.obj為后綴的就是目標文件
而一個項目中可能會有很多.c后綴的源文件,分別處理后每經過編譯器單獨處理,然后會生成對應的目標文件(.obj),然后總體經過連接器處理,最終變成可執行程序。
目標文件最后還要加上鏈接庫整體一起通過鏈接器鏈接,變成可執行程序.
鏈接庫:在編寫代碼的時候,會有一些不屬于我們自己寫的函數(如printf),這些函數是自帶的庫里面包含的,這些庫就叫鏈接庫
(補函數的聲明與定義里面的靜態庫)
從源文件生成可執行程序的這一個過程就叫做翻譯環境
2.gcc C語言編譯器來演示編譯過程
2.1編譯
預編譯→編譯→匯編
預編譯(預處理):
文本操作:
1.頭文件的包含,#include——預編譯指令,將包含的頭文件給展開
2.刪除注釋(注釋被空格替換)
3.#define定義符號的替換
2.2編譯:
生成.s的文件
把c語言代碼轉換成匯編代碼
1.語法分析
2.詞法分析
3.語義分析
4.符號匯總——匯總的是全局符號
《程序員的自我修養》——通俗地講解代碼編譯過程的細節
匯編:
生成了test.o
把匯編代碼轉換成二進制指令
形成符號表:
框內是十六進制是地址
鏈接:
最終將.o文件鏈接成.exe可執行程序
1.合并段表
2.符號表的合并和重定位(像Add一開始地址為默認0和另一個.c文件內的Add地址的為0x200,會重新定位)
符號表的意義:多個目標文件進行鏈接的時候會通過符號表查看來自外部的符號是否真實存在
2.3運行環境
1.程序必須載入內存中。在有操作系統的環境中:一般這個由操作系統完成。在獨立的環境中,程序的載入必須由手工安排(電焊好伐),也可能是通過可執行代碼置入只讀內存來完成。
2.程序的執行便開始。接著便調用main函數。
3.開始執行程序代碼。這個時候程序將使用一個運行時堆棧(stack)(也就是之前博客中寫到的函數棧幀的創建與銷毀),存儲函數的局部變量和返回地址。程序同時也可以使用靜態(static)內存,存儲于靜態內存中的變量在程序的整個執行過程一直保留他們的值。
4.終止程序。正常終止main函數;也有可能是意外終止。
3詳解預處理
3.1預定義符號
- __DATE__
- __FILE__
- __LINE__
- __TIME__
- __STDC__? ?//如果編譯器遵循ANSI C,其值為1,否則未定義
作用:記錄日志:可記錄在哪個文件在哪個日期在什么時候在哪個文件在哪一行
#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; }
預處理符號的應用:
//預處理符號的應用——寫日志 #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定義標識符
兩種用法:
#define MM 100 #define reg register——關鍵字替換
#define末尾有時候可以加分號有時又不可以加上分號,
不可以加上分號的情況:
//不可以加上分號的情況 #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; }
因為加上分號會使得宏在替換的時候也帶上分號,所以在調用在一些函數內部的時候會出現錯誤。
綜上,當我們定義宏的時候,最好不要加分號在末尾。
3.2.2 #define定義宏
這里也是將全部參數給替換掉,在預處理的時候就替換掉了,不信的話可以在解決方案處右擊,點擊屬性后選擇預處理,然后就可以在debug里面發現又應該.i文件,點開后就可以發現這里已經被替換掉了。
#define Max(x,y) ((x)>(y)?(x):(y)) // Max->宏的名字 // x和y->宏的參數 // ((x)>(y)?(x):(y))->宏的內容
ps:在定義宏的內容的時候,最好每個參數都要加上小括號,然后最后整體加上小括號,否則如果傳入參數不是單獨一個值而是表達式的時候,會產生一些沒有意料到的優先級計算改變
Tips:宏后面的參數的小括號一定要緊挨著宏的名
3.2.3 #define替換規則
1.先看宏的參數內是不是有define的符號,優先替換掉define符號
2.對于宏,參數名被他們的值替換
注意!!
1.宏的參數里可以出現其他#define定義的符號,但不可以遞歸
2.當define掃描預處理時,字符串常量的內容并不被搜索(也就是說字符串里面的東西是不會被宏預處理的)
3.2.4 #和##
#
相當于把宏的參數放進字符串中變成所對應的字符串
// #的用法 #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; }
##
可以把兩邊分離片段合成一個符號
#define CAT(C,num) C##num int main() { int Class104=10000; printf("%d\n",CAT(Class,104)); return 0; }
3.2.5帶副作用的宏參數
#define MAX(x,y) ((x)>(y)?(x):(y)) int main() { int a=3; int b=5; int m=MAX(a++,b++);//宏的參數是直接替換進去 所以替換完之后為: int m=((a++)>(b++)?(a++):(b++));//會出現錯誤 printf("%d\n",m); printf("%d %d\n",a,b); return 0; }
3.2.6宏和函數對比
宏的優點:
1.用于調用函數和從函數返回的代碼可能比實際執行這個小型計算工作所需要的時間更多。 所以宏比函數在程序的規模和速度方面更勝一籌
2.更為重要的是函數的參數必須聲明為特定的類型。 所以函數只能在類型合適的表達式上使用。反之這個宏怎可以適用于整形、長整型、浮點型等可以 用于>來比較的類型。
宏的缺點:
1.每次使用宏的時候,一份宏定義的代碼將插入到程序中。除非宏比較短,否則可能大幅度增加程序的長度。
2.宏是沒法調試的。
3.宏由于類型無關,也就不夠嚴謹。
4.宏可能會帶來運算符優先級的問題,導致程容易出現錯
可從以下方面比較宏與函數的區別:
- 代碼長度——宏如果不是特別小的話,每一次使用的時候都要替換成宏的定義,可能會導致最終代碼特別長,大幅增長程序長度,而函數每次都只調用那一段代碼
- 執行速度——宏只需要執行一行的代碼,而函數擁有調用函數,執行代碼,返回參數這三步操作,所以相對來說會慢一些
- 操作符優先級——由于宏是不經過計算直接將參數傳進去的,所以在傳參后可能會有優先級的不同導致結果與我們想要的最終結果有出入,除非加上括號。相對的函數參數只在函數調用的時候求值一次,會比較容易猜測結果。
- 帶有副作用的參數——參數可能被替換到宏體中的多個位置,所以帶有副作用的參數求值可能會產生不可預料的結果。函數參數只在傳參的時候求值一次,結果更容易控制。
- 參數類型——宏的參數類型相對自由,只要對參數的操作合法,就可以任何只要符合規定的參數類型。函數的參數類型是固定死的。
- 調試——宏不方便調試。函數能夠調試。
- 遞歸——宏不能夠遞歸,而函數可以遞歸
3.2.7 命名的約定
一般來講宏與函數的使用語法很類似,所以以后使用這種方法區分宏與函數:
- 宏名全部大寫
- 函數名不要全部大寫(可開頭或部分大寫)
3.3 undef
去除一個宏定義
#undef 宏名
3.4命令行定義
許多C 的編譯器提供了一種能力,允許在命令行中定義符號。用于啟動編譯過程。 例如:當我們根據同一個源文件要編譯出不同的一個程序的不同版本的時候,這個特性有點用處。(假定某個程序中聲明了一個某個長度的數組,如果機器內存有限,我們需要一個很小的數組,但是另外一個機器內存大些,我們需要一個數組能夠大些。)
#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這個符號始終沒有被定義,到這里為止我們的程序還是無法運行的,會報錯,但是:
編譯指令:
//linux 環境演示 gcc -D SZ=10 programe.c
在編譯完這一行以后程序就能夠執行了,這是因為我們在命令行中已經將SZ這個符號定義好了。
3.5 條件編譯
有一段代碼,編譯了麻煩,刪去了可惜,這時可以選擇是否編譯,這時候就要用到條件編譯。
應用場景:當我們使用在不同系統時,比如在用到windows系統時我們需要用到這一段代碼,而在Linus系統上又要用到另一段代碼而不能用windows那段代碼的時候,不可以刪除,因為要實現一個程序的跨平臺使用,這時候就需要用到條件編譯來選擇什么時候使用哪段代碼。
Tips:我們要明確條件編譯指令也是預處理指令
//條件編譯 #define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> int main() { int i = 0; for (i = 0; i < 10; i++) { #if 1 //這里的常量非0,于是執行,如果為0,則不執行 printf("%d\n", i); #endif return 0; } }
不能放變量,因為是預處理階段執行的,而變量在預處理中還沒有出現,所以我們只能放常量進去,否則放變量進去的話只能判定為0。
常見的條件編譯指令:
1.#if 常量/常量表達式? ? ?#endif
2.#if 常量表達式 #elif 常量表達式 #else 常量表達式? ? #endif
3.判斷是否被定義:只要你定義了宏就為真
- #if defined(宏名) ...#endif #ifdef 宏名 ... #endif
- 如果要做到的是沒有定義,則在前面加上!
#if !defined(宏名) ...#endif? ? ? ? ? ? ?#ifdef !宏名 ... #endif
4.嵌套指令
//全部類型的條件編譯 #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文件包含
我們知道在預編譯時會包含頭文件,而頭文件例如<stdio.h>從預編譯.i文件上看我們可以知道有2000多行代碼,若真的重復包含了五六次,則代碼量直接上升到了一萬多行,這時候就會使得代碼過于冗長,同時也占用很多內存,這個時候我們就需要文件包含來確認是否重復包含了同一個頭文件。
方法一:
//在頭文件中 #ifndef __TEST_H__ #define __TEST_H__ int Add(int x,int y); #endif
方法二:
//在頭文件中 #pragma once int Add(int x,int y);
ps:這個#pragma once是比較高級的寫法,在遠古編譯器里面是無法使用的(如vc)
3.6.1頭文件被包含的方式
在小綠本人之前的三子棋以及掃雷的博客中都有自己創造頭文件而我們在引用自己創造的頭文件時是以#include "fun.h" 這樣的形式引用的,但是在引用庫函數時確實以#include <stdio.h>的方式引用,那么用不同的符號引用頭文件有什么不一樣呢?
""的查找策略
""的查找策略是:先在源文件所在的目錄下查找,如果沒有找到,編譯器就像查找庫函數頭文件一樣在標準位置查找頭文件。再如果找不到就提示編譯錯誤。
<>的查找策略
查找頭文件直接去標準位置下查找,如果找不到就直接提示編譯錯誤。
總結
原文鏈接:https://blog.csdn.net/Green_756/article/details/123674688
相關推薦
- 2022-08-21 python數字圖像處理之邊緣輪廓檢測_python
- 2022-04-23 git如何提交本地倉庫并同步碼云倉庫
- 2023-08-30 linux服務器使用rsync 和 inotify或者sersync 實現服務器之間文件實時同步
- 2022-12-22 postgresql13主從搭建Ubuntu_PostgreSQL
- 2022-07-12 jmm內存模型及volatile實現原理
- 2022-06-23 巧妙使用python?opencv庫玩轉視頻幀率_python
- 2021-12-03 C++類和對象實戰之Date類的實現方法_C 語言
- 2022-12-13 C++?Boost?Format超詳細講解_C 語言
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支