23.深入解析C++宏的用法与优化

一、前言

编程语言的目的是通过编写合适的代码去控制计算机执行一些工作,这是最基本的需求。

然而很多时候,编程语言本身的代码书写过程中又会出现很多麻烦的事情,比如反复输入重复的内容,又或者需要控制某段代码在某个系统上要不要被编译器进行编译。

为了解决这一系列的问题,所以就出现了一个概念:

它的主要目的是为了简化代码、提升代码的可读性可维护性

二、基本使用

它的使用方式非常简单:

#define NUM 100

通过#define指令,就可以将一个东西,定义为另外一个东西。

比如常见的,由于数学中Π是一个常数,所以可能我们就会出现这样的定义:

#define Π 3.1425926

在这之后,我们如果想要使用Π时,就可以使用前面的那个名字,而不是后面那一大串数字了:

double area = Π * 3 * 3;

这很像是在定义一个变量,不是吗?让前面的Π等于一个数字,后面就可以直接用这个Π。

但它的原理却与变量天差地别,因为它的本质只是替换,将所有用到前面宏名的地方,都替换成后面的那个东西。

所以虽然你看到的是 Π * 3 * 3,但实际上编译器开始编译的时候,看到的依旧是3.1425926*3*3,编译器会完成这个替换的步骤,这是在预处理阶段完成的:深入解析C/C++编译过程:预处理、编译、汇编与链接详解

替换是宏的精髓,了解了这个,甚至你可以直接通过宏定义一个中文编程语言:

image-20231102142849714

虽然宏看起来很好用,但替换这一特性,也常常是使用宏的问题根源。

三、宏函数

前面只是宏最基本的用法,事实上它还可以像函数一样,接受参数进行替换:

#include <iostream>
using namespace std;
#define fun(x) x*x;
int main()
{
	cout<<fun(10);
}

这看起来非常像一个函数,但它绝不是函数!其本质上依旧会被替换为:cout<<10*10

而这种用法就是很多时候,宏出现问题的原因:

image-20231102143437474

上面的代码,按我们的直觉来看,是不是应该等于100?毕竟10*5*2嘛!

但此时你运行上面这段代码就会发现,并不是:

image-20231102143549328

它居然等于26!这怎么算出来的?

其实这就是替换的问题,上面的替换后实际上是这样一个表达式:10*2+3*2,根据运算符优先级,先算乘法再算加法,自然结果是26了。

所以为了解决上面的这种问题,一般的方案就是采取用最高运算符()来将其括起来:

#define sum(x,y) (x+y)

替换是宏函数最基本的用法,但它还有高阶的用法。

首先是字符串化,,通过添加#号,你可以将任意内容转换为字符串:

image.png

其本质上,其实等价于给任意传入的内容用双引号括起来,只不过双引号本身就代码了字符串,并不能直接在宏中使用。

除了字符串化以外,它还支持粘合:

image.png

也就是把两个相同的数据类型粘在一起,合并为一个数据。

最后,宏函数还支持可变参数:

image.png

只需要在最后一个参数用...写入即可,然后在定义中使用##__VA_ARGS_的方式引用传入的可变参数,其中##一位着和前面的字符串拼接,后面的__VA_ARGS_是可变参数值。

其最佳的用法就是和printf这样拥有可变参数的宏一起使用,可以用来非常方便的定义一些打印日志操作宏。

四、条件编译

上面的两种基本的用法,其实并不太推荐大家使用,因为宏一旦出现错误,是很难调试的。

如果想要定义常量,那直接使用const即可,如果需要函数,那就定义一个普通函数即可,没必要使用宏。

但宏既然还存在,那必然就还有其存在的价值,否则应该被彻底淘汰了才对。

而条件编译,便是目前宏最大的存在价值,它可以让你实现在不同情况下编译不同的代码,从而实现所谓的跨平台!

其实用起来和基本的判断语句区别并不大:

image-20231102145308686

这里的ifdefif define的缩写,意思就是,如果定义了这个宏,那就编译后面的代码,否则就不进行编译。

由于vs用的msvc编译器默认会定义这个_WIN32宏,所以就可以通过这种方式,来进行跨平台开发。

更多预定义宏可以查看官方文档:预定义宏

实现的逻辑就是,在一个openfile函数中,分别写好两个平台的代码,通过宏的条件判断当前所处平台,编译对应平台的代码。

并且在vs中还会清晰给你标注出来,将要被编译的代码是正常颜色的,而这里不会被编译的代码则是暗色调的。

除了用它写跨平台代码外,另一个常见的用途就是防止代码被重复编译。

比如这样:

image-20231102150140172

如果我在这里将其包含两次,那实际上就相当于声明了两个同名的类:

image-20231102150239923

此时你就需要考虑代码中是否有可能出现重定义的错误了,并且由于多次包含,编译器需要处理的代码变多,一定程度上可能也会影响编译的速度。

那么此时就可以这样做:

image-20231102150435512

这里的ifndefif no define的缩写,意思就是如果没有定义后面这个宏,才编译内部的代码,与ifdef刚好相反。

进入内部的代码中,第一次事情就是将这个宏给定义了,即:#define STACK_H

注意我们这里只是想要检测宏有没有定义,并没有利用到所谓的替换功能,所以没有写第二个被替换的内容,也就是空宏

那么此时多次包含后,实际上会被转化为下面这样的代码:

image-20231102150815482

由于第一次没有定义,所以进入,并且将其定义了,内部的代码也会被编译。

而如果第二次进入,此时该宏已经在前面被定义过了,所以不会编译内部的代码,这样就完成防止多次包含的问题。

这种用法在C++代码中是非常常见的,也是目前宏最主要的用法。

至于这个宏名,你可以随意取,但一般的习惯是直接用该文件名全大写,由于一般都是头文件,所以后面还会跟一个_H的后缀。

比如这里的头文件名为Stack.h,所以对应的宏名就是STACK_H

五、预定义宏

正如上面提到的_WIN32,编译器本身自己就会定义一些宏可以让我们直接用。

下面几个便是比较通用的宏,基本各大编译器中都可以直接用:

  • __DATE__:当前日期。
  • __TIME__hh:mm:ss格式的当前时间。
  • __FILE__:当前源文件的名字
  • __LINE__:当前所在文件的代码行数
  • __func__:当前函数的名字

一个简单的用例如下:

image-20231102152039459

这些信息对于日志输出非常有用,比如我们可以自定义一个日志宏函数:

#define log_fun(str) printf("%s:%s %s:%d=>%s\n",__DATE__,__TIME__,__FILE__,__LINE__,str);

然后在我们想要输出日志的地方,进行打印即可:

image.png

这对于稍微大一点的项目中都是非常重要的,因为这可以极大的提高我们排查错误的效率,毕竟连这是哪个文件、哪一行都打印出来了。

更优雅的方式是用上前面介绍过的可变参数宏函数:

#define log_fun(format,...) printf("%s:%s %s:%d=>"##format,__DATE__,__TIME__,__FILE__,__LINE__,##__VA_ARGS__);

效果如下:

image.png

作者:余识
全部文章:0
会员文章:0
总阅读量:0
c/c++pythonrustJavaScriptwindowslinux