严格来说,这个题目起名为C++是不合适的,因为宏定义是C语言的遗留特性。CleanCode并不推荐C++中使用宏定义。我当时还在公司做过宏定义为什么应该被取代的报告。但是适当使用宏定义对代码是有好处的。坏处也有一些。
无参宏定义
最常见的一种宏定义,如下:
#define NUM_1 1
需要注意的是,宏定义执行的是替换操作,在预处理阶段就完成了。因此编译期间或者运行期间的代码是感知不到宏定义的存在的,这也是宏定义不被推荐的原因——出了事很难找到问题。关于C++程序生成的各个阶段可以参考我的这篇文章:【C++】template方法undefined reference to(二):C++代码的编译过程
#include <iostream>
#define NUM_1 1
using 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: b
using 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();
};