高階C語言7

sleeeeeping發表於2024-05-06

預處理:

程式設計師所編譯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

相關文章