c語言的常用的預處理指令和條件編譯
預處理詳解
最早接觸預處理,還是在一篇文章看懂c語言_如何看懂c語言代碼-CSDN博客中介紹了c語言中#define
定義的符號在編譯階段會被替換的行為。
這里將我所了解的預處理符號簡單做個收集。預處理指令肯定不止這些,曾經遇到過的:
#pragma pack()
也是預處理指令。
詳細參考《c語言深度剖析》。
預定義符號
__FILE__ //進行編譯的源文件
__LINE__ //文件當前的行號
__DATE__ //文件被編譯的日期
__TIME__ //文件被編譯的時間
__STDC__ //如果編譯器遵循ANSI C,其值為1,否則未定義
輸出這些符號可以知道代碼的信息。
#include <stdio.h>
#include <windows.h>int main()
{printf("%s\n%d\n%s\n%s\n", __FILE__, __LINE__, __DATE__, __TIME__);printf("%s", __STDC__);//vs下未定義,說明vs不是嚴格遵循ANSI C標準return 0;
}
在某一個的Devc++5.11的輸出:
D:\aDarkwanderor\_01ComputerLearn\_01C_Language_and_C++\_4CppProjectDebug\testC.c
6
Apr 25 2025
21:39:22
#define
#define 定義標識符
#define
的用法:
#define name stuff
定義階段會將name
替換成stuff
。stuff
可以是數字,可以是關鍵字,還可以是語句。
如果定義的 stuff
過長,可以分成幾行寫,除了最后一行外,每行的后面都加一個反斜杠(續行符)。
#define MAX 1000//為 register這個關鍵字,創建一個簡短的名字
#define reg register//用更形象的符號來替換一種實現
#define do_forever for(;;)//在寫case語句的時候自動把 break寫上
#define CASE break;case // 如果定義的 stuff過長,可以分成幾行寫,除了最后一行外,每行的后面都加一個反斜杠(續行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \date:%s\ttime:%s\n" ,\__FILE__,__LINE__ , \__DATE__,__TIME__ )
注意staff
加;
需要謹慎,因為符號用到的地方可能是某個語句的一部分。
#define 定義宏
#define
機制包括了一個規定,允許把參數替換到文本中,這種實現通常稱為宏(macro)或定義宏(define macro)。
下面是宏的申明方式:
#define name( parament-list ) stuff
其中的 parament-list
是一個由逗號隔開的符號表,它們可能出現在stuff
中。
注意:
-
參數列表的左括號必須與
name
緊鄰。 -
如果兩者之間有任何空白存在,參數列表就會被解釋為
stuff
的一部分。 -
對每個出現的參數和最終的宏,盡量用括號
()
括起來。
簡單使用宏:
#include <stdio.h>#define MUL(x) x*x
#define MUL2(x) (x)*(x)
#define ADD(x) (x)+(x)int main() {int a = 5;printf("%d\n", MUL(a));printf("%d\n", MUL(a + 1));printf("%d\n", MUL2(a + 1));printf("%d\n", ADD(a) * 6);return 0;
}
輸出:
25
11
36
35
但我們想要的預期輸出很明顯是{25,36,36,60}
,在MUL
和ADD
兩個宏上出現了問題。
將宏的符號替換(預編譯之后),并省略stdio.h
展開后的所有代碼,main
函數變成這個樣子:
int main() {int a = 5;printf("%d\n", a * a);printf("%d\n", a + 1 * a + 1);printf("%d\n", (a + 1) * (a + 1));printf("%d\n", a + a * 6);return 0;
}
第4行的宏替換后,因為*
的優先級高,故先計算1*a
,使得結果錯誤,第6行的宏也是如此。因此對每個出現的參數和最終的宏,盡量用括號()
括起來。
對#define
替換規則進行總結:
在程序中擴展#define
定義符號和宏時,需要涉及幾個步驟。
-
在調用宏時,首先對參數(或源碼)進行檢查,看看是否包含任何由
#define
定義的符號。如果是,它們首先被替換。 -
替換文本隨后被插入到程序中原來文本的位置。對于宏,參數名被他們的值所替換。
-
最后,再次對結果文件進行掃描,看看它是否包含任何由
#define
定義的符號。如果是,就重復1、2。
注意:
-
宏參數和
#define
定義中可以出現其他#define
定義的符號。但是對于宏,不能出現遞歸。 -
當預處理器搜索
#define
定義的符號的時候,字符串常量的內容并不被搜索。
帶副作用的宏參數
當宏參數在宏的定義中出現超過一次的時候,如果參數帶有副作用,那么你在使用這個宏的時候就可能
出現危險,導致不可預測的后果。副作用就是表達式求值的時候出現的永久性效果。
例如:
x+1;//不帶副作用
x++;//帶有副作用
這里的MAX
宏可以證明具有副作用的參數所引起的問題。
#include <stdio.h>#define MAX(a, b) ( (a) > (b) ? (a) : (b) )int main() {int x = 5,y = 8;int z = MAX(x++, y++);printf("x=%d y=%d z=%d\n", x, y, z);return 0;
}
輸出:
x=6 y=10 z=9
宏和函數的對比
宏通常被應用于執行簡單的運算,比如在兩個數中找出較大的一個:
#define MAX(a, b) ((a)>(b)?(a):(b))
不用函數來完成這個任務的原因有二:
-
用于調用函數和從函數返回的代碼可能比實際執行這個小型計算工作所需要的時間更多(就是函數費時,宏省時)。所以宏比函數在程序的規模和速度方面更勝一籌。
-
更為重要的是函數的參數必須聲明為特定的類型。
所以函數只能在類型合適的表達式上使用。反之這個宏可以適用于整形、長整型、浮點型等可以用于>
來比較的類型。即宏是類型無關的。
此外,宏有時候可以做函數做不到的事情。比如:宏的參數可以出現類型,但是函數做不到。
宏的缺點:和函數相比宏也有劣勢的地方:
-
每次使用宏的時候,一份宏定義的代碼將插入到程序中。除非宏比較短,否則可能大幅度增加程序的長度。
-
宏是沒法調試的。
-
宏由于類型無關,也就不夠嚴謹。
-
宏可能會帶來運算符優先級的問題,導致程容易出現錯。
-
宏無法實現遞歸。
-
參數可能被替換到宏體中的多個位置,所以帶有副作用的參數求值可能會產生不可預料的結果。
用一張表列出二者的差別:
對比項 | #define 定義宏 | 函數 |
---|---|---|
代碼長度 | 每次使用時,宏代碼都會被插入到程序中。除了非常小的宏之外,程序的長度會大幅度增長 | 函數代碼只出現于一個地方;每次使用這個函數時,都調用那個地方的同一份代碼 |
執行速度 | 更快 | 存在函數的調用和返回的額外開銷,所以相對慢一些 |
操作符優先級 | 宏參數的求值是在所有周圍表達式的上下文環境里,除非加上括號,否則鄰近操作符的優先級可能會產生不可預料的后果,所以建議宏在書寫的時候多些括號。 | 函數參數只在函數調用的時候求值一次,它的結果值傳遞給函數。表達式的求值結果更容易預測。 |
帶有副作用的參數 | 參數可能被替換到宏體中的多個位置,所以帶有副作用的參數求值可能會產生不可預料的結果。 | 函數參數只在傳參的時候求值一次,結果更容易控制。 |
參數類型 | 宏的參數與類型無關,只要對參數的操作是合法的,它就可以使用于任何參數類型。 | 函數的參數是與類型有關的,如果參數的類型不同,就需要不同的函數,即使他們執行的任務是相同的。 |
調試 | 宏是不方便調試的 | 函數是可以逐語句調試的 |
遞歸 | 宏是不能遞歸的 | 函數是可以遞歸的 |
#define命名約定和#undef移除宏
函數的宏的使用語法很相似。所以語言本身沒法幫我們區分二者。那我們平時的一個習慣是:
-
把宏名全部大寫。
-
函數名不要全部大寫。
此外如果現存的一個名字需要被重新定義,那么它的舊名字首先要被移除。可以用#undef
移除宏。
#define NAME stuff
//...
#undef NAME
# 和 ## 參數插入字符串
字符串的自動連接
案例:
#include <stdio.h>int main() {char* p = "hello ""world\n";char* p2 = "hello"\" wor"\"ld\n";printf("hello"" world\n");printf("%s", p);printf("%s", p2);return 0;
}
輸出:
hello world
hello world
hello world
這說明,字符串是有自動連接的特點的。
因此可以利用這個特點繼續實現表現更豐富的宏。
#宏參數
在這之前先介紹#宏參數
,這里的#宏參數
是指把宏參數轉換成字符串。
例如:
#include <stdio.h>#define STR(x) #xint main() {printf(STR(aasfsdgsg));return 0;
}
輸出:
aasfsdgsg
因此利用#宏參數
和字符串的自動連接特性,完善的宏如下:
#include <stdio.h>#define PRINT(FORMAT, VALUE) \printf("the value is "FORMAT"\n", VALUE)#define PRINT2(FORMAT, VALUE) \printf("the value of " #VALUE " is "FORMAT "\n", VALUE);int main() {int i = 10;PRINT("%d", 10);//利用字符串的自動連接特性PRINT2("%d", i + 3);//利用#宏參數和字符串的自動連接特性return 0;
}
輸出:
the value is 10
the value of i + 3 is 13
##
可以把位于它兩邊的符號合成一個符號。它允許宏定義從分離的文本片段創建標識符。
例如:
#include <stdio.h>#define CAT(x,y) x##yint main() {int a = 2025;int b = CAT(a, -3);//將a和-3連接在一起變成a-3printf("%d", b);return 0;
}
這樣的連接必須產生一個合法的標識符。否則其結果就是未定義的。
命令行定義
許多C 的編譯器提供了一種能力,允許在命令行中定義符號。用于啟動編譯過程。
命令行是一種通過輸入文本命令來與計算機系統(特別是操作系統)進行交互的界面,它與圖形用戶界面(GUI)相對應,具有高效、靈活和強大的特點,在系統管理、軟件開發等領域應用廣泛。
c語言代碼通過編譯最終變成的可執行程序,只要滿足條件是可以直接運行在操作系統上的。從代碼到可執行程序,在一些編譯器可以通過輸入命令干涉編譯過程。
當我們根據同一個源文件要編譯出一個程序的不同版本的時候,這個特性有點用處。
舉個例子,假定某個程序中聲明了一個某個長度的數組,如果機器內存有限,我們需要一個很小的數組,但是另外一個機器內存大些,我們需要一個數組能夠大些。
例如這個代碼:
#include <stdio.h>
int main()
{int array[ARRAY_SIZE];int i = 0;for (i = 0; i < ARRAY_SIZE; i++){array[i] = i;}for (i = 0; i < ARRAY_SIZE; i++){printf("%d ", array[i]);}printf("\n");return 0;
}
在封裝嚴密的 IDE 比如Devc++、vs2019等,這個代碼可能直接報錯。
但如果是在vscode,則可以通過命令行在編譯階段指定ARRAY_SIZE
變成我們想要的數值,從而生成不同功能的程序。
例如通過命令gcc -D ARRAY_SIZE=10 testc.c
可以指定ARRAY_SIZE
變成指定數值10,然后生成可執行程序 a.exe 。再輸入.\a.exe
即可執行它。
也可在原命令的基礎上加-o ProgramName.exe
來指定生成的可執行程序的程序名。這里讓生成的可執行程序名和c語言的代碼名保持一致。
條件編譯
在編譯一個程序的時候若要將一條語句(一組語句)編譯或者放棄參與編譯,可通過條件
編譯指令實現。
這些調試性的代碼,刪除可惜,保留又礙事,所以我們可以選擇性的編譯。
常見的條件編譯指令如下。
#if和#endif
#if 常量表達式//...
#endif
常量表達式由預處理器求值。
例如:
#include <stdio.h>
int main()
{
#if 0printf("asdfghjkl\n");
#endifprintf("qwertyyiop");return 0;
}
輸出:
qwertyyiop
在模塊化編程中的某個頭文件用#define
定義某個符號,這個符號也能通過#if
進行條件編譯。
例如,這里將#define
和c語言代碼放在一起:
#include <stdio.h>#define ABC 1int main()
{
#if ABCprintf("asdfghjkl\n");
#endifprintf("qwertyyiop");return 0;
}
輸出:
asdfghjkl
qwertyyiop
多分支條件編譯#if、#elif、#else和#endif
#if
實現多分支編譯的用法:
#if 常量表達式1//...
#elif 常量表達式2//...
#else//...
#endif
其中#elif
不可放在#else
之后,一般#else
是作為所有常量表達式都不滿足的情況下,作為最后的選擇。
例如:
#include <stdio.h>#define A 1
#define B 2
#define C 3void f1() {
#if A==1&&B!=2printf("1\n");
#elif A==1&&B==2printf("2\n");
#endif
}void f2() {
#if A==1&&B!=2printf("1\n");
#elseprintf("2\n");
#endif
}void f3() {
#if A==1&&B!=2printf("1\n");
#elif A==1&&B==2&&C!=3printf("2\n");
#elseprintf("3\n");
#endif
}int main()
{//f1();//f2();f3();printf("six");return 0;
}
#ifdef和#ifndef 判斷某符號是否定義
若定義了某個符號就執行相關語句:
#if defined(symbol)
//...
#endif#ifdef symbol
//...
#endif
若沒定義某個符號就執行相關與:
#if !defined(symbol)
//...
#endif#ifndef symbol
//...
#endif
其中symbol
表示某一標識符,這個標識符通常由#define
預定義。
一般更喜歡用#ifdef
和#ifndef
,因為可以少敲一個單詞。
例如,#ifdef
的用法:
#include<stdio.h>#define A 1int main()
{
#ifdef Aprintf("A\n");
#endif
#ifdef Bprintf("B\n");
#endifprintf("six");return 0;
}
#ifndef
:
#ifndef _CRT_SECURE_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS 1
#endif#include <stdio.h>int main()
{int a;scanf("%d", &a);//在vs定義了_CRT_SECURE_NO_WARNINGS,才能正常使用scanf和其他別的函數printf("%d", a);return 0;
}
一般#ifndef
和#ifdef
通常用于避免頭文件重復包含。
避免頭文件包含還可以在頭文件開頭加這樣一句:
#pragma once