C/C++ 如何构造出强悍的宏

news/2024/4/18 15:32:32/文章来源:https://blog.csdn.net/m0_37383484/article/details/129216649

目录

前言

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、总结

宏的构造与使用的方法还有很多,而本章内容仅仅是列举了一个关于语句表达式构造宏的基础方法,希望能给大家带来触类旁通,举一反三的效果。

 



 

 

 

 

 

 

 

 

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_75417.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux: malloc()的指向指针发生指向偏移后,释放前需要将指针指向复原。

Linux: malloc()的指向指针发生指向偏移后&#xff0c;释放前需要将指针指向复原。 #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <string.h> #include <time…

MIT:只需一层RF传感器,就能为AR头显赋予“X光”穿透视力

近年来&#xff0c;AR在仓库、工厂等场景得到应用&#xff0c;比如GlobalFoundries、亚马逊、菜鸟裹裹就使用摄像头扫描定位货品&#xff0c;并使用AR来导航和标记。目前&#xff0c;这种方案主要基于视觉算法&#xff0c;因此仅能定位视线范围内的目标。然而&#xff0c;在一些…

C++ string类(二)及深浅拷贝

一、string类方法使用举例1.迭代器迭代器本质&#xff1a;指针&#xff08;理解&#xff09;迭代器&#xff1a;正向迭代器&#xff1a; begin() | end() 反向迭代器&#xff1a; rbegin() | rend()2.find使用//找到s中某个字符 void TestString3() {string s("AAADEFNUIE…

携程面经1

面经 HDFS读写流程 1.读流程 客户端向NameNode发起读请求&#xff08;如果存在&#xff09;NameNode返回一批block地址客户端与第一个block的拓扑距离最近的节点建立连接以packet&#xff08;64kb&#xff09;的单位读取数据块。一个block读取完成后客户端会断开与该DataNod…

5个开源的Java项目快速开发脚手架

概览 &#xff1a; GunspigRuoYiJeecg-bootiBase4J 一、Guns 推荐指数 &#xff1a;⭐⭐⭐⭐⭐ 简介 采用主流框架 &#xff1a; 基于 Spring Boot2.0版本开发&#xff0c;并且支持 Spring Cloud Alibaba 微服务。功能齐全 &#xff1a;包含系统管理&#xff0c;代码生成&a…

这么强才给我28k,我头都不回,转身拿下40k~

时间真的过得很快&#xff0c;眨眼就从校园刚出来的帅气小伙变成了油腻大叔&#xff0c;给各位刚入道的测试朋友一点小建议&#xff0c;希望你们直通罗马吧&#xff01; 如何选择自己合适的方向 关于选择测试管理&#xff1a; 第一&#xff0c;你一定不会是一个喜欢技术&…

Vue的组件(注册、局部、组件复用、props、emit、生命周期)全解

文章目录前言知识点组件注册局部组件组件复用组件间通信props 类型检测子父组件通信之 emit动态组件生命周期函数前言 Vue 支持模块化和组件化开发&#xff0c;可以将整个页面进行模块化分割&#xff0c;低耦合高内聚&#xff0c;使得代码可以在各个地方使用。 知识点 组件注册…

accent-color一行代码,让你的表单组件变好看

不做切图仔,从关注本专栏开始 文章目录 不做切图仔,从关注本专栏开始前言兼容性语法继承性智能前言 在之前的网站开发中,我们是很难去更改的你某些控件的颜色。我们可能要使用各种技巧来自定义我们的控件。好消息是,今天如果我们想要去改变控件的颜色,css为我们提供了一些…

心系区域发展,高德用一体化出行服务平台“聚”力区域未来

交通&#xff0c;是城市的血脉。通过对人、资源、产业的连接&#xff0c;交通建设往往是城市和区域经济发展的前提。不过&#xff0c;在度过了“要想富&#xff0c;先修路”的初级建设阶段后&#xff0c;交通产业内部也出现了挑战&#xff0c;诸如城市秩序、发展成本、用户使用…

【目标检测】Dynamic Head Unifying Object Detection Heads with Attentions

文章目录一、背景二、方法2.1 scale-aware attention2.2 spatial-aware attention2.3 task-aware attention2.4 总体过程2.5 和现有的检测器适配2.6 和其他注意力机制的关联三、效果四、代码论文链接&#xff1a; https://arxiv.org/pdf/2106.08322.pdf代码链接&#xff1a;htt…

Windows 安装RocketMQ

文章目录一、RocketMQ是什么&#xff1f;二、准备工作1.环境要求2.下载与解压3.启动MQ4. 测试是否成功启动三、安装管理端1. 代码下载2. 修改配置文件3. 启动MQ客户端jar包四、rocketMQ代码的使用入门五、问题记录1. 启动mqbroker.cmd没有反应2.消费者重复消费消息一、RocketMQ…

NCRE计算机等级考试Python真题(六)

第六套试题1、算法的时间复杂度是指A.执行算法程序所需要的时间B.算法程序的长度C.算法程序中的指令条数D.算法执行过程中所需要的基本运算次数正确答案&#xff1a; D2、下列关于栈的叙述中正确的是A.在栈中只能插入数据B.在栈中只能删除数据C.栈是先进先出的线性表D.栈是先进…

【Django功能开发】如何正确使用定时任务(启动、停止)

系列文章目录 【Django开发入门】ORM的增删改查和批量操作 【Django功能开发】编写自定义manage命令 文章目录系列文章目录前言一、django定时任务二、django-apscheduler基本使用1.安装django-apscheduler2.配置settings.py的INSTALLED_APPS3.通过命令生成定时记录表3.如何创…

嵌入式 linux 系统开发网络的设置

目录 一、前言 二、linux网络静态地址设置 前言 为什么要对linux系统下的ubuntu进行网络设置呢&#xff1f; 因为我们在嵌入式开发中&#xff0c;我们要保证windows系统、linux系统、开发板的ip要处于同一个网段&#xff0c;而默认ubuntu下的linux系统的ip是动态分配的&#…

如何彻底删除SQL Server 2008中的登录账号

我个人遇到的最烦人的事情之一是 SQL Server Management Studio中“服务器名称和登录名”对话框的下拉列表。 以下是我想从 SSMS 连接屏幕中删除某些内容的两种情况: 键入的服务器名称不正确 服务器将来不需要。当我看到服务器的名称,它已经存在了很长一段时间,我知道我不会…

图像处理实战--Opencv实现人像迁移

前言&#xff1a; Hello大家好&#xff0c;我是Dream。 今天来学习一下如何使用Opencv实现人像迁移&#xff0c;欢迎大家一起参与探讨交流~ 本文目录&#xff1a;一、实验要求二、实验环境三、实验原理及操作1.照片准备2.图像增强3.实现美颜功能4.背景虚化5.图像二值化处理6.人…

Day21【元宇宙的实践构想07】—— 元宇宙与人工智能

&#x1f483;&#x1f3fc; 本人简介&#xff1a;男 &#x1f476;&#x1f3fc; 年龄&#xff1a;18 &#x1f91e; 作者&#xff1a;那就叫我亮亮叭 &#x1f4d5; 专栏&#xff1a;元宇宙 0.0 写在前面 “元宇宙”在2021年成为时髦的概念。元宇宙到底是什么&#xff1f;元宇…

【论文笔记】Decoupling Representation and Classifier for Long-Tailed Recognition

这一篇其实并不是提出什么新的东西&#xff0c;而且是做了点类似综述的技术调用实验。省流&#xff1a;T-normalization最好用 摘要 现状&#xff1a;Existing solutions usually involve class-balancing strategies, e.g. by loss re-weighting, data re-sampling, or tran…

高燃!GitHub上标星75k+超牛的Java面试突击版

前言不论是校招还是社招都避免不了各种面试。笔试&#xff0c;如何去准备这些东西就显得格外重要。不论是笔试还是面试都是有章可循的&#xff0c;我这个有章可循‘说的意思只是说应对技术面试是可以提前准备。运筹帷幄之后&#xff0c;决胜千里之外!不打毫无准备的仗,我觉得大…

扒系统CR8记录

目录 终极改造目标 过程记录 参考 为了将一套在线安装的系统&#xff0c;在不了解其架构、各模块细节的基础上&#xff0c;进行扒弄清楚&#xff0c;作以下记录。 终极改造目标 最终的目标&#xff0c;就是只通过CreMedia8_20230207.tar.gz解压 install 就把业务包安装了&…