C 程式設計/預處理器指令和宏
預處理器是在 C 程式實際編譯之前對文字進行處理的一種方式。在每個 C 程式實際編譯之前,它都會經過預處理器。預處理器會檢視程式,試圖找出它可以理解的特定指令,稱為預處理器指令。所有預處理器指令都以 #(井號)符號開頭。C++ 編譯器使用相同的 C 預處理器。[1]
預處理器是編譯器的一部分,在編譯器看到程式碼之前,它對程式碼執行預處理操作(條件編譯程式碼、包含檔案等)。這些轉換是詞法上的,這意味著預處理器的輸出仍然是文字。
|
注意:從技術上講,C 預處理階段的輸出是一系列標記,而不是原始碼文字,但很容易輸出等效於給定標記序列的原始碼文字,並且這通常由編譯器透過以下方式支援:-E或/E選項——雖然 C 編譯器的命令列選項並不完全是標準的,但許多編譯器遵循類似的規則。 |
指令是針對預處理器(預處理器指令)或編譯器(編譯器指令)的特殊指令,指示它如何處理部分或全部原始碼或在最終目標檔案上設定一些標誌,它們用於使編寫原始碼更容易(例如更便攜)並使原始碼更易理解。指令由預處理器處理,預處理器要麼是編譯器呼叫的單獨程式,要麼是編譯器本身的一部分。
C 語言包含一些作為語言一部分的功能,還包含一些作為標準庫一部分的功能,標準庫是與每個符合標準的 C 編譯器一起提供的程式碼庫。當 C 編譯器編譯程式時,它通常也會將程式與標準 C 庫連結。例如,在遇到#include <stdio.h>指令時,它會用stdio.h標頭檔案的內容替換該指令。stdio.h標頭檔案。
當您使用庫中的功能時,C 要求您宣告要使用的內容。程式中的第一行是預處理指令,它應該如下所示:
#include <stdio.h>
上面的程式碼行會導致 C 宣告(在stdio.h 標頭檔案中)被包含在您的程式中以便使用。通常,這是透過將標頭檔案稱為stdio.h的內容插入到您的程式中來實現的,該檔案位於系統相關的位置。此類檔案的位置可能在您的編譯器文件中描述。標準 C 標頭檔案的列表在下面的“標頭檔案”表中列出。
stdio.hstdio.h標頭檔案包含用於使用稱為流的 I/O 機制抽象的輸入/輸出 (I/O) 的各種宣告。例如,有一個名為stdout的輸出流物件,它用於將文字輸出到標準輸出,通常將文字顯示在計算機螢幕上。
如果使用與上面的示例類似的尖括號,則預處理器被指示沿著標準包含的開發環境路徑搜尋包含檔案。
#include "other.h"
如果您使用引號(" "),則預處理器預計會在一些額外的、通常是使用者定義的位置中搜索標頭檔案,並且僅當它在這些額外的位置中找不到時才會回退到標準包含路徑。這種形式通常包括在包含#include指令的檔案所在的相同目錄中進行搜尋。
|
注意:您應該檢查正在使用的開發環境的文件以瞭解#include指令的檔案所在的相同目錄中進行搜尋。 |
C90 標準標頭檔案列表
自 C90 以來新增的標頭檔案
pragma(實用資訊)指令是標準的一部分,但任何 pragma 的含義都取決於所使用的標準的軟體實現。#pragma 指令提供了一種從編譯器請求特殊行為的方法。對於非常大的程式或需要利用特定編譯器功能的程式,此指令非常有用。
Pragma 用於源程式中。
#pragma token(s)
- pragma 通常後跟一個標記,該標記表示要編譯器執行的命令。您應該檢查打算使用的 C 標準的軟體實現以獲取支援的標記列表。不出所料,可以在 #pragma 指令中出現的命令集對於每個編譯器都是不同的;您必須查閱編譯器的文件以瞭解它允許哪些命令以及這些命令的作用。
例如,最常實現的預處理器指令之一是 #pragma once,當它放在標頭檔案開頭時,表示如果預處理器多次包含它所在的程式碼,則會跳過該檔案。
|
注意:存在其他方法來執行此操作,通常被稱為使用包含守衛。 |
每個 #define 預處理器指令都定義一個宏。例如,
#define PI 3.14159265358979323846 /* pi */
以名稱後緊跟空格定義的宏稱為常量或文字。以名稱後緊跟括號定義的宏稱為函式式宏。[2]
|
警告:雖然預處理器宏很誘人,但如果使用不當,可能會產生意想不到的結果。請始終牢記,宏是在編譯任何內容之前對原始碼進行的文字替換。編譯器不知道宏,也永遠不會看到它們。這可能會導致難以理解的錯誤,以及其他負面影響。如果存在等效功能,請優先使用語言功能。例如,使用 也就是說,在某些情況下,宏非常有用(請參閱下面的 |
stdio.h#define指令用於定義宏。宏由預處理器在編譯之前用於操作程式原始碼。由於預處理器宏定義是在編譯器對原始碼進行操作之前進行替換的,因此由#define引入的任何錯誤都很難追蹤。
按照慣例,使用#define定義的宏以大寫字母命名。雖然這樣做不是必需的,但被認為是一種非常不好的做法。這使得在閱讀原始碼時可以輕鬆識別宏。(我們在後面的章節中提到了使用 #define 的許多其他常見約定,C 程式設計/常見做法)。
今天,#define主要用於處理編譯器和平臺差異。例如,一個定義可能包含一個常量,它是系統呼叫的適當錯誤程式碼。使用#define因此,除非絕對必要,否則應該限制使用;typedef語句和常量變數通常可以更安全地執行相同的功能。
的另一個特點是#define命令是可以接受引數的,這使得它作為偽函式建立器非常有用。考慮以下程式碼
#define ABSOLUTE_VALUE( x ) ( ((x) < 0) ? -(x) : (x) )
...
int x = -1;
while( ABSOLUTE_VALUE( x ) ) {
...
}
在使用複雜宏時,通常最好使用額外的括號。注意,在上面的例子中,變數“x”始終在其自己的括號內。這樣,它將被完整地計算,然後再與 0 進行比較或乘以 -1。此外,整個宏都被括號包圍,以防止它被其他程式碼汙染。如果不注意,就有可能導致編譯器錯誤地解釋您的程式碼。
由於存在副作用,使用上面描述的宏函式被認為是一個非常糟糕的做法。
int x = -10; int y = ABSOLUTE_VALUE( x++ );
如果 ABSOLUTE_VALUE() 是一個真正的函式,“x”現在將具有“-9”的值,但因為它是在宏中作為引數傳遞的,所以它被擴充套件了兩次,因此具有 -8 的值。
|
示例 為了說明宏的危險性,請考慮這個簡單的宏 #define MAX(a,b) a>b?a:b 以及程式碼 i = MAX(2,3)+5; j = MAX(3,2)+5; 仔細看看並考慮執行後的值可能是什麼。語句被轉換為 int i = 2>3?2:3+5; int j = 3>2?3:2+5; 因此,執行後i=8和j=3而不是預期的結果i=j=8!這就是為什麼建議您在上面使用額外的括號的原因,但即使有這些括號,這條路也充滿了危險。警覺的讀者可能會很快意識到,如果 #define MAX(a,b) ((a)>(b)?(a):(b)) 這可行,前提是a,b沒有副作用。實際上, i = 2; j = 3; k = MAX(i++, j++); 將導致k=4, i=3和j=5。這對於任何期望的人來說都是非常令人驚訝的MAX()表現得像一個函式。 那麼正確的解決方案是什麼?解決方案是根本不使用宏。一個全域性的行內函數,像這樣 inline int max(int a, int b) {
return a>b?a:b
}
沒有上述任何陷阱,但不能與所有型別一起使用。
|
(#, ##)
# 和 ## 運算子與#define宏一起使用。使用 # 會導致 # 後的第一個引數以引號內的字串形式返回。例如,命令
#define as_string( s ) # s
將使編譯器將此命令
puts( as_string( Hello World! ) ) ;
變成
puts( "Hello World!" );
使用 ## 將 ## 之前的部分與之後的部分連線起來。例如,命令
#define concatenate( x, y ) x ## y ... int xy = 10; ...
將使編譯器將
printf( "%d", concatenate( x, y ));
變成
printf( "%d", xy);
轉換為,當然,將顯示10到標準輸出。
可以將宏引數與常量字首或字尾連線起來,以獲得有效的識別符號,例如
#define make_function( name ) int my_ ## name (int foo) {}
make_function( bar )
這將定義一個名為my_bar()的函式。但不能使用連線運算子將宏引數整合到常量字串中。為了獲得這樣的效果,可以使用 ANSI C 屬性,該屬性認為兩個或多個連續的字串常量在遇到時等效於單個字串常量。使用此屬性,可以編寫
#define eat( what ) puts( "I'm eating " #what " today." ) eat( fruit )
宏處理器將將其轉換為
puts( "I'm eating " "fruit" " today." )
這反過來將被 C 解析器解釋為單個字串常量。
以下技巧可用於將數字常量轉換為字串文字
#define num2str(x) str(x) #define str(x) #x #define CONST 23 puts(num2str(CONST));
這有點棘手,因為它分兩步展開。首先,num2str(CONST) 被替換為str(23),這反過來被替換為"23"。這在以下示例中很有用
#ifdef DEBUG #define debug(msg) fputs(__FILE__ ":" num2str(__LINE__) " - " msg, stderr) #else #define debug(msg) #endif
這將為您提供一個不錯的除錯訊息,包括髮出訊息的檔案和行。但是,如果未定義 DEBUG,則除錯訊息將完全從程式碼中消失。小心不要將這種結構與任何有副作用的東西一起使用,因為這會導致錯誤,這些錯誤會根據編譯引數出現和消失。
宏
[edit | edit source]宏不進行型別檢查,因此它們不評估引數。此外,它們沒有正確地遵守作用域,而是簡單地獲取傳遞給它們的字串,並將宏文字中宏引數的每次出現替換為該引數的實際字串(程式碼實際上被複制到它被呼叫的位置)。
有關如何使用宏的示例
#include <stdio.h>
#define SLICES 8
#define ADD(x) ( (x) / SLICES )
int main(void)
{
int a = 0, b = 10, c = 6;
a = ADD(b + c);
printf("%d\n", a);
return 0;
}
-- “a” 的結果應為“2”(b + c = 16 -> 傳遞給 ADD -> 16 / SLICES -> 結果為“2”)
|
注意 只有在無法透過函式或其他機制實現相同結果時,才應定義宏。一些編譯器能夠最佳化程式碼,以將對小函式的呼叫替換為內聯程式碼,從而消除了任何可能的加速優勢。使用 typedef、列舉和inline(在 C99 中)通常是一個更好的選擇。 |
行內函數無法正常工作的情況之一(因此您幾乎被迫使用類似函式的宏)是初始化編譯時常量(結構的靜態初始化)。當宏的引數是編譯器可以最佳化為另一個文字的文字時,就會發生這種情況。 [3]
#error
[edit | edit source]#error 指令停止編譯。當遇到一個 #error 指令時,標準規定編譯器應該發出包含指令中剩餘標記的診斷資訊。這主要用於除錯目的。
程式設計師在條件塊內使用 "#error",以便在 "#if" 或 "#ifdef"(在塊的開頭)檢測到編譯時問題時立即停止編譯器。通常,編譯器會跳過塊(以及其中的 "#error" 指令),編譯會繼續進行。
#error message
#warning
[edit | edit source]許多編譯器支援 #warning 指令。當遇到一個 #warning 指令時,編譯器會發出包含指令中剩餘標記的診斷資訊。
#warning message
#undef
[edit | edit source]#undef 指令取消定義宏。識別符號不必先前已定義。
#if,#else,#elif,#endif (條件語句)
[edit | edit source]#if 命令檢查控制條件表示式是否計算為零或非零,並分別排除或包含程式碼塊。例如
#if 1
/* This block will be included */
#endif
#if 0
/* This block will not be included */
#endif
條件表示式可以包含任何 C 運算子,除了賦值運算子、遞增和遞減運算子、取地址運算子和 sizeof 運算子。
預處理中唯一使用的運算子是 defined 運算子。如果宏名稱(可選地括在括號中)當前已定義,則它返回 1;否則返回 0。
#endif 命令結束由 #if、#ifdef 或 #ifndef 啟動的塊。
#elif 命令類似於 #if,但它用於從一系列程式碼塊中提取一個。例如
#if /* some expression */
:
:
:
#elif /* another expression */
:
/* imagine many more #elifs here ... */
:
#else
/* The optional #else block is selected if none of the previous #if or
#elif blocks are selected */
:
:
#endif /* The end of the #if block */
#ifdef,#ifndef
[edit | edit source]#ifdef 命令類似於 #if,但如果宏名稱已定義,則選擇其後的程式碼塊。在這方面,
#ifdef NAME
等效於
#if defined NAME
#ifndef 命令類似於 #ifdef,但測試結果相反
#ifndef NAME
等效於
#if !defined NAME
此預處理指令用於將緊隨指令的行的檔名和行號設定為新值。這用於設定 __FILE__ 和 __LINE__ 宏。
ANSI C 定義了一些有用的預處理宏和變數,[4][5] 也稱為“神奇常量”,包括
__FILE__ => 當前檔案的名稱,作為字串字面量
__LINE__ => 原始檔的當前行號,作為數字字面量
__DATE__ => 當前系統日期,作為字串
__TIME__ => 當前系統時間,作為字串
__TIMESTAMP__ => 日期和時間(非標準)
__cplusplus => 當您的 C 程式碼被 C 編譯器編譯時未定義;當您的 C 程式碼被符合 1998 C++ 標準的 C++ 編譯器編譯時為 199711L。
__func__ => 原始檔的當前函式名稱,作為字串(C99 的一部分)
__PRETTY_FUNCTION__ => “修飾的”原始檔的當前函式名稱,作為字串(在 GCC 中;非標準)
編譯時斷言可以幫助您比僅使用執行時 assert() 語句更快地除錯,因為編譯時斷言都在編譯時進行測試,而程式的測試執行可能無法執行某些執行時 assert() 語句。
在 C11 標準之前,有些人[6][7][8] 定義了一個預處理宏來允許編譯時斷言,類似於
#define COMPILE_TIME_ASSERT(pred) switch(0){case 0:case pred:;}
COMPILE_TIME_ASSERT( BOOLEAN CONDITION );
static_assert.hpp Boost 庫 定義了一個類似的宏。[9]
從 C11 開始,這些宏已經過時,因為 _Static_assert 及其宏等效項 static_assert 已標準化並內置於語言中。
| 一位華夏公益教科書作者建議將本書或章節合併到C 程式設計/序列化#X-宏中。 請在 討論頁面 上討論是否應該進行此合併。 |
C 預處理器的另一個鮮為人知的用法模式被稱為“X-宏”。[10][11][12][13] X-宏是一個 標頭檔案 或宏。通常它們使用副檔名“.def”而不是傳統的“.h”。該檔案包含一個類似宏呼叫的列表,可以稱為“元件宏”。然後,該包含檔案在以下模式中被反覆引用。這裡,包含檔案是“xmacro.def”,它包含一個樣式為“foo(x, y, z)”的元件宏列表。
#define foo(x, y, z) doSomethingWith(x, y, z);
#include "xmacro.def"
#undef foo
#define foo(x, y, z) doSomethingElseWith(x, y, z);
#include "xmacro.def"
#undef foo
(etc...)
X-宏最常見的用法是建立一個 C 物件列表,然後自動為每個物件生成程式碼。一些實現還執行它們在 X-宏內部所需的任何 #undef,而不是期望呼叫者取消定義它們。
常見的物件集是一組全域性配置設定、一組 結構 成員、用於將 XML 檔案轉換為快速遍歷樹的可能的 XML 標籤列表或 列舉 宣告的主體;其他列表也是可能的。
一旦 X-宏已被處理以建立物件列表,就可以重新定義元件宏來生成例如 訪問器和/或修改器 函式。結構 序列化和反序列化 也通常完成。
以下是一個 X-宏示例,它建立一個結構並自動建立序列化/反序列化函式。為了簡單起見,此示例不考慮位元組序或緩衝區溢位。
檔案 star.def
EXPAND_EXPAND_STAR_MEMBER(x, int)
EXPAND_EXPAND_STAR_MEMBER(y, int)
EXPAND_EXPAND_STAR_MEMBER(z, int)
EXPAND_EXPAND_STAR_MEMBER(radius, double)
#undef EXPAND_EXPAND_STAR_MEMBER
檔案 star_table.c
typedef struct {
#define EXPAND_EXPAND_STAR_MEMBER(member, type) type member;
#include "star.def"
} starStruct;
void serialize_star(const starStruct *const star, unsigned char *buffer) {
#define EXPAND_EXPAND_STAR_MEMBER(member, type) \
memcpy(buffer, &(star->member), sizeof(star->member)); \
buffer += sizeof(star->member);
#include "star.def"
}
void deserialize_star(starStruct *const star, const unsigned char *buffer) {
#define EXPAND_EXPAND_STAR_MEMBER(member, type) \
memcpy(&(star->member), buffer, sizeof(star->member)); \
buffer += sizeof(star->member);
#include "star.def"
}
可以使用令牌串聯(“##”)和引用(“#”)運算子建立和訪問單個數據型別的處理程式。例如,以下內容可以新增到上面的程式碼中
#define print_int(val) printf("%d", val)
#define print_double(val) printf("%g", val)
void print_star(const starStruct *const star) {
/* print_##type will be replaced with print_int or print_double */
#define EXPAND_EXPAND_STAR_MEMBER(member, type) \
printf("%s: ", #member); \
print_##type(star->member); \
printf("\n");
#include "star.def"
}
請注意,在此示例中,您還可以透過為每個受支援的型別定義列印格式來避免為每個資料型別建立單獨的處理程式函式,這也有助於減少此標頭檔案生成的擴充套件程式碼
#define FORMAT_(type) FORMAT_##type
#define FORMAT_int "%d"
#define FORMAT_double "%g"
void print_star(const starStruct *const star) {
/* FORMAT_(type) will be replaced with FORMAT_int or FORMAT_double */
#define EXPAND_EXPAND_STAR_MEMBER(member, type) \
printf("%s: " FORMAT_(type) "\n", #member, star->member);
#include "star.def"
}
可以透過建立一個包含檔案內容的單個宏來避免建立單獨的標頭檔案。例如,上面的檔案“star.def”可以用此宏替換,位於…的開頭
檔案 star_table.c
#define EXPAND_STAR \
EXPAND_STAR_MEMBER(x, int) \
EXPAND_STAR_MEMBER(y, int) \
EXPAND_STAR_MEMBER(z, int) \
EXPAND_STAR_MEMBER(radius, double)
然後所有對 #include "star.def" 的呼叫都可以用簡單的 EXPAND_STAR 語句替換。上面檔案的其餘部分將變成
typedef struct {
#define EXPAND_STAR_MEMBER(member, type) type member;
EXPAND_STAR
#undef EXPAND_STAR_MEMBER
} starStruct;
void serialize_star(const starStruct *const star, unsigned char *buffer) {
#define EXPAND_STAR_MEMBER(member, type) \
memcpy(buffer, &(star->member), sizeof(star->member)); \
buffer += sizeof(star->member);
EXPAND_STAR
#undef EXPAND_STAR_MEMBER
}
void deserialize_star(starStruct *const star, const unsigned char *buffer) {
#define EXPAND_STAR_MEMBER(member, type) \
memcpy(&(star->member), buffer, sizeof(star->member)); \
buffer += sizeof(star->member);
EXPAND_STAR
#undef EXPAND_STAR_MEMBER
}
並且列印處理程式也可以新增,例如
#define print_int(val) printf("%d", val)
#define print_double(val) printf("%g", val)
void print_star(const starStruct *const star) {
/* print_##type will be replaced with print_int or print_double */
#define EXPAND_STAR_MEMBER(member, type) \
printf("%s: ", #member); \
print_##type(star->member); \
printf("\n");
EXPAND_STAR
#undef EXPAND_STAR_MEMBER
}
或作為
#define FORMAT_(type) FORMAT_##type
#define FORMAT_int "%d"
#define FORMAT_double "%g"
void print_star(const starStruct *const star) {
/* FORMAT_(type) will be replaced with FORMAT_int or FORMAT_double */
#define EXPAND_STAR_MEMBER(member, type) \
printf("%s: " FORMAT_(type) "\n", #member, star->member);
EXPAND_STAR
#undef EXPAND_STAR_MEMBER
}
一個變體,它避免需要知道任何擴充套件子宏的成員,是將運算子作為引數傳遞給列表宏
檔案 star_table.c
/*
Generic
*/
#define STRUCT_MEMBER(member, type, dummy) type member;
#define SERIALIZE_MEMBER(member, type, obj, buffer) \
memcpy(buffer, &(obj->member), sizeof(obj->member)); \
buffer += sizeof(obj->member);
#define DESERIALIZE_MEMBER(member, type, obj, buffer) \
memcpy(&(obj->member), buffer, sizeof(obj->member)); \
buffer += sizeof(obj->member);
#define FORMAT_(type) FORMAT_##type
#define FORMAT_int "%d"
#define FORMAT_double "%g"
/* FORMAT_(type) will be replaced with FORMAT_int or FORMAT_double */
#define PRINT_MEMBER(member, type, obj) \
printf("%s: " FORMAT_(type) "\n", #member, obj->member);
/*
starStruct
*/
#define EXPAND_STAR(_, ...) \
_(x, int, __VA_ARGS__) \
_(y, int, __VA_ARGS__) \
_(z, int, __VA_ARGS__) \
_(radius, double, __VA_ARGS__)
typedef struct {
EXPAND_STAR(STRUCT_MEMBER, )
} starStruct;
void serialize_star(const starStruct *const star, unsigned char *buffer) {
EXPAND_STAR(SERIALIZE_MEMBER, star, buffer)
}
void deserialize_star(starStruct *const star, const unsigned char *buffer) {
EXPAND_STAR(DESERIALIZE_MEMBER, star, buffer)
}
void print_star(const starStruct *const star) {
EXPAND_STAR(PRINT_MEMBER, star)
}
這種方法很危險,因為整個宏集總是被解釋為如果它在單行原始碼中,這可能會遇到具有複雜元件宏和/或長成員列表的編譯器限制。
該技術由 Lars Wirzenius[14] 在 2000 年 1 月 17 日的網頁中報道,他在其中將“完善和發展”該技術歸功於 Kenneth Oksanen,該技術早於 1997 年。其他參考文獻將其描述為至少在世紀之交前十年的方法。
我們在後面的部分更詳細地討論了 X-宏,序列化和 X-宏。
- ↑ 理解 C++/C 預處理器
- ↑ "為樂趣和利益利用預處理器".
- ↑ David Hart, Jon Reid. "預處理器的 9 種程式碼異味". 2012.
- ↑ HP C 編譯器參考手冊
- ↑ C++ 參考:預定義預處理變數
- ↑ "C 中的編譯時斷言",作者 Jon Jagger,1999 年
- ↑ Pádraig Brady. "靜態斷言".
- ↑ "三元運算子與常量(真)值?".
- ↑ 維基百科:C++0x#靜態斷言
- ↑ Wirzenius, Lars. "C 預處理器技巧:實現類似的資料型別". 檢索於 2011 年 1 月 9 日.
- ↑ Meyers, Randy (2001 年 5 月 1 日). "新的 C:X 宏". Dr. Dobb's Journal. 檢索於 2024 年 4 月 5 日.
- ↑ Beal, Stephan (2004 年 8 月 22 日). "超級宏". 檢索於 2008 年 10 月 27 日.
{{cite web}}: CS1 maint: date and year (link) - ↑ Keith Schwarz. "高階預處理器技術". 2009 年. 包含 "預處理器的實際應用 II:X 宏技巧".
- ↑ Wirzenius, Lars. C 預處理器技巧:實現類似的資料型別 檢索於 2011 年 1 月 9 日.