一、前言
编程语言的目的是通过编写合适的代码去控制计算机执行一些工作,这是最基本的需求。
然而很多时候,编程语言本身的代码书写过程中又会出现很多麻烦的事情,比如反复输入重复的内容,又或者需要控制某段代码在某个系统上要不要被编译器进行编译。
为了解决这一系列的问题,所以就出现了一个概念:宏。
它的主要目的是为了简化代码、提升代码的可读性和可维护性。
二、基本使用
它的使用方式非常简单:
#define NUM 100
通过#define
指令,就可以将一个东西,定义为另外一个东西。
比如常见的,由于数学中Π是一个常数,所以可能我们就会出现这样的定义:
#define Π 3.1425926
在这之后,我们如果想要使用Π
时,就可以使用前面的那个名字,而不是后面那一大串数字了:
double area = Π * 3 * 3;
这很像是在定义一个变量,不是吗?让前面的Π等于一个数字,后面就可以直接用这个Π。
但它的原理却与变量天差地别,因为它的本质只是替换,将所有用到前面宏名的地方,都替换成后面的那个东西。
所以虽然你看到的是 Π * 3 * 3
,但实际上编译器开始编译的时候,看到的依旧是3.1425926*3*3
,编译器会完成这个替换的步骤,这是在预处理阶段完成的:深入解析C/C++编译过程:预处理、编译、汇编与链接详解。
替换是宏的精髓,了解了这个,甚至你可以直接通过宏定义一个中文编程语言:
虽然宏看起来很好用,但替换这一特性,也常常是使用宏的问题根源。
三、宏函数
前面只是宏最基本的用法,事实上它还可以像函数一样,接受参数进行替换:
#include <iostream>
using namespace std;
#define fun(x) x*x;
int main()
{
cout<<fun(10);
}
这看起来非常像一个函数,但它绝不是函数!其本质上依旧会被替换为:cout<<10*10
。
而这种用法就是很多时候,宏出现问题的原因:
上面的代码,按我们的直觉来看,是不是应该等于100
?毕竟10*5*2
嘛!
但此时你运行上面这段代码就会发现,并不是:
它居然等于26
!这怎么算出来的?
其实这就是替换的问题,上面的替换后实际上是这样一个表达式:10*2+3*2
,根据运算符优先级,先算乘法再算加法,自然结果是26
了。
所以为了解决上面的这种问题,一般的方案就是采取用最高运算符()
来将其括起来:
#define sum(x,y) (x+y)
替换是宏函数最基本的用法,但它还有高阶的用法。
首先是字符串化,,通过添加#
号,你可以将任意内容转换为字符串:
其本质上,其实等价于给任意传入的内容用双引号括起来,只不过双引号本身就代码了字符串,并不能直接在宏中使用。
除了字符串化以外,它还支持粘合:
也就是把两个相同的数据类型粘在一起,合并为一个数据。
最后,宏函数还支持可变参数:
只需要在最后一个参数用...
写入即可,然后在定义中使用##__VA_ARGS_
的方式引用传入的可变参数,其中##
一位着和前面的字符串拼接,后面的__VA_ARGS_
是可变参数值。
其最佳的用法就是和printf这样拥有可变参数的宏一起使用,可以用来非常方便的定义一些打印日志操作宏。
四、条件编译
上面的两种基本的用法,其实并不太推荐大家使用,因为宏一旦出现错误,是很难调试的。
如果想要定义常量,那直接使用const
即可,如果需要函数,那就定义一个普通函数即可,没必要使用宏。
但宏既然还存在,那必然就还有其存在的价值,否则应该被彻底淘汰了才对。
而条件编译,便是目前宏最大的存在价值,它可以让你实现在不同情况下编译不同的代码,从而实现所谓的跨平台!
其实用起来和基本的判断语句区别并不大:
这里的ifdef
是if define
的缩写,意思就是,如果定义了这个宏,那就编译后面的代码,否则就不进行编译。
由于vs用的msvc编译器默认会定义这个_WIN32
宏,所以就可以通过这种方式,来进行跨平台开发。
更多预定义宏可以查看官方文档:预定义宏。
实现的逻辑就是,在一个openfile
函数中,分别写好两个平台的代码,通过宏的条件判断当前所处平台,编译对应平台的代码。
并且在vs中还会清晰给你标注出来,将要被编译的代码是正常颜色的,而这里不会被编译的代码则是暗色调的。
除了用它写跨平台代码外,另一个常见的用途就是防止代码被重复编译。
比如这样:
如果我在这里将其包含两次,那实际上就相当于声明了两个同名的类:
此时你就需要考虑代码中是否有可能出现重定义的错误了,并且由于多次包含,编译器需要处理的代码变多,一定程度上可能也会影响编译的速度。
那么此时就可以这样做:
这里的ifndef
是if no define
的缩写,意思就是如果没有定义后面这个宏,才编译内部的代码,与ifdef
刚好相反。
进入内部的代码中,第一次事情就是将这个宏给定义了,即:#define STACK_H
。
注意我们这里只是想要检测宏有没有定义,并没有利用到所谓的替换功能,所以没有写第二个被替换的内容,也就是空宏。
那么此时多次包含后,实际上会被转化为下面这样的代码:
由于第一次没有定义,所以进入,并且将其定义了,内部的代码也会被编译。
而如果第二次进入,此时该宏已经在前面被定义过了,所以不会编译内部的代码,这样就完成防止多次包含的问题。
这种用法在C++代码中是非常常见的,也是目前宏最主要的用法。
至于这个宏名,你可以随意取,但一般的习惯是直接用该文件名全大写,由于一般都是头文件,所以后面还会跟一个_H
的后缀。
比如这里的头文件名为Stack.h
,所以对应的宏名就是STACK_H
。
五、预定义宏
正如上面提到的_WIN32
,编译器本身自己就会定义一些宏可以让我们直接用。
下面几个便是比较通用的宏,基本各大编译器中都可以直接用:
__DATE__
:当前日期。__TIME__
:hh:mm:ss
格式的当前时间。__FILE__
:当前源文件的名字__LINE__
:当前所在文件的代码行数__func__
:当前函数的名字
一个简单的用例如下:
这些信息对于日志输出非常有用,比如我们可以自定义一个日志宏函数:
#define log_fun(str) printf("%s:%s %s:%d=>%s\n",__DATE__,__TIME__,__FILE__,__LINE__,str);
然后在我们想要输出日志的地方,进行打印即可:
这对于稍微大一点的项目中都是非常重要的,因为这可以极大的提高我们排查错误的效率,毕竟连这是哪个文件、哪一行都打印出来了。
更优雅的方式是用上前面介绍过的可变参数宏函数:
#define log_fun(format,...) printf("%s:%s %s:%d=>"##format,__DATE__,__TIME__,__FILE__,__LINE__,##__VA_ARGS__);
效果如下: