#define宏定义是什么?怎么写?一文搞懂。

前言

第一次接触宏定义的时候,是在学校的C++课程大作业中。当时有幸看到了大佬写的源码,里面有很多的宏定义。当时并不了解宏定义的妙用,只感觉它像是定义了一个变量一样,不好理解还有些多此一举。
这之后在刷题的过程中接触到了CodeForce,在偶然瞻仰大佬刷题模板的时候,我震惊了。大佬的模板中,充斥着各种骚气的宏定义。
常规一点的,比如定义一个for循环:

#define FOR(i,a,b) for (int i = (a); i < (b); ++i)

风骚一点,骗hack的,像样的:

#define false 1
#define true 0

或者这样的(这个我也经常用):

#define int long long

甚至卖个萌这样也行:

#define _ 0
/* 代码段 */
return 0^_^0;

当然了,还有比较实用的#ifdef#ifndef,配合debug很方便,这里就不列举了。
所以在这之后,我才算是真正的了解了宏定义的妙用,并在刷题中得以运用。
后来在工作中,我也慢慢接触到了很多工程中宏定义的用法,这些用法更实用,安全性也更高。所以就根据自己的一些经验、理解做一番总结,方便大家食用。如果有不对的地方也欢迎大家讨论、指正。

----------------------------------正文分割线(本文略长,可选择性食用)--------------------------------

正文

宏定义到底是什么呢?

简单说,宏定义就是给你的代码块重新起个简短的小名,当你需要使用这段代码的时候,就使用这个小名,以达到少写代码的目的。编译器会在预编译阶段把你定义的那一段代码整体替换并展开到你使用的地方。

#define的作用主要有以下几个

  • 用于将复杂的重复代码块定义成简短的宏,便于书写
  • 充当全局常量,让代码结构更加明确,同时减少因为变量定义带来内存占用与其他问题
  • #ifdef#ifndef搭配使用可以对编译的代码块进行控制
  • 可以与###等东西搭配使用,减少代码冗余
  • 可以使用某些骚操作,彰显自己很厉害

之后,我们通过一个具体的需求来一步一步的写出一个功能完备,代码规范的宏定义
现在,我们需要写一个宏,功能是返回两个正整数中较大的一个数,原型如下:

#define my_max(num_1, num_2)

你会怎么写?很多人第一反应,用三目运算符? :一行不就搞定了,是的,写出后,它长这样:

#define my_max(num_1, num_2) num_1>num_2 ? num_1 : num_2

测试一下,看起来好像没有问题
​​​​​​​​在这里插入图片描述
再测试一下
在这里插入图片描述
出现问题了,输出结果应该是8才对,问题出在哪呢?
原因是宏定义的代码会进行整体替换,或者换句话说就是直接把代码搬过来,那么我们展开宏定义,printf语句就会变成这样:

printf("%d\n", 3 + 3 > 5 ? 3 : 5);

显然+的优先级高于>,所以先计算加法,之后6>5成立,导致输出为3
那么如何修改?显而易见的方式是给宏定义的主体代码加上括号,让其先进行运算。
修改后再测试如下:
在这里插入图片描述
问题解决了,输出符合预期,所以我们有了第一条小技巧
技巧1:宏定义的主体代码要加完备的括号

再测试一下:
在这里插入图片描述
程序又不对了,问题在哪呢?我们再次宏替换一下:

printf("%d\n", (3 > 4 > 2 ? 3 : 4 > 2));

显然3>4>2逻辑值为0,所以程序返回4>2的逻辑值1
那么如何解决,想必你已经想到了,那就是给变量也加上完备的括号即可,所以我们有了第二条小技巧
技巧2:宏定义主体代码中的参量也要加完备的括号

修改后,测试一番:
在这里插入图片描述
这次对了,那么,再测试一下:
在这里插入图片描述
好像又出现了问题,在我们的认知中,++后缀应该在使用时还是原来的值,使用完成后才变为+1的值,所以输出应该是2才对,那么问题出现在哪?我们还是展开看一下:

printf("%d\n", ((num_1++) > (1) ? (num_1++) : (1)));

显然(num_1++) > (1)判断结束后,num_11,此时值为3,故返回3
既然这样,问题变得有些麻烦,看来不能用一行代码搞定了。
但我们可以用两个变量来承接传入的参数,获得它们的值,再进行运算即可,于是修改成如下形式:

#define my_max(num_1, num_2) int num_1_temp = (num_1); int num_2_temp = (num_2); (num_1_temp) > (num_2_temp) ? (num_1_temp) : (num_2_temp);

注意:这里的()圆括号其实可以不用添加,赋值运算符=的优先级最低,所以即使num_1num_2是表达式也不会发生错误。而三目运算符中的括号,是对我们之前申请的变量进行运算,不是表达式,所以也不会发生表达式混乱的情况,但这里以防万一还是加上了完备的括号,这也是一个编程的好习惯,不确定的情况,多加()是总没有错的。

我们这样的确可以解决问题,但是我们将代码写到了一行,显然这有些丑陋,而且不符合编程规范。
加之宏定义的作用域只有它自己本身的这一行,不会识别下一行的语句,所以不能够分成多行编写,这要如何解决?
当然,c++语言帮我们想到了这个问题,我们可以使用续行符\对代码进行连接。这样也可以让复杂的宏定义便于阅读,看起来更像一段“正常”的代码,修改后的代码如下:

#define my_max(num_1, num_2)    \
    int num_1_temp = (num_1);    \
    int num_2_temp = (num_2);    \
    (num_1_temp) > (num_2_temp) ? (num_1_temp) : (num_2_temp);

注意:最后一行并不需要续行符,因为续行符的作用是将本行与下一行的代码连成一行,实际上就是帮我们把代码隐式的放在了一行。程序段中也可以使用续行符对过长的代码段进行连接,但即使不使用,编译器在未识别到语句结束的情况下也会自动帮我们进行连接。这也是我们平常不使用续行符的原因之一。但在宏定义中,宏的定义只局限在它本身这一行,因此需要显式的添加续行符以便连接代码。

技巧3:宏定义中出现多条语句的情况,要显式的添加续行符\进行连接

代码改完后,我们又会发现另一个问题,那就是语句有三条,导致宏并不具有返回值,进而我们不能够直接打印。这怎么办?不用担心,这个问题我们后续会解决,现在我们先改变一下需求:我们暂时不限定宏一定要有返回值,而只要能得到最大值即可,于是我们有了如下代码:

#include<stdio.h>

int max_num = 0;

#define my_max(num_1, num_2)    \
    int num_1_temp = (num_1);    \
    int num_2_temp = (num_2);    \
    max_num = (num_1_temp) > (num_2_temp) ? (num_1_temp) : (num_2_temp);

int main()
{
    int num_1 = 2;

    my_max(num_1++, 1);
    printf("%d\n", max_num);

    return 0;
}

如上述代码,我们用一个全局变量max_num去承接最大值,然后加以输出即可。之后我们测试一下:
在这里插入图片描述
没有问题,之后我们我们在主函数开头添加一行代码如下:

int num_1_temp = 10;

再来测试一番:
在这里插入图片描述
编译器提示重复定义,这是什么原因呢?
我们发现对于变量num_1_temp,在主函数中定义过一次,宏定义展开后,里面又定义过一次,所以自然造成了重复定义。

int num_1_temp = 10;
int num_1 = 2;
/* 宏定义展开后 */
int num_1_temp = num_1++;

那么如何修改,我们知道两个变量如果在不同的作用域中,它们的命名就不会冲突,比如不同函数中的局部变量全局变量和函数中的局部变量函数中的局部变量和循环结构中的局部变量
所以解决方案就是给宏定义加上{}花括号,让它的代码段在自己的作用域中即可,同时记得加上续行符。
修改后的宏定义如下:

#define my_max(num_1, num_2)    \
{    \
    int num_1_temp = (num_1);    \
    int num_2_temp = (num_2);    \
    max_num = (num_1_temp) > (num_2_temp) ? (num_1_temp) : (num_2_temp);    \
}

再进行测试,顺带输出num_1_temp的值进行观察:
在这里插入图片描述
问题解决了,而且我们主函数中的num_1_temp的值并没有被改变
技巧4:宏定义中出现变量定义的情况,要使用{}将代码包含在其中

注:如果我们将num_1_temp传入宏当中,对于Visual Stdio的编译器来说会报错告知使用了未初始化的变量,原因是在进行宏替换后,变为了int num_1_temp = num_1_temp,这样自然会出现问题,而在gcc下,会初始化为0,之后加以使用,但这种方式一般也得不到我们想要的答案。这里对这种情况不多做考虑,有兴趣的读者可以自行研究。

再来测试一番,测试代码如下:

int main()
{
    if (1)
        my_max(7, 1);
    else
        my_max(3, 1);

    printf("%d\n", max_num);

    return 0;
}

代码并不规范(故意为之),而且总会进入if分支,不过这不重要,重要的是程序报错了,错误如下:
在这里插入图片描述
else分支找不到匹配的if,这是怎么回事,我们冷静分析一番,将宏定义展开如下:

int main()
{
    if (1)
    {
        int num_1_temp = (7);
        int num_2_temp = (1);
        max_num = (num_1_temp) > (num_2_temp) ? (num_1_temp) : (num_2_temp);
    };
    else
    {
        int num_1_temp = (3);
        int num_2_temp = (1);
        max_num = (num_1_temp) > (num_2_temp) ? (num_1_temp) : (num_2_temp);
    };

    printf("%d\n", max_num);

    return 0;
}

显然,进行宏替换后,花括号成为了if语句结构的一部分,我们按照正常习惯,在使用宏的时候添加了;,所以导致if语句被强行结束,else自然无法匹配到if语句了。
知道了问题后,如何修改呢?

第一种方式

当然,在if-else分支结构上都加上完备的花括号,就可以解决问题,而且这也是编程规范之一。但这显然不是从内部解决问题的方法,如果有人就是喜欢不加花括号呢?
没关系,还有余下两种方法可以解决这个问题。
第二种方式
介绍解决方法之前,我们先看看程序中隐藏的一个BUG,是的,当参数的大小超过了int类型所能表示的最大值时,那么就会发生溢出现象,无法得到我们要的答案。

所以我们再添加一条要求:当参数大小超过int类型所能表示的最大值时就停止执行宏定义,并抛出异常。

这次我们仔细分析下,发现实际上我们要做的就是在适时的时候让宏定义break,而不能是return,因为return会直接结束调用宏定义的函数,这不是我们想要的。
提到break,我们就只能从三个循环语句下手,而且代码中孤零零的{}也已经暗暗提示我们需要一个循环关键词与之匹配。那么,对于循环体,我们所熟悉的有三个:forwhiledo{}while

到底使用哪一个?
答案是使用do{}while语句

for循环又臭又长,首先不考虑。
之后我们考虑到我们只需要执行一次宏定义内的代码块,那么使用while语句控制程序只循环一次,则需要使用一个额外的循环变量。
但使用do{}while(0)便可以轻松实现只循环一次的功能。
而且它正好可以适应我们在语句末添加;封号的习惯,让我们不用考虑调用宏定义与调用函数的区别,直接解决这个问题。

所以最后出于能少写代码则少写代码符合用户使用习惯的原则,我们选择使用do{}while结构,代码如下:

#define my_max(num_1, num_2)do    \
{    \
    if (num_1 > 0x7fffffff || num_2 > 0x7fffffff)    \
    {    \
        break;    \
    }    \
    int num_1_temp = (num_1);    \
    int num_2_temp = (num_2);    \
    (num_1_temp) > (num_2_temp) ? (num_1_temp) : (num_2_temp);    \
}while(0)

我们有了思路后,测试一下需功能是否完善
在这里插入图片描述
没有问题,其中第二次因为break,所以输出的值没有改变。
再测试一下之前的if-else分支错误
在这里插入图片描述
也没有问题,于是我们又学会了一条技巧

技巧5:宏定义中出现多条语句的情况,可以使用do{}while(0)语句将代码包含在其中,便于中断代码执行

第三种方式

介绍第三种方法之前,我们再回到开始遗留的那个问题,我们的宏并没有返回值,那么如何解决它,答案就是将宏定义写为可以作为右值的形式即可。
代码也很简单,就是在{}的两边加上(),因为加上()后,花括号中的代码块就可以以一个整体被作为右值赋给其他变量,而且也可以正常的添加;结尾。
()内的最后一条语句应该对应于最终的赋值,代码如下:

#define my_max(num_1, num_2)    \
({    \
    int num_1_temp = (num_1);    \
    int num_2_temp = (num_2);    \
    (num_1_temp) > (num_2_temp) ? (num_1_temp) : (num_2_temp);    \
})

我们接着测试一番,不过遗憾的是Visual Stdio的编译器因为对安全性的考虑,并不支持这样的野鸡写法,好在万能的gcc可以编译,测试结果如下:
在这里插入图片描述
没有问题,对于if-else错误进行测试如下
在这里插入图片描述

也没有问题,至此我们得到了最后一条技巧

技巧6:宏定义中出现多条语句的情况,可以使用({})将代码块包含在其中,获得代码块最后一条语句的值作为返回值。
----------------------------------------------至此,我们解决了所有问题。------------------------------------

总结

技巧1:宏定义的主体代码要加完备的括号

技巧2:宏定义主体代码中的参量也要加完备的括号

技巧3:宏定义中出现多条语句的情况,要显式的添加续行符\进行连接

技巧4:宏定义中出现变量定义的情况,要使用{}将代码包含在其中

技巧5:宏定义中出现多条语句的情况,可以使用do{}while语句将代码包含在其中,便于中断代码执行

技巧6:宏定义中出现多条语句的情况,可以使用({})将代码块包含在其中,获得代码块最后一条语句的值作为返回值。

最后,切记,善于#define可以让你写程序变得更加顺畅,定义更加明确,代码冗余减少,更加清(nan)新(yi)脱(yue)俗(du)。
没有正确使用#define,会让你的代码成为BUG源头。

后记

记得之前看到过一篇博文,讲的是#define宏定义与const常量和inline函数之间的一些对比,最后博主得出结论,几乎只要可以使用#define的情况都可以用后两者代替,所以尽量少使用#define

说的对不对呢?说对了一半吧,依旧用我们所设计的这个宏做例子,它要完成判断非法输入,还要具有返回值,几乎做了一个函数应该做的事情。而且还有这么多坑点需要注意,那我为什么不直接写个函数,加上inline,也能达到同样的效果啊,还不用考虑那么多复杂的情况。

是的,这样的确可以,而且对于复杂的宏定义,的确写为函数效果会更好。
但并不意味着#define宏定义将失去它的地位。比如我前文所说的那些宏定义的优点,就是其他方法无法做到的。而且知用、善用宏定义更是一个优秀的C/C++程序员所应该具备的能力。

参考资料

a>b?a:b就结束了?五级max()函数宏,你能写到第几级?

  • 12
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值