目錄
一,翻譯環境
編譯器
聯結器
二,執行環境
三,預處理詳解
預定義符號
#define
#
##
帶副作用的宏引數
宏和函式的對比
#undef
命令列定義
條件編譯
檔案包含
其他預處理指令
在ANSI C的任何一種實作中,存在兩個不同的環境
- 翻譯環境,將源代碼轉換為可執行的機器指令;
- 執行環境,用于實際執行代碼;

一,翻譯環境
- 每個源檔案通過編譯程序分別轉換成目標檔案;
- 每個目標檔案由聯結器捆綁在一起,形成一個單一而完整的可執行程式;
- 聯結器同時也會引入標準C函式庫中任何被該程式用到的函式,且可搜索程式員個人的程式庫,將其需要的函式也鏈接到程式中;

編譯器
- 編譯 = 預編譯(預處理) + 編譯 + 匯編;
- cl.exe,是Microsoft C/C++編譯器;
預編譯/預處理(文本操作)
- #include,完成了頭檔案的包含;
- #define,定義的符號和宏的替換;
- 洗掉注釋;
編譯
- 把C語言代碼,轉換為匯編代碼;
- 語法分析;
- 詞法分析;
- 語意分析;
- 符號匯總;
《編譯原理》
匯編
- 把匯編代碼轉換為機器指令(二進制指令);
- 匯編后生成.obj檔案(elf格式);
- 生成符號表;
聯結器
- 鏈接,把多個目標檔案和鏈接庫鏈接生成可執行程式(elf格式);
- link.exe;

《程式員的自我修養》
二,執行環境
程式執行程序
- 程式必須載入記憶體中;作業系統環境中,一般由作業系統完成;獨立環境中,必須手動安排,也可能通過執行代碼置入只讀記憶體來完成;
- 開始執行程式,接著呼叫main函式;
- 開始執行程式代碼,此時程式使用一個運行時堆疊,存盤函式的區域變數和回傳地址;程式同時也可使用靜態記憶體,存盤靜態記憶體中的變數在程式的整個執行程序中一直保留它們的值;
- 終止程式,正常終止main函式,或意外終止;

三,預處理詳解
預定義符號
- C語言已預先定義好的內置符號;
- 可用于日志資訊,以便于除錯等;
__FILE__ //進行編譯的源檔案
__LINE__ //檔案當前的行號
__DATE__ //檔案被編譯的日期
__TIME__ //檔案被編譯的時間
__STDC__ //如檔案編譯器遵循ANSI C,其值為1,否則未定義(gcc支持、vs不支持)
int main()
{
printf("%s\n", __FILE__); //F:\VS\Project1\test.c
printf("%d\n", __LINE__); //1990
printf("%s\n", __DATE__); //Aug 1 2021
printf("%s\n", __TIME__); //10:23 : 43
}
#define
#define定義的識別符號
- 預處理階段時,替換;
- 末尾建議不要加(;),否則多一個空陳述句或語法錯誤;
#define MAX 1000 //可替換數值
#define reg register //可替換關鍵字
#define do forever for(;;) //可替換一段陳述句
#define CASE break;case //可替換一段代碼
//可替換多行代碼(\續行符)
#define DEBUG_PRINT printf("file:%s\tline:%d\tdate:%s\ttime:%s\n",\
__FILE__, __LINE__, \
__DATE__, __TIME__)
#define定義宏
- 允許把引數替換到文本中,稱為宏(macro)或定義宏(define macro);
#define name( parament-list ) stuff
- parament-list 引數串列,會在stuff中完成替換;
- 括號()必須緊鄰name;
#define SQUARE(x) x*x
int main()
{
int ret = SQUARE(4); //先傳參,在替換
printf("%d", ret);
}
//結果:16
注:
- 宏是先替換,在計算的;
- 對數值運算式進行求值的宏定義,應加上括號,避免在使用宏時由于引數中的運算子和鄰近運算子之間產生歧義;
#define SQUARE(x) x*x
int main()
{
int ret1 = SQUARE(3 + 1); //3+1*3+1
int ret2 = 3 * SQUARE(3 + 1); //3*3+1*3+1
printf("%d %d", ret1, ret2);
}
//結果:7 13
#define SQUARE(x) ((x)*(x))
int main()
{
int ret1 = SQUARE(3 + 1); //(3+1)*(3+1)
int ret2 = 3 * SQUARE(3 + 1); //3*((3+1)*(3+1))
printf("%d %d", ret1, ret2);
}
//結果:16 48
#define替換規則
- 在呼叫宏時,首先對引數進行檢查,看是否包含任何由#define定義的符號;如果是,它們首先被替換;
- 替換文本會被插入到程式中原來文本的位置;對于宏,引數名被他們的值替換;
- 最后,再次對結果檔案進行掃描,看是否包含任何由#define定義的符號,如果是,就重復上述處理程序,
#define M 100
#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{
int ret = MAX(10, M);
return 0;
}
注:
- 宏引數和#define定義中,可以出現其他#define定義的變數,但是對于宏,不能出現遞回,
- 當前處理器搜索#define定義的符號的時候,字串常量的內容并不被搜索,
#define M 100
int main()
{
printf("M"); //此M不會被替換
return 0;
}
#
- #宏引數,可把一個宏引數變成對應的字串;
- 可實作在字串中插入引數;
#define fun(x) #x //即轉換為“x”
int main()
{
char str[] = fun(abcd);
printf("%s", str);
return 0;
}
//結果:abcd
#define print(x, Format) printf("The value of "#x" "Format"!\n", x)
int main()
{
int num1 = 10;
float num2 = 20;
print(num1, "%d"); //printf("The value of ""num1"" %d!\n", num1)
print(num2, "%f"); //printf("The value of ""num2"" %f!\n", num2)
return 0;
}
//The value of num1 10!
//The value of num2 20.000000!
##
- 可把兩邊的符號合成一個符號;
- 連接后需產生一個合法的識別符號;
#define CAT(x, y) x##y
int main()
{
char str[] = CAT("ab", "cd");
int num1 = 10;
int num2 = CAT(num, 1);
return 0;
}
帶副作用的宏引數
- 當宏引數在宏定義中出現超過一次時,帶有副作用的引數,可能會出現未知風險;
#define MAX(x, y) ((x)>(y)?(x):(y))
int main()
{
int a = 5;
int b = 8;
int ret = MAX(a++, b++); //((a++)>(b++)?(a++):(b++))
printf("%d %d %d", ret, a, b); //9 6 10
}
宏和函式的對比
宏,通常被用于執行簡單的運算;
宏優勢
- 用于呼叫函式和函式回傳的代碼,可能會比實際執行小型計算的作業量所需時間更多;所以宏比函式在程式的規模和速度方面更優;
- 函式的引數必須宣告為特定的型別,所以函式只能在型別合適的運算式上使用;但,宏與型別無關;
宏劣勢
- 每次使用宏時,會插入到程式中;可能會大幅度增加程式的長度;
- 宏無法除錯,除錯是在可執行程式后;
- 宏與型別無關,所以不夠嚴謹;
- 宏可能帶來運算子優先級的問題,容易出錯;
//宏,可傳型別引數
#define MALLOC(num,type) (type*)malloc(num*sizeof(type))
int main()
{
//(int*)malloc(10*sizeof(int))
MALLOC(10, int);
return 0;
}
注:
- 命名約定,宏名全部大寫,函式名不全部大寫;

#undef
- 取消宏定義
#define M 100
int main()
{
#undef M
printf("%d", M); //M為未定義的識別符號
return 0;
}
命令列定義
- 許多C編譯器,均允許命令列中定義符號,用于啟動編譯程序;
- 如,在命令列中定義數值長度;
條件編譯
#define M 100
int main()
{
#ifndef N
printf("%d", M); //如未定義了N,即編譯此陳述句
#endif
#ifdef M
printf("%d", M); //如定義了M,即編譯此陳述句
#endif
return 0;
}

檔案包含
#include
- 可使另一個檔案被編譯;
- 替換程序為前處理器先洗掉此指令,用包含檔案的內容替換,被包含幾次就替換幾次;
頭檔案包含方式
- 本地檔案包含,#include “filename”;先在源檔案所在目錄下查找,如該頭檔案未找到,編譯器在像查找庫函式頭檔案一樣,在標準位置查找頭檔案,如在未找到,提示編譯錯誤;
- 庫檔案包含,#include <filename>;直接去標準路徑下去查找,如果找不到就提示編譯錯誤;
注:
- linux環境標準頭檔案的路徑,/usr/include;
- VS環境標準頭檔案的路徑,C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\ucrt;
嵌套檔案包含
- 會造成多次重復參考頭檔案;
- 解決方法,條件編譯(在每個頭檔案開頭添加#ifndef...#endif、或#pragma once);
其他預處理指令
- #error
- #pragma
- #line
- #pragma pack()
- ...
《高質量C/C++編程指南》
《C語言深度剖析》
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/291912.html
標籤:其他
