預處理:
程式設計師所編譯C程式碼不能被直接編譯,它需要一段程式把它先翻譯一下,被翻譯過程預處理,負責翻譯的程式叫前處理器,被翻譯的指令叫預處理指令,C程式碼中以#開頭的都是預處理指令。
gcc -E xxx.c 檢視C程式碼的預處理結果,顯示在終端
gcc -E xxx.c -o xxx.i 把預處理的結果儲存到檔案中,以.i結尾的檔案也被稱為預處理檔案。
檔案包含指令:
#include 預處理指令的功能是匯入一個標頭檔案到當檔案中,它用兩中使用方法:
方法1:#include <file_name.h>
從系統指定的路徑查詢並導致標頭檔案,一般用於匯入標準庫、系統、第三方庫的標頭檔案。getch.h
/usr/include
方法2:#include "file_name.h"
從系統當前路徑查詢並導致標頭檔案,如果沒有再從系統指定的路徑查詢並導致標頭檔案,一般用於匯入自定義標頭檔案。
作業系統透過設定環境變數來指定標頭檔案查詢路徑,或者設定編譯器引數
gcc xxx -I /path(大寫i)
宏替換指令:
定義宏名:
#define 宏名 會被替換的內容
使用宏名:
printf("%d\n",宏名);
注意:在預處理階段,前處理器會把程式碼使用的宏名替換成宏名後面的那段內容。
宏常量:
#define 宏名 字面值資料
給沒有意義的字面值資料,取一個有意義名字,代替它,這樣可以提高程式碼的可讀性、可擴充套件性,還可以方便程式碼擴充套件。
#include <stdio.h>
#define NUM 100
#define ARR_LEN 10
void show_arr(int arr[], int len) {
for (int i = 0; i < len; ++i) {
printf("%d%c", arr[i], " \n"[i == len - 1]);
}
}
int main() {
int num = 100, len = 10;
printf("%d\n",NUM);
printf("%d\n",num);
int arr[ARR_LEN] = {1,12,3,4,5};
len = 100;
ARR_LEN = 100;
show_arr(arr,ARR_LEN);
}
宏表示式:
#define 宏名 表示式、操作、更復雜的識別符號
#include <stdio.h>
typedef struct Student {
int id;
char name[20];
char sex;
float score;
} Student;
#define STU_FORMAT "%d %s %c %f"
#define pf printf
#define sf scanf
#define YEAR_SEC 3600*24*365lu
int main() {
Student stu = {1001,"hehe",'w',88.8};
printf("請輸入學生資訊:");
sf(STU_FORMAT,&stu.id,stu.name,&stu.sex,&stu.score);
pf(STU_FORMAT,stu.id,stu.name,stu.sex,stu.score);
}
定義宏常量和宏表示式要注意的問題:
由於宏常量和宏表示式可能使用在表示式中,因此在定義宏常量和宏表示式的末尾不要加分號。
一般宏名全部大寫以作區分
(區域性變數全部小寫,全域性變數首字母大寫,迴圈變數i、j、k、函式名全部小寫+下劃線、陣列arr、字串str、指標p)
列舉常量與宏常量的區別:
提前:它們都可以提高程式碼的可讀性,給沒有意義字面值資料取一個有意義的名字。
1、從安全形度來說列舉常量要比宏常量安全,因為宏常量是透過替換實現的,在替換過程中可能會導致新的錯誤,如:有與宏名重名和函式或變數。
2、但列舉常量沒有宏常量方便,列舉常量只能是整型資料,而宏常量可以是任何型別的,甚至是複雜的表示式,宏常量的使用範圍更廣。
總結:如果是大量的整型字面值資料建議定義為列舉常量,如果少量的或整型以外的型別的字面值資料建議定義為宏常量。
預定義的宏:
編譯器預定義的宏:
__FILE__ 獲取當前檔名
__func__ 獲取當前函式名
__LINE__ 獲取當前行號
__DATE__ 獲取當前日期
__TIME__ 獲取當前時間
__WORDSIZE 獲取當前編譯器的位數
// 適合用來顯示警告、錯誤資訊。
#include <stdio.h>
int main() {
printf("%s\n",__FILE__);
printf("%s\n",__func__);
printf("%d\n",__LINE__);
printf("%s\n",__DATE__);
printf("%s\n",__TIME__);
printf("%d\n",__WORDSIZE);
}
標準庫預定義的宏:
// limits.h 標頭檔案中定義的所有整數型別最大值、最小值
#define SCHAR_MIN (-128)
#define SCHAR_MAX 127
#define UCHAR_MAX 255
#define SHRT_MIN (-32768)
#define SHRT_MAX 32767
#define USHRT_MAX 65535
#define INT_MIN (-INT_MAX - 1)
#define INT_MAX 2147483647
#define UINT_MAX 4294967295U
#define LLONG_MAX 9223372036854775807LL
#define LLONG_MIN (-LLONG_MAX - 1LL)
#define ULLONG_MAX 18446744073709551615ULL
PATH_MAX
// stdlib.h 標頭檔案定義兩個結標誌
#define EXIT_SUCCESS (0)
#define EXIT_FAILURE (-1)
// stdbool.h 標頭檔案定義了bool、true、false
#define bool _Bool
#define true 1
#define false 0
// libio.h 標頭檔案定義了NULL
#define NULL ((void*)0)
宏函式:
什麼是宏函式:
宏函式不是真正的函式,而是帶引數的宏替換,只是使用方法像函式而已。
在程式碼中使用宏函式,預處理時會經歷兩次替換,第一次把宏函式替換成它後面的一串程式碼、表示式,第二次把宏函式中的引數替換到表示式中。
#define 宏名(a,b,c,...) a+b*c
定義宏函式要注意的問題:
1、假如宏函式執行復雜的多條語句,可能會因為在if分支中缺少大括號而出現問題,可以使用大括號包括,進行保護,避免if的影響。
#define 宏名(a,b,c,...) {程式碼1; 程式碼2; ...}
2、、可以透過加大括號解決問題1,但是如果if後面有else,也會出現問題
3、因此linux核心和C++開源的程式碼中,經常會在宏定義中使用do-while(0)來保證程式碼安全,除此之外還可以起到:
在宏函式中定義同名變數、而不會衝突(語句塊定義)
還可以在解決程式碼冗餘問題上替換goto的效果
4、宏函式後面的程式碼不能直接換行,如果程式碼確定太長,可以使用續行符換行。
#define 宏名(a,b,c,...) { \
程式碼1; \
程式碼2; \
... \
}
#define 宏名(a,b,c,...) do { \
程式碼1; \
程式碼2; \
... \
} while(0)
5、為了防止宏函式出現二義性,對宏引數要儘量多加小括號。
二義性:就是使用宏函式的環境不同、引數不同,造成宏函式有多執行規則,會出現出乎意料的執行結果,這種宏函式的二義性,設計宏函式時要儘量杜絕。
呼叫宏函式要注意的問題:
1、傳遞給宏函式的引數不能使用自變運算子,因為我們無法知道引數在宏程式碼中會被替換多少次。
2、宏函式沒有返回值,只是個別宏函式表示式有計算結果。
普通函式與宏函式的優缺點?
宏函式的優點:
1、執行速度快,它不是真正的函式呼叫,而是程式碼替換,不會經歷傳參、跳轉、返回值。
2、不會檢查引數的型別,因此通用性強。
宏函式的缺點:
1、由於它不是真正的函式呼叫,而是程式碼替換,每使用一次,就會替換出一份程式碼,會造成程式碼冗餘、編譯速度慢、可執行檔案變大。
2、沒有返回值,最多可以有個執行結果。
3、型別檢查不嚴格,安全性低。
4、無法進行遞迴呼叫。
普通函式的優點:
1、不存在程式碼冗餘的情況,函式的程式碼只會在程式碼段中儲存一份,使用時跳轉過去執行,執行結束後再返回,還可以附加返回值。
2、安全性高,會對引數進行型別檢查。
3、可以進行遞迴呼叫,實現分治演算法。
函式的缺點:
1、相比宏函式它的執行速度慢,呼叫時會經歷傳參、跳轉、返回等過程,該過程耗費大量的時間。
2、型別專用,形參什麼型別,實參必須是什麼型別,無法通用。
什麼樣的程式碼適合封裝成宏函式?
1、程式碼量少,即使多次使用也不會造成程式碼段過度冗餘。
2、呼叫次數少,但執行次數多,也就是宏函式會在迴圈語句中呼叫。
3、函式的功能對返回值沒有要求,也就是函式的功能不是透過返回值達到的。
封裝一個malloc、free函式:
#include <stdio.h>
#include <stdlib.h>
void* _my_malloc(const char* filename, const char* func, size_t line, size_t size) {
void *ptr = malloc(size);
printf("%s %s %u %p\n", filename, func, line, ptr);
return ptr;
}
#define my_malloc(size) _my_malloc(__FILE__, __func__, __LINE__, size)
#define my_free(ptr) do {\
printf("%s %s %u %p\n", __FILE__, __func__, __LINE__, ptr);\
free(ptr);} while (0);
int main() {
int *p = my_malloc(40);
my_free(p);
}
實現一個通用的變數交換函式。
#include <stdio.h>
// 只適合數值型交換、資料可能溢位
#define SWAP(a, b) do {(a) = ((a) + (b)), (b) = ((a) - (b)); (a) = ((a) - (b));} while (0);
// 資料不溢位,只適合整型資料,並且不能是同一個值
#define SWAP(a, b) do {a = a ^ b; b = a ^ b; a = a ^ b;} while (0);
// 不能交換結構變數 浪費記憶體
#define SWAP(a, b) do {tong double t = a; a = b; b = t;} while (0);
// 可以交換任意型別,多一個引數
#define SWAP(a, b, type) do {type t = a; a = b; b = t;} while (0):
// 只能在GNU系列編譯器下使用
#define SWAP(a, b) \
do {typedef(a) (t) = (a); (a) = (b); (b) = (t);} while (0);
int main() {
}
條件編譯:
條件語句(if、switch、for、while、do while)會根據條件選擇執行哪些程式碼,條件編譯就是前處理器根據條件選擇哪些程式碼參與下一步的編譯。
負責條件編譯的預處理指令有:
#if #ifdef #ifndef #elif #else #endif
標頭檔案衛士:
這種固定寫法,在標頭檔案中使用,它能防止標頭檔案被重複包含,所有的標頭檔案都要遵循這個規則。
#ifndef FILE_H // 判斷FILE_H宏是否正在,不存在則條件為真
#define FILE_H // 定義FILE_H宏
// 標頭檔案衛士能保證此處不重複出現
#endif//FILE_H // #ifndef的結尾
註釋程式碼:
// 只能註釋單行程式碼,早期的編譯器不支援該用
/* 多行註釋,但不能巢狀 */
#if 0|1
可註釋大塊程式碼,可以巢狀
#endif
版本、環境判斷:
#if __WORDSIZE == 64
typedef long int int64_t;
#else
typedef long long int int64_t;
#endif
// 判斷是否是Linux作業系統:
#if __linux__
#endif
// 判斷是否是Windows作業系統:
#if __WIN32 | __WIN32__ | __WIN64__
#endif
// 判斷gcc還是g++:
int main() {
#if __cplusplus
printf("你使用是g++編譯器\n");
#else
printf("你使用是gcc編譯器\n");
#endif
}
DEBUG宏:
專門用於除錯程式的宏函式,這種宏函式在程式測試、除錯、試執行階段執行,在程式正式上線階段不執行,這類函式會根據DEBUG宏是否定義確定執行的流程。
一些操作提示,如:xxx操作成功,xxx操作失敗,分配記憶體的記錄、釋放記憶體的記錄,這型別訊息開發人員、測試人員需要看到,但使用者不需要看到。
不常用的預處理指令:
#line <常整數> 設定當前程式碼的行號,目前沒有發現它有什麼用
#error "在預處理階段提示錯誤資訊",一旦預處理遇到它,將不再繼續編譯,它不能單獨使用必須與條件判斷系列語句配合使用
#warning "在預處理階段提示警告資訊" 不能建議單獨使用,最好與條件判斷系列語句配合使用。
#pragma GCC poison <識別符號> 把識別符號設定病毒,禁止在程式碼中使用
#pragma pack(n) 設定最大對齊和補齊位元組數
每個系統在進行對齊和補齊都有一個最大對齊和補齊位元組數n,也就是超出n位元組按n位元組計算,例如:linux32系統n=4,windows32 n=8
設定要求:
1、n < 系統預設的最大對齊、補齊位元組數,往大了調整沒有意義,速度不會提升還會導致記憶體浪費。
2、n必須是2的x次方,也就是必須是1、2、4、8、16這一類的整數
宏函式的變長引數:
#define func(...) __VA_ARGS__
注意:這種用法必須配合,printf/fprintf/sprintf系列支援變長引數的函式使用。
在編譯時定義宏:
gcc xxx.c -D ARR_LEN=3
-D ARR_LEN=3 <=> #define ARR_LEN 3 跟在程式碼中定義宏的效果一樣
gcc xxx.c -D DEBUG
-D DEBUG <=> #define DEBUG