嚴格來說,這個題目起名為C++是不合適的,因為宏定義是C語言的遺留特性。CleanCode并不推薦C++中使用宏定義。我當時還在公司做過宏定義為什么應該被取代的報告。但是適當使用宏定義對代碼是有好處的。壞處也有一些。
無參宏定義
最常見的一種宏定義,如下:
#define NUM_1 1
需要注意的是,宏定義執行的是替換操作,在預處理階段就完成了。因此編譯期間或者運行期間的代碼是感知不到宏定義的存在的,這也是宏定義不被推薦的原因——出了事很難找到問題。關于C++程序生成的各個階段可以參考我的這篇文章:【C++】template方法undefined reference to(二):C++代碼的編譯過程
#include <iostream>
#define NUM_1 1using namespace std;int main()
{cout << NUM_1 << endl;
}
數字的類型是可以通過后綴指定的,比如1U
代表unsigned int
類型的1。
可以使用const代替常量宏:
const int NUM_1 = 1;
如果你的編譯器支持C++11標準,那應該使用constexpr
,這樣const
就只有“只讀”的含義了。
constexpr int NUM_1 = 1;
有參宏定義
C++語言允許宏帶有參數。在宏定義中的參數稱為形式參數,在宏調用中的參數稱為實際參數。對帶參數的宏,在調用中,不僅要宏展開,而且要用實參去代換形參。你可以理解為一種“函數”。有參宏定義也是宏的另一個大量使用的用途。
一個最簡單的使用三元表達式返回更大值的宏定義:
#define MAX(a, b) a > b? a: b
代碼中使用:
#include <iostream>
#define MAX(a, b) a > b? a: busing namespace std;const int NUM_1 = 1;int main()
{int a = MAX(1,2);cout << a << endl;
}
這種宏定義的優點在于不會受參數類型的影響,現代C++提倡使用模板方法代替之:
template <typename T>
T max(T &a, T *b)
{return a > b ? a : b;
}
調用的地方基本一致:
int b = max(1, 2);cout << b << endl;
由于模板方法本質是將函數實現挪到了編譯期,對所有模板類型的調用生成對應的函數,所以,它和正常的函數調用沒有任何區別。可以直接寫在里面:
cout << max(1, 2) << endl;
模板方法也存在很多問題,比如多文件編譯時存在聲明實現不可分離的問題
:【C++】template方法undefined reference to
宏定義的副作用
仍然以剛才的MAX
宏為例
感謝知乎閃耀大叔提供的例子,原文鏈接從Linux內核中學習高級C語言宏技巧
為了方便理解,我把所有數字都改成二進制標識:
int main(void)
{int i = 0b1110;int j = 0b0011;printf ("i&0b101 = %d\n",i&0b101);printf ("j&0b101 = %d\n",j&0b101);printf("max=%d\n",MAX(i&0b101,j&0b101));return 0;
}
輸出結果為:
顯然不符合預期,問題在哪呢?因為>
運算符優先級大于&
,所以會先進行比較再進行按位與。
Linux 內核中的寫法其實是這樣的:
#define MAX(x, y) ({ \typeof(x) _max1 = (x); \typeof(y) _max2 = (y); \(void) (&_max1 == &_max2); \_max1 > _max2 ? _max1 : _max2; })
具體可以看上面的文章,這里就不展開了。
實現語法糖
C++有一些約定俗成的寫法,其實可以用宏定義進行簡化。
比如可以將if-continue
簡化為一個宏:
#define CONTINUE_IF(exp) \if (exp) \continue
還有其他比如return
相關的:
#define RETURN_IF_VOID(exp) \if (exp) \return#define RETURN_IF(exp, result) \if (exp) \return result
代碼里就可以替換,這里僅給出一個例子:
int main()
{for (int i = 0; i < 10; i++){CONTINUE_IF(i % 2 == 0);printf("%d, ", i);}
}
另外一種情況,比如C++的多態的一個重要實現就是虛函數和繼承,我們可以簡化虛函數的寫法。定義如下的宏:
#define OVERRIDE(exp) virtual exp override
就可以簡化虛函數繼承的寫法。如下:
struct Student
{virtual void printName(){printf("Student Name\n");}
};struct PrimaryStudent : Student
{OVERRIDE(void printName()){printf("PrimaryStudent Name\n");}
};void printName(Student& student)
{student.printName();
}
override關鍵字只有在C++11以后才能生效,我們可以使用__GNUC__
宏來判斷。 __GNUC__
的值表示gcc的版本。需要針對gcc特定版本編寫代碼時,可以使用該宏進行條件編譯。C++11標準從GCC4.8.1版本完全支持,__GNUC__
、__GNUC_MINOR__
、__GNUC_PATCHLEVEL__
分別代表gcc的主版本號,次版本號,修正版本號。我們可以寫出如下判斷:
#ifdef __GNUC__printf("__GNUC__ = %d\n", __GNUC__);
#endif
#ifdef __GNUC_MINOR__printf("__GNUC_MINOR__ = %d\n", __GNUC_MINOR__);
#endif
#ifdef __GNUC_PATCHLEVEL__printf("__GNUC_PATCHLEVEL__ = %d\n", __GNUC_PATCHLEVEL__);
#endif
輸出結果為:
和控制臺的輸出是一致的:
PS D:\Codes\CPP\VSCodeProjects\2024\June\CPPMacros> g++ --version
g++.exe (GCC) 13.2.0
Copyright (C) 2023 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
接下來我們寫一個條件判斷宏,僅在4.8.1版本后在虛函數后加上overrride標識。由于gcc版本宏判斷過于復雜,我們用一個宏先將版本號轉為整數:
#define GCC_VERSION (__GNUC__ * 10000 \+ __GNUC_MINOR__ * 100 \+ __GNUC_PATCHLEVEL__)
然后判斷版本號是否大于40801,否則不使用override關鍵字:
#if GCC_VERSION > 40801
#define OVERRIDE(exp) virtual exp override
#else
#define OVERRIDE(exp) virtual exp
#endif
遺憾的是我本地沒有多個編譯器,沒法判斷這個代碼是否成功了。
簡化代碼
我們有時會碰到一個類有多個類似的方法的情況,比如:
struct Student
{
public:int getAge();int getNumber();int getPoint();
};
此時可以使用宏來簡化這種寫法。__VA_ARGS__
是一個可變參數的宏。將宏定義中參數列表的最后一個參數為省略號(也就是三個點)。這樣預定義宏__VA_ARGS__就可以被用在替換部分中,替換省略號所代表的字符串。##運算符可以用于函數宏的替換部分,這個運算符把兩個語言符號組合成單個語言符號。
可以寫出如下代碼:
#define GET(...) int get##__VA_ARGS__()
struct Student
{
public:GET(Age);int getNumber();int getPoint();
};