C/C++ 如何构造出强悍的宏
目录
前言
1、基础复习:表达式、语句和代码块
1.1 表达式
1.2 语句
1.3 代码块
2、语句表达式
2.1 什么是语句表达式?
2.2 语句表达式内使用 goto 跳转
3、在宏定义中使用语句表达式
3.1 合格的写法
3.2 中等的写法
3.3 良好的写法
3.4 优秀的写法
3.5 最强悍的写法
3.6 在 Linux 内核中使用的示例
4、总结
前言
在使用 C/C++ 编写代码过程中,若能合理的利用宏这把利器,将能帮助我们很舒畅的写出非常美观而实用的代码。那么如何才能写出牛逼哄哄的宏呢?本章将基于语句表达式这把利器来阐述宏构造的高级写法。
1、基础复习:表达式、语句和代码块
为了兼顾零基础的 C/C++ 童鞋们,我们先来简单的复习一下关于本章需要用到的基础知识:表达式、语句和代码块。
1.1 表达式
- 表达式和语句是 C/C++ 语言中的基础概念。
- 什么是表达式呢?表达式就是由一系列操作符和操作数构成的式子:
- 操作符,可以是 C/C++ 语言标准规定的各种算术运算符、逻辑运算符、赋值运算符、比较运算符等。
- 操作数,可以是一个常量,也可以是一个变量。
- 不过,表达式也可以没有操作符,单独的一个常量或者一个字符串,也可以是一个表达式。例如下面的字符序列都是表达式:
1) 1 + 2 2) 6 3) sum = 2 + 3 4) sum = ++i + 5 5) "sdhf"
- 表达式一般用来实现某种功能的算法。根据操作符的不同,表达式可以分为多种类型,如:
- 关系表达式
- 逻辑表达式
- 条件表达式
- 赋值表达式
- 算术表达式
- ……
1.2 语句
- 语句是构成程序的基本单元,一般形式如下:
表达式: sum = 1 + 2;
- 表达式的后面加一个分号; 就构成了一条基本的语句。编译器在编译程序、解析程序时,不是根据物理行,而是根据分号 ; 来判断一条语句的结束标记的。如 sum = 1 + 2; 这条语句,你写成下面的样子也是可以编译通过的:
sum = 1 + 2 ;
1.3 代码块
- 一个代码块由不同的语句使用大括号 {} 括起来构成。C/C++ 语言允许在代码块里定义一个变量,这个变量的作用域也仅限于这个代码块内,因为编译器就是根据 {} 来做入栈出栈操作来管理变量的作用域的。如下面的程序:
int main(void) {int i = 7;printf("i = %d\n", i);{int i = 8;printf("i = %d\n", i);} printf("i = %d\n", i);return 0; }
运行结果如下: i = 7 i = 8 i = 7
2、语句表达式
经过以上的基础知识回顾,现在我们就正式进入本章的话题吧。
2.1 什么是语句表达式?
C/C++ 允许在一个表达式里内嵌语句,允许在表达式内部使用局部变量、for 循环和 goto 跳转等语句。这样的表达式,我们称之为语句表达式。语句表达式的格式如下:
({ 表达式1; 表达式2; 表达式3; })
语句表达式最外面使用小括号 () 括起来,里面一对大括号 {} 包起来的是代码块,代码块里允许内嵌各种语句。跟一般表达式一样,语句表达式也有自己的值。语句表达式的值为内嵌语句中最后一个表达式的值。下面我们使用语句表达式求值:
int main(void)
{int sum = 0;sum =({int i = 0;int count = 0;for (i = 0; i < 5; i++){count = count + i;}count; //注意:语句表达式的值等于最后一个表达式的值,此句必须添加!!!});printf("sum = %d\n", sum); return 0;
}
运行结果:
sum = 10
在上面的程序中,在 for 循环的后面要添加一个 count; 语句,它表示整个语句表达式的值。如果不加这一句,你会发现 sum = 0。或者改为 sum = 100,你会发现最后 sum 的值就变成了100,这是因为语句表达式的值总等于最后一个表达式的值。
2.2 语句表达式内使用 goto 跳转
在上面的程序中,我们在语句表达式内定义了局部变量,使用了 for 循环语句。在语句表达式内,同样也可以使用 goto 进行跳转:
int main(void)
{int sum = 0;sum =({int i = 0;int count = 0;for (i = 0; i < 5; i++){count = count + i;}goto end;count; //上一语句使用了goto,此句已不生效!!!});printf("sum = %d\n", sum);
end:printf("here sum end:\n");printf("sum = %d\n", sum);return 0;
}
运行结果:
here sum end:
sum = 0
在上面的程序中,你会发现由于goto提前结束了语句表达式,并没有执行到最后一个表达式的值,所以运算结果为 sum = 0。
3、在宏定义中使用语句表达式
一般情况下语句表达式都是用在定义复杂功能的宏。使用语句表达式来定义宏,不仅可以实现复杂的功能,而且还能避免宏定义带来的歧义和漏洞。下面就以“定义一个宏,求两个数的最大值”为例,让我们来感受一下语句表达式在宏定义中的威力!
3.1 合格的写法
对于学过 C/C++ 语言的童鞋们,写出这个宏根本就不是什么难事,不就使用个条件运算符就能完成了吗:
#define MAX(x, y) x > y ? x : y
这是最基本的 C 语言语法,如果你连这个也写不出来,那你赶紧提起精神好好看完这篇吧。就算你能把这个宏能写出来,也请不要夜郎自大,沾沾自喜了,因为这只能说明你还是有点 C 语言基础的,但是还有很大的进步空间。比如,我们写一个程序,验证一下这个宏是否严谨:
#define MAX(x, y) x > y ? x : yint main(void)
{printf("max = %d\n", MAX(5, 6));printf("max = %d\n", MAX(6, 5));printf("max = %d\n", MAX(5, 5));printf("max = %d\n", MAX(5!=5, 5!=6));return 0;
}
当程序运行到第4行,这时宏的参数是一个表达式,发现实际运行结果为 max=0,而跟我们预期的结果 max=1 不一样。原因是宏展开后就变成了这个样子:
printf("max = %d", 5 != 5 > 5 != 6 ? 5 != 5 : 5 != 6);
因为比较运算符 >(优先级为6)大于 !=(优先级为7),所以展开的表达式,运算顺序发生了改变,结果就跟我们的预期不一样了。
3.2 中等的写法
为了避免以上宏展开后出现的错误,我们可以给宏的参数加一个小括号()来防止展开后,表达式的运算顺序发生变化。这样才算一个合格的宏:
#define MAX(x,y) (x) > (y) ? (x) : (y)
可是这个宏只能算合格,但还是存在漏洞。比如,我们使用下面的代码测试:
#define MAX(x,y) (x) > (y) ? (x) : (y)int main(void)
{printf("max = %d\n", 6 + MAX(3, 4));return 0;
}
在程序中,我们打印表达式 6 + MAX(3, 4) 的值,预期结果应该是10,但实际运行结果却是3。我们展开后,发现同样有问题:
6 + (3) > (4) ? (3) : (4);
因为运算符 + 的优先级大于比较运算符 >,所以这个表达式就变为 9 > 4 ? 3 : 4,最后结果为 3 也就不足为怪了。
3.3 良好的写法
为了改良以上的宏定义存在的问题,我们可以使用小括号()将宏定义包起来,这样就避免了当一个表达式同时含有宏定义和其它高优先级运算符时,破坏整个表达式的运算顺序:
#define MAX(x,y) ((x) > (y) ? (x) : (y))
这个宏虽然解决了运算符优先级带来的问题,但仍存在漏洞。比如,我们使用下面的测试程序来测试一下:
#define MAX(x,y) ((x) > (y) ? (x) : (y))int main(void)
{int i = 3;int j = 9;printf("max = %d\n", MAX(i++, j++));return 0;
}
以上程序中定义了两个变量 i 和 j,比较两者的大小,并作自增运算。实际运行结果发现 max = 10,而不是预期结果 max = 9。原因是变量 i 和 j 在宏展开后,做了2次自增运算。如下所示:
(i++) > (j++) ? (i++) : (j++)
注:i++, j++ 是先使用变量 i 和 j 的值,然后再将 i 和 j 的值递增加1。
3.4 优秀的写法
遇到以上的情况,有没有解决的办法呢?答案肯定是有的,这时我们可以使用语句表达式来定义这个宏,并在语句表达式中定义两个临时变量 __x 和 __y,分别来暂储 i 和 j 的值,然后进行比较,这样就避免了变量的2次自增、自减问题。
#define MAX(x, y) ({ \int __x = x; \int __y = y; \__x > __y ? __x : __y; })int main(void)
{int i = 3;int j = 9;printf("max = %d\n", MAX(i++, j++));return 0;
}
运行程序后,会发现运算结果与我们预期的一致 max = 9。
在上面这个宏中,发现定义的两个临时变量数据类型是 int 型,只能比较两个整型的数据。那么对于其它类型的数据,还需要另外再定义一个宏,这样太麻烦了!我们可以基于这个宏继续优化,让它可以支持任意类型的数据比较大小:
#define MAX(type, x, y) ({ \type __x = x; \type __y = y; \__x > __y ? __x : __y; })int main(void)
{int i = 3;int j = 9;printf("max = %d\n", MAX(i++, j++));return 0;
}
在这个宏中,我们添加一个参数:type,用来指定临时变量 __x 和 __y 的类型。这样,我们在比较两个数的大小时,只要将2个数据的类型作为参数传给宏,就可以比较任意类型的数据了。
3.5 最强悍的写法
如果你能把上面的宏写出来 ,可以说你是可以稍微傲娇一下了。不过人外有人,山外有山,对于这样一个宏,我们还能不能进一步优化的更优秀呢?答案也是肯定的,大招使出来,弹唱一曲“无敌是多么,多么空虚~~”。
在上面的宏,虽然增加了一个 type 参数来兼容不同的数据类型,但我还是觉得参数太多了,总感觉欠缺了一点高级的味道。这时我们不妨将 type 参数去除,改用 typeof 来获取数据类型(如果想了解的更细致,请看 typeof 关键字的作用):
#define max(x, y) ({ \typeof(x) __x = (x); \typeof(y) __y = (y); \(void) (&__x == &__y); \__x > __y ? __x : __y; })
在这个宏定义中,隐藏了一句干货知识就是 (void) (&x == &y); 这个设计太精妙了,它包含了两方面的用途:
1)如果对于 __x 和 __y 是不同类型的指针比较,编译器会给一个警告,提示两种数据类型不同;
2)当两个值比较,比较的结果没有用到,有些编译器可能会给出一个warning,加个(void)后,就可以消除这个警告!
3.6 在 Linux 内核中使用的示例
语句表达式,作为 GNU C 对 C 标准的一个扩展,在 Linux 在内核的宏定义中,被大量的使用。使用语句表达式定义宏,不仅可以实现复杂的功能,还可以避免宏定义带来的一些歧义和漏洞。比如,在 Linux 内核中 min 和 max 的宏定义,就使用了语句表达式:
#define min(x, y) ({ \typeof(x) _min1 = (x); \typeof(y) _min2 = (y); \(void) (&_min1 == &_min2); \_min1 < _min2 ? _min1 : _min2; })#define max(x, y) ({ \typeof(x) _max1 = (x); \typeof(y) _max2 = (y); \(void) (&_max1 == &_max2); \_max1 > _max2 ? _max1 : _max2; })
4、总结
宏的构造与使用的方法还有很多,而本章内容仅仅是列举了一个关于语句表达式构造宏的基础方法,希望能给大家带来触类旁通,举一反三的效果。
C/C++ 如何构造出强悍的宏相关推荐
- LeetCode 1798. 你能构造出连续值的最大数目
文章目录 1. 题目 2. 解题 1. 题目 给你一个长度为 n 的整数数组 coins ,它代表你拥有的 n 个硬币. 第 i 个硬币的值为 coins[i] .如果你从这些硬币中选出一部分硬币,它 ...
- 计算机图形学是指使用计算机通过,【填空题】计算机图形学是指使用计算机通过()在显示设备上构造出图形来。...
[填空题]计算机图形学是指使用计算机通过()在显示设备上构造出图形来. 更多相关问题 跨立,左脚跨出时,腿弯曲.纠正方法:左脚跨出时, ( ) . 在IS曲线和LM曲线相交时,表示产品市场( ) 6. ...
- 【原创】如何判断三条边能否构造出一个三角形
一.三角形的定义 众所周知,三角形是由不在同一直线上的三条线段首尾顺次连接所组成的封闭图形. 常见的三角形按边分有普通三角形(三条边都不相等),等腰三角(腰与底不等的等腰三角形.腰与底相等的等腰三角形 ...
- 深度 | 谷歌的新CNN特征可视化方法,构造出一个华丽繁复的新世界
作者:晓凡 概要:近日,来自谷歌大脑和谷歌研究院的一篇技术文章又从一个新的角度拓展了人类对神经网络的理解,得到的可视化结果也非常亮眼.非常魔性. 深度神经网络解释性不好的问题一直是所有研究人员和商业应 ...
- 如何锻炼出强悍的分析能力?
一.提升了分析能力的6个核心要点. 1 明确目标,梳理框架,积累模型 2 溯源延伸,深入本质,把握核心 3 拆解要素,逐个分析,关联思考 4 系统思维,多维视角,整合要素 5 丰富渠道,数据处理,寻找 ...
- 全加器构造出补码加法/减法器
具体构造实验结果如下: 全加器补码加/减法器模拟电路图. 图 1 四位全加补码加/减法器模拟电路图 根据上图1,得到如图 6.7 所示的实验电路 图 2 四位全加补码加/减法器设计电路图 图 2 四位 ...
- python中构造出一个空的DataFrame,并在循环中不断往dataframe里面添加内容
这是管培期间的一个学习任务里面其中解决问题用到的方法 关键是下面的result.loc()巧妙地利用len()获取dataframe数据长度 import pandas as pd import nu ...
- 有4个节点可以构造出 二叉树_简单4个步骤就可以拍摄出美丽的城市夜景,赶紧试试吧...
主题与构图 由于光线的缘故,许多白天看上去动人的景色都会为夜色所遮盖,我们所拍摄的则只剩下暗夜里五彩缤纷的灯火与色彩,可以说拍摄夜景其实就是拍摄夜间最具有特色的光线和色彩,我们首先应该确定自己拍摄的照 ...
- C++ 在一个类中用指针的形式申请堆内存构造出另一个类并访问私有成员变量(包括智能指针版本)
一.普通指针版本 代码 #include<iostream> using namespace std;class A {public:A(){ax=2;cout<<" ...
最新文章
- 小程序仿 axios 请求封装
- android 源码各版本下载 原生纯净版
- 池化层在全连接层之间吗,了解最大池化层之后的全连接层的尺寸
- android vcard解析代码,Android使用vcard文件的方法简单实例
- 根据屏幕大小制定显示页面
- react系列之isMounted is an Antipattern
- java中example函数作用,MyBatis逆向工程中的Mapper接口以及Example的实例函数及详解...
- ref:LDAP入门
- Windows远程桌面实现之十二:桌面屏幕通过ONVIF协议与NVR等监控录像设备对接,以及进一步增强直播功能
- Java代码混淆工具:JavaObfuscator介绍
- 微信小程序 布局对齐属性
- 20162327WJH实验五——数据结构综合应用
- 云知声打磨四年的AI芯片出炉,中国造芯势力群起
- 【绝知此事要躬行】线性表之链表OJ(下)
- 内存的基础知识(常用数量单位、进程运行原理、存储单元、内存地址、绝对装入、静态重定位、动态重定位、静态链接、动态链接等)
- 使用cocos2d-x实现一款类似《Flappy Bird》的游戏
- Ubuntu18.04安装美化工具tweak 和 dash to dock
- VS C++ 和 MCR 调用MATLAB DLL
- 354. Russian Doll Envelopes刷题笔记
- 利用Python探测附近WIFI密码的详细代码