C語言:預處理
- 預定義符號
- #define
- 定義常量
- 定義宏
- 宏與函數對比
- #操作符
- ##操作符
- 條件編譯
- 頭文件包含
- 庫文件包含
- 本地文件包含
- 嵌套文件包含
預定義符號
C語?設置了?些預定義符號,可以直接使?,預定義符號也是在預處理期間處理的。
__FILE__ //進?編譯的源?件
__LINE__ //?件當前的?號
__DATE__ //?件被編譯的?期
__TIME__ //?件被編譯的時間
__STDC__ //如果編譯器遵循ANSI C,其值為1,否則未定義
示例:
printf("file:%s line:%d\n", __FILE__, __LINE__);
輸出結果:
file:test.c line:1
以上預定義符號,都是一些常量,可以自己一一嘗試。
#define
在C語言中,#define
是一個預處理器指令,用于定義宏。
宏是一個被預處理器替換的標識符或一個標識符和參數的組合。宏定義可以用來簡化代碼、提高代碼的可讀性和可維護性。
使用#define
可以定義常量、函數宏等。
定義常量
- 定義常量:
#define PI 3.14
這樣就可以在代碼中使用PI
來代表3.14
。
- 定義關鍵字:
#define reg register
將 reg
定義為關鍵字 register
,可以將reg
這個簡寫代替register
關鍵字使用。
- 定義代碼段
#define CASE break;case
正常的switch
語句每一個case
都要加上break
,通過這個寫法,我們可以在寫CASE
時自動補齊break
。
定義宏
#define
機制允許把參數替換到文本中,這種功能叫做 宏(macro) / 定義宏(define macro)
語法:
#define name(parament-list) stuff
其中的 parament-list
是?個由逗號隔開的符號表,它們可能出現在stuff
中。
示例:
#define SQUARE(x) x * x
當我們在代碼中輸入以下代碼:
int main()
{int a = 5;int b = SQUARE(a);return 0;
}
在編譯后就會轉化為:
int main()
{int a = 5;int b = a * a;return 0;
}
也就是直接發生了文本替換,這種宏的形式非常像函數,因此也可以稱為宏函數。但是其也有很多需要注意的地方。
比如以下代碼:
int a = 5;
int b = SQUARE(a + 1);
我們希望先執行a + 1
,然后再傳入SQUARE
中,但是其不會這樣做因為其會將上述代碼直接替換為:
int a = 5;
int b = a + 1 * a + 1;
由于操作符優先級的問題,我們不會得到想要的結果。為了處理這個情況,我們需要把參數用小括號括起來:
#define SQUARE(x) (x) * (x)
代碼就變成:
int a = 5;
int b = (a + 1) * (a + 1);
這樣我們就可以行使預期的功能了。
那么我們再看到一串代碼:
#define DOUBLE(x) (x) + (x)int a = 5;
int b = 10 * DOUBLE(a);
代碼編譯后為:
int a = 5;
int b = 10 * 5 + 5;
又出現了一樣的問題,我們的 10 * DOUBLE(a)
并沒有先執行DOUBLE(a)
,而展開后,又出現了操作符優先級問題,所以我們的宏還要再優化:
#define DOUBLE(x) ((x) + (x))
在宏的最外側再加一層括號,就可以獨立運行,不受外界操作符影響了。
通過以上推斷,我們可以發現,宏雖然可以很好的替換代碼,但是會受到外界操作符的影響,此時就要注意很多細節。
宏與函數對比
函數在調用時,是會開辟內存創建棧幀的,而宏則直接執行,所以速度更快。但是由于宏是在編譯階段就已經處理好了,所以宏不能通過調試觀察現象,還要操作符優先級帶來的種種問題。因此宏不適合處理復雜的函數,但是很適合短小簡單的函數。
#操作符
功能:
#
可以將宏的參數轉化為字符串
比如以下代碼:
#define PRINT(n) printf("the value of "#n " is %d", n);
我們嘗試調用:
int a = 5;
PRINT(a);
代碼就會被轉化為:
int a = 5;
printf("the value of ""a" " is %d", a);
可以看到,兩個n
的替換效果是不同的,對于n
其會直接被替換為變量a
;而對于#n
,其不是簡單的替換,而是把參數名轉化為了字符串”a“
。
##操作符
##
操縱符可以將兩個符號合并為一個符號
示例:
#define GENERIC_MAX(type) \
type type##_max(type x, type y)\
{ \
return (x>y?x:y); \
}
該宏用于創建不同類型的比大小函數,由于不同函數需要不同的函數名,于是利用##
來連接函數名,也就是type##_max
部分。type
是一個宏參數,當傳入float
,type##_max
整體就被連接為float_max
,當傳入int
,整體就被連接為int_max
。也就是##
起到一個連接作用。
條件編譯
條件編譯是C語言中的一種編譯指令,用于在編譯過程中根據指定的條件選擇性地包含或排除某些代碼。它主要是為了滿足不同平臺、不同編譯選項或不同場景下的需求。
條件編譯使用預處理指令實現,預處理指令以#開頭。下面是一些常用的條件編譯指令及其用法:
#if
/#elif
/#else /
#endif
#if
用于基于預處理器常量的值進行條件判斷。
#elif
用于在多個條件之間進行選擇。
#else
用于在沒有匹配的#if
或#elif
時執行。
#endif
用于結束條件編譯塊。
示例:
#define NUM 5#if NUM > 10printf("NUM is greater than 10\n");#elif NUM > 0printf("NUM is greater than 0\n");#elseprintf("NUM is less than or equal to 0\n");#endif
這個代碼和C語言的if
代碼很像,不過多講解了。
#ifdef
/#ifndef
#ifdef
用于檢查一個標識符是否已經定義,如果已定義則編譯后面的代碼,否則跳過。
#ifndef
與#ifdef
相反,用于檢查一個標識符是否未定義。
示例:
#ifdef DEBUGprintf("Debug mode enabled\n");
#endif
以上代碼中,只要我們定義了DEBUG
這個變量,就會輸出"Debug mode enabled\n"
語句。
-
#define
#define
用于定義宏。宏是一種將一組指令作為一個整體進行替換的方式。
示例:#define MAX(a, b) ((a) > (b) ? (a) : (b))int x = 10; int y = 20; int max = MAX(x, y);
-
#include
#include
用于將指定的頭文件包含到當前文件中。
示例:#include <stdio.h>int main() {printf("Hello, World!\n");return 0; }
-
#pragma
#pragma
用于向編譯器發出特定的指令,如優化選項、警告控制等。它的語法和功能因編譯器而異。
示例:#pragma warning(disable: 4996)
這些是C語言中常用的條件編譯指令和代碼用法。通過合理使用條件編譯,我們可以根據不同的需求自由地控制代碼的編譯過程。
頭文件包含
頭文件包含分兩種形式:本地頭文件與庫文件。
庫文件包含
語法:
#include <filename.h>
查找頭?件會直接去標準路徑下去查找,如果找不到就提?編譯錯誤
我們平常使用的庫文件都通過尖括號<>
來包含,其會直接到存放庫文件的路徑中查找。
本地文件包含
#include "filename"
先在源?件所在?錄下查找,如果該頭?件未找到,編譯器就像查找庫函數頭?件?樣在標準位置查找頭?件,如果找不到就提?編譯錯誤
如果是用戶自己編寫的頭文件,我們要用雙引號""
包含,如果通過這種方式包含頭文件,那么會先在當前源文件的目錄下查找,如果沒有找到,再去庫文件中查找。
也就是說:庫文件也可以通過雙引號包含,但是會多出額外的查找步驟,所以庫文件還是用尖括號包含更好,而自己編寫的頭文件必須雙引號包含。
嵌套文件包含
假設我們現在有以下文件結構:
頭文件test.h
:
void test();
struct Stu
{int id;char name[20];
};
源文件test.c
:
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
int main()
{return 0;
}
我們在test.c
中多次包含了頭文件,這就會導致test.h
反復被展開,產生大量重復代碼。這就是嵌套文件包含的問題,那么我們要如何處理這個問題,讓其只能被包含一次呢?
條件編譯方法:
在頭文件test.h
中加入以下代碼:
#ifndef __TEST_H__
#define __TEST_H__
//頭?件的內容
#endif //__TEST_H__
第一次包含頭文件:
先執行#ifndef __TEST_H__
,我們此時沒有定義__TEST_H__
這個變量,if
成立,此時頭文件會被展開,同時執行#define __TEST_H__
,此時__TEST_H__
就已經被定義了
第二次包含頭文件:
第二次站時,由于上一次展開已經定義了__TEST_H__
這個變量,導致#ifndef __TEST_H__
判斷為假,此時整個頭文件都不會再被編譯,直接舍棄
后續再展開頭文件,都會因為 __TEST_H__
被定義而不會編譯,解決了嵌套編譯的問題。
一般而言,我們這個用于判斷頭文件有沒有被展開過的變量,是頭文件名通過一定規則轉化來的:
- 在頭文件前后加上兩個下劃線
__頭文件.h__
- 把頭文件中的點
.
也改為下劃線__頭文件_h__
因此test.h
的常量就是:__TEST_H__
。
pragma:
通過條件編譯其實是比較傳統的寫法,我們還有一種更加簡潔方便的寫法:
#pragma once
只要在任何頭文件前面加上這句話,就只會被編譯一次了。