C 程式設計/預處理指令和宏
預處理器是在實際編譯之前對 C 程式進行文字處理的一種方式。在實際編譯每個 C 程式之前,它都會經過預處理器。預處理器會檢視程式,試圖找出它能理解的特定指令,稱為預處理器指令。所有預處理器指令都以 #(井號)符號開頭。C++ 編譯器使用相同的 C 預處理器。[1]
該 預處理器 是編譯器的一部分,在編譯器看到程式碼之前,它會對程式碼進行初步操作(有條件地編譯程式碼、包含檔案等)。這些轉換是詞彙上的,這意味著預處理器的輸出仍然是文字。
|
注意:從技術上講,C 的預處理階段的輸出由一系列標記組成,而不是原始碼,但很容易輸出等效於給定標記序列的原始碼,並且這通常由編譯器透過-E或/E選項 - 儘管 C 編譯器的命令列選項並不完全標準,但許多編譯器遵循類似的規則。 |
指令是針對預處理器(預處理器指令)或 編譯器 (編譯器指令)的特殊指令,用於指示編譯器如何處理部分或全部原始碼,或在最終物件上設定一些標誌,並用於使編寫原始碼更容易(例如更便攜)以及使原始碼更易於理解。指令由預處理器處理,預處理器是一個獨立的程式,由編譯器呼叫,或者本身是編譯器的一部分。
C 有一些功能是作為語言的一部分,另一些功能是作為標準庫的一部分,標準庫是一個程式碼庫,與每個符合標準的 C 編譯器一起提供。當 C 編譯器編譯你的程式時,它通常還會將它與標準 C 庫連結。例如,當遇到一個#include <stdio.h>指令時,它會用stdio.h標頭檔案的內容替換該指令。
當你使用庫中的功能時,C 要求你宣告你將使用什麼。程式中的第一行是一個預處理指令,它應該如下所示
#include <stdio.h>
上面的程式碼行會導致位於stdio.h 標頭檔案中的 C 宣告被包含在你的程式中。通常,這是透過將標頭檔案的內容插入到你的程式中來實現的,該標頭檔案稱為stdio.h,位於系統相關的目錄中。這些檔案的目錄可能在你的編譯器文件中描述。標準 C 標頭檔案列表在下面的標頭檔案表中列出。
該stdio.h標頭檔案包含各種使用稱為流的 I/O 機制抽象的輸入/輸出 (I/O) 宣告。例如,有一個名為stdout的輸出流物件,它用於將文字輸出到標準輸出,通常將文字顯示在計算機螢幕上。
如果使用尖括號(如上面的示例),則預處理器被指示沿著標準包含的開發環境路徑搜尋包含檔案。
#include "other.h"
如果你使用引號 (" "),則預處理器應該在一些額外的、通常是使用者定義的目錄中搜索標頭檔案,並且只有在這些額外的目錄中沒有找到該檔案時,才會回退到標準包含路徑。通常,這種形式包括在包含#include指令的檔案所在的相同目錄中搜索。
|
注意:你應該檢查你正在使用的開發環境的文件,以瞭解任何供應商特定的實現#include指令的檔案所在的相同目錄中搜索。 |
C90 標準標頭檔案列表
自 C90 以來新增的標頭檔案
該pragma(實用資訊)指令是標準的一部分,但任何 pragma 的含義都取決於所使用的標準的軟體實現。#pragma 指令提供了一種從編譯器請求特殊行為的方法。對於特別大的程式或需要利用特定編譯器功能的程式,此指令非常有用。
Pragmas 用於源程式中。
#pragma token(s)
- pragma 通常後跟一個標記,它表示要編譯器服從的命令。你應該檢查你打算使用的 C 標準的軟體實現,以獲取支援的標記列表。不出所料,可以在 #pragma 指令中出現的命令集對於每個編譯器都是不同的;你需要檢視你的編譯器的文件,以檢視它允許哪些命令以及這些命令的功能。
例如,最常實現的預處理器指令之一,#pragma once,當放置在標頭檔案的開頭時,表示如果預處理器多次包含該檔案,它將跳過該檔案。
|
注意:還存在其他方法來執行此操作,此操作通常稱為使用包含守衛。 |
每個#define預處理器指令都定義一個宏。例如,
#define PI 3.14159265358979323846 /* pi */
在名稱後面緊跟著一個空格的宏稱為常量或文字。在名稱後面緊跟著一個括號的宏稱為類函式宏。[2]
|
警告:預處理器宏雖然很誘人,但如果使用不當,可能會產生意想不到的結果。請始終牢記,宏是在編譯任何內容之前對原始碼進行的文字替換。編譯器不知道宏,也永遠不會看到它們。這可能會導致難以理解的錯誤,以及其他負面影響。如果存在等效的語言功能,最好使用它們。例如,使用 也就是說,宏在某些情況下非常有用(請參閱下面的 |
該#define指令用於定義宏。宏由預處理器在編譯原始碼之前使用,用於操作程式原始碼。由於預處理器宏定義是在編譯器處理原始碼之前進行替換的,因此由#define引入的任何錯誤都難以追蹤。
按照慣例,使用#define定義的宏使用大寫字母命名。雖然這不是必需的,但反之則被認為是非常糟糕的習慣。這樣一來,在閱讀原始碼時可以很容易地識別宏。(在後面的章節中,我們提到了許多其他使用#define的通用約定,C 程式設計/通用實踐)。
今天,#define主要用於處理編譯器和平臺差異。例如,一個 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")
|
注意 只有在無法使用函式或其他機制實現相同結果時,才應定義宏。一些編譯器能夠最佳化程式碼,將對小型函式的呼叫替換為內聯程式碼,從而消除了任何可能的效能優勢。使用typedefs、enums和inline(在C99中)通常是一個更好的選擇。 |
行內函數無法正常工作的情況之一是初始化編譯時常量(結構的靜態初始化)。當宏的引數是編譯器可以最佳化為另一個文字的文字時,就會發生這種情況。 [3]
#error
[edit | edit source]#error指令停止編譯。遇到一個時,標準規定編譯器應該發出一個包含指令中剩餘標記的診斷資訊。這主要用於除錯目的。
程式設計師在條件塊內使用"#error",以便在"#if"或"#ifdef"(塊的開頭)檢測到編譯時問題時立即停止編譯。通常,編譯器會跳過該塊(以及其中的"#error"指令),編譯繼續進行。
#error message
#warning
[edit | edit source]許多編譯器支援#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
#line
[edit | edit source]此預處理器指令用於將指令後一行的檔名和行號設定為新值。這用於設定__FILE__和__LINE__宏。
用於除錯的有用預處理器宏
[edit | edit source]ANSI C定義了一些有用的預處理器宏和變數,[4][5] 也稱為“神奇常量”,包括
__FILE__ => 當前檔名,作為字串文字
__LINE__ => 原始檔的當前行號,作為數字文字
__DATE__ => 當前系統日期,作為字串
__TIME__ => 當前系統時間,作為字串
__TIMESTAMP__ => 日期和時間(非標準)
__cplusplus => 當您的C程式碼由C編譯器編譯時未定義;當您的C程式碼由符合1998 C++標準的C++編譯器編譯時為199711L。
__func__ => 原始檔的當前函式名稱,作為字串(C99的一部分)
__PRETTY_FUNCTION__ => “修飾”的原始檔的當前函式名稱,作為字串(在GCC中;非標準)
編譯時斷言可以幫助您比僅使用執行時斷言語句更快地除錯,因為編譯時斷言是在編譯時測試的,而程式的測試執行可能會無法執行某些執行時斷言語句。
在 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 已被標準化並內建到語言中。
| 一位 Wikibookian 建議將本書或本章與 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. "靜態斷言".
- ↑ "帶有常量(true)值的條件運算子?".
- ↑ 維基百科: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 維護:日期和年份 (連結) - ↑ Keith Schwarz. "高階預處理器技術". 2009. 包括“預處理器的實際應用二:X 宏技巧”。
- ↑ Wirzenius, Lars. 用於實現類似資料型別的 C 預處理器技巧 檢索於 2011 年 1 月 9 日。