巨集
巨集分為兩大類:類似物件的巨集和類似函式的巨集。巨集在編譯過程的早期被視為令牌替換。這意味著可以將大型(或重複)程式碼段抽象為前處理器巨集。
// This is an object-like macro
#define PI 3.14159265358979
// This is a function-like macro.
// Note that we can use previously defined macros
// in other macro definitions (object-like or function-like)
// But watch out, its quite useful if you know what you're doing, but the
// Compiler doesnt know which type to handle, so using inline functions instead
// is quite recommended (But e.g. for Minimum/Maximum functions it is quite useful)
#define AREA(r) (PI*(r)*(r))
// They can be used like this:
double pi_macro = PI;
double area_macro = AREA(4.6);
Qt 庫利用這種技術建立一個元物件系統,方法是讓使用者在擴充套件 QObject 的使用者定義類的頭部宣告 Q_OBJECT 巨集。
巨集名稱通常以全部大寫字母書寫,以便更容易區分普通程式碼。這不是一個要求,但僅僅被許多程式設計師認為是好的風格。
當遇到類似物件的巨集時,它會作為簡單的複製貼上操作進行擴充套件,巨集的名稱將被其定義替換。遇到類似函式的巨集時,會擴充套件其名稱及其引數。
double pi_squared = PI * PI;
// Compiler sees:
double pi_squared = 3.14159265358979 * 3.14159265358979;
double area = AREA(5);
// Compiler sees:
double area = (3.14159265358979*(5)*(5))
因此,類似函式的巨集引數通常包含在括號內,如上面的 AREA()
所示。這是為了防止在巨集擴充套件期間可能發生的任何錯誤,特別是由單個巨集引數由多個實際值組成的錯誤。
#define BAD_AREA(r) PI * r * r
double bad_area = BAD_AREA(5 + 1.6);
// Compiler sees:
double bad_area = 3.14159265358979 * 5 + 1.6 * 5 + 1.6;
double good_area = AREA(5 + 1.6);
// Compiler sees:
double good_area = (3.14159265358979*(5 + 1.6)*(5 + 1.6));
另請注意,由於這種簡單的擴充套件,必須注意傳遞給巨集的引數,以防止意外的副作用。如果在評估期間修改了引數,則每次在擴充套件巨集中使用它時都會對其進行修改,這通常不是我們想要的。即使巨集將引數括在括號中以防止擴充套件破壞任何內容,也是如此。
int oops = 5;
double incremental_damage = AREA(oops++);
// Compiler sees:
double incremental_damage = (3.14159265358979*(oops++)*(oops++));
此外,巨集不提供型別安全性,導致難以理解的型別不匹配錯誤。
由於程式設計師通常使用分號終止行,因此用作獨立行的巨集通常被設計為吞下分號; 這可以防止任何意外的錯誤由額外的分號引起。
#define IF_BREAKER(Func) Func();
if (some_condition)
// Oops.
IF_BREAKER(some_func);
else
std::cout << "I am accidentally an orphan." << std::endl;
在這個例子中,無意的雙分號打破了 if...else
塊,阻止編譯器將 else
與 if
匹配。為了防止這種情況,巨集定義中省略了分號,這將導致它在使用分號後立即吞下分號。
#define IF_FIXER(Func) Func()
if (some_condition)
IF_FIXER(some_func);
else
std::cout << "Hooray! I work again!" << std::endl;
退出尾隨分號還允許使用巨集而不結束當前語句,這可能是有益的。
#define DO_SOMETHING(Func, Param) Func(Param, 2)
// ...
some_function(DO_SOMETHING(some_func, 3), DO_SOMETHING(some_func, 42));
通常,巨集定義在行尾結束。但是,如果巨集需要覆蓋多行,則可以在行尾使用反斜槓來表示這一點。此反斜槓必須是行中的最後一個字元,它向前處理器指示應將以下行連線到當前行,將它們視為單行。這可以連續多次使用。
#define TEXT "I \
am \
many \
lines."
// ...
std::cout << TEXT << std::endl; // Output: I am many lines.
這在複雜的類函式巨集中尤其有用,它可能需要覆蓋多行。
#define CREATE_OUTPUT_AND_DELETE(Str) \
std::string* tmp = new std::string(Str); \
std::cout << *tmp << std::endl; \
delete tmp;
// ...
CREATE_OUTPUT_AND_DELETE("There's no real need for this to use 'new'.")
在更復雜的類函式巨集的情況下,給它們自己的範圍以防止可能的名稱衝突或導致在巨集的末尾銷燬物件(類似於實際函式)是有用的。一個常見的習慣用語是 0 ,其中巨集被包含在 do-while 塊中。該塊通常不帶分號,允許它吞下分號。
#define DO_STUFF(Type, Param, ReturnVar) do { \
Type temp(some_setup_values); \
ReturnVar = temp.process(Param); \
} while (0)
int x;
DO_STUFF(MyClass, 41153.7, x);
// Compiler sees:
int x;
do {
MyClass temp(some_setup_values);
x = temp.process(41153.7);
} while (0);
還有可變巨集; 類似於可變引數函式,它們採用可變數量的引數,然後將它們全部展開以代替特殊的 Varargs
引數 __VA_ARGS__
。
#define VARIADIC(Param, ...) Param(__VA_ARGS__)
VARIADIC(printf, "%d", 8);
// Compiler sees:
printf("%d", 8);
請注意,在擴充套件期間,__VA_ARGS__
可以放在定義中的任何位置,並且可以正確擴充套件。
#define VARIADIC2(POne, PTwo, PThree, ...) POne(PThree, __VA_ARGS__, PTwo)
VARIADIC2(some_func, 3, 8, 6, 9);
// Compiler sees:
some_func(8, 6, 9, 3);
在零引數可變引數的情況下,不同的編譯器將以不同方式處理尾隨逗號。某些編譯器(如 Visual Studio)將在沒有任何特殊語法的情況下靜默地吞下逗號。其他編譯器,如 GCC,要求你在 __VA_ARGS__
之前立即放置 ##
。因此,在關注可移植性時有條件地定義可變引數巨集是明智的。
// In this example, COMPILER is a user-defined macro specifying the compiler being used.
#if COMPILER == "VS"
#define VARIADIC3(Name, Param, ...) Name(Param, __VA_ARGS__)
#elif COMPILER == "GCC"
#define VARIADIC3(Name, Param, ...) Name(Param, ##__VA_ARGS__)
#endif /* COMPILER */