【C语言】#define宏与函数的优劣对比

本篇文章目录

  • 1. 预处理指令#define宏
  • 2. #define定义标识符或宏,要不要最后加上分号?
  • 3.宏的参数替换后产生的运算符优先级问题
    • 3.1 问题产生
    • 3.2 不太完美的解决办法
    • 3.3 完美的解决办法
  • 4.#define的替换规则
  • 5. 有副作用的宏参数
  • 6. 宏与函数的优劣对比
    • 6.1 宏的优点
      • 6.1.1 宏的执行速度更快
      • 6.1.2 宏不关心参数类型
      • 6.1.2 宏的参数可以出现数据类型
    • 6.2 宏的缺点
  • 7. 总结宏和函数的对比
  • 8. 宏的命名约定

1. 预处理指令#define宏

#define除了能定义标识符常量外,还允许把参数替换到文本中,这种实现通常称为(macro)或定义宏(define macro)。

下面是宏的申明方式:

#define name(parament-list) statement

其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在statement中。

例如实现一个宏,求两个数中的最大值:

#define MAX(x, y) ((x) > (y) ? (x) : (y))

参数列表的左括号必须与name紧邻。如果两者之间有任何空白存在,参数列表就会被解释为statement的一部分。

如:

#define MAX (x, y) ((x) > (y) ? (x) : (y))

MAX后面加了一个空格,这样写的话是没法使用的,(x, y)会被误认为是表达式而不是宏的参数。

2. #define定义标识符或宏,要不要最后加上分号?

比如:

#define PI 3.14;
#define MAX(x, y) ((x) > (y) ? (x) : (y));

建议不要加上分号,这样容易导致问题,比如下面的场景:

int max;
int a = 10;
int b = 20;
if(condition)max = MAX(a, b); // error
elsemax = 0;

我们都知道#define在预编译后就会完成符号的替换,在代码中所有出现过#define定义的标识符或宏,都会被替换成所代表的常量或宏参数。

那么上面这么写的话,实际上就变成了这样:

int max;
int a = 10;
int b = 20;
if(condition)max = MAX((a) > (b) ? (a) : (b));;
elsemax = 0;

那么后面会有两个分号,一个分号是#define后面带上的,另一个是编码习惯性地带上一个分号。

最关键的是这样替换后的代码,在你实际编写的.c源文件中是看不到的,所以在你的眼中代码任然长这样:

int max;
int a = 10;
int b = 20;
if(condition)max = MAX(a, b); 
elsemax = 0;

这样甚至都不需要去编译运行,IDE就能早早发现错误给你报错了,if后没接大括号只能有一条语句,而实际上宏参数被替换后多了个分号,这个分号虽然没有实际的意义,但它也是一条语句。

在这里插入图片描述

3.宏的参数替换后产生的运算符优先级问题

3.1 问题产生

如果你写了一个这样的宏,求一个数的平方:

#define SQUARE(n) n * n

然后使用这个宏:

int ret = SQUARE(4);
printf("%d", ret);

咋一看肯定没毛病,能得出正确的结果:
在这里插入图片描述

但如果这么使用:

int ret = SQUARE(4 + 1);
printf("%d", ret);

这时你心里想结果是25,实际运行后的结果却是:
在这里插入图片描述
为啥呢?其实表达式预编译后长这样:

int ret = 4 + 1 * 4 + 1;
printf("%d", ret);

#define完成的是符号的替换,无论是定义的标识符常量还是宏,要么就是把标识符替换成常量,要么就是将宏表达式的参数替换成你传入的参数。仅仅只是替换工作,并不会帮你计算好再传参,要知道计算的工作是真正在程序运行后才能执行的,预编译阶段才仅仅是编译的第一个阶段呢!文章:.c源文件从编译到链接生成可执行程序的过程

3.2 不太完美的解决办法

解决的办法就是在宏体表达式中,给每个参数加上括号:

#define SQUARE(n) (n) * (n)

这样就确实能得到正确的结果:
在这里插入图片描述

3.3 完美的解决办法

但实际上像上面这样加上括号任然存在问题!比如有这么一个宏:

#define DOUBLE(n) (n) + (n)

然后我这么使用:

int ret = 10 * DOUBLE(5);
printf("%d", ret);

预测结果是100,结果却是:
在这里插入图片描述
经过前面的分析,大伙也不难分析出问题是怎样产生的,原因就是预编译后替换成了这样:

int ret = 10 * (5) + (5);
printf("%d", ret);

这个问题,的解决办法是在宏定义表达式两边加上一对括号就可以了。

// #define DOUBLE(n) (n) + (n)
#define DOUBLE(n) ((n) + (n))

所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。

4.#define的替换规则

在程序中扩展#define定义符号和宏时,需要涉及几个步骤。

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
  2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。

其它:

  1. 宏参数和#define 定义中可以出现其他#define定义的符号。
#define N 10
#define CAL(x) ((x) + N) 
  1. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
#define N 10
#define STR "NOT"
printf("No");

"NOT"中的N并不会被替换,"No"中的N也不会被替换。

  1. 对于宏,不能出现递归。

5. 有副作用的宏参数

宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。

#include <stdio.h>
#define MAX(x,y) ((x) > (y) ? (x) : (y))
int main() {int a = 10;int b = 20;int max = MAX(a++, b++);printf("%d\n", max);printf("%d\n", a);printf("%d\n", b);
}

你可能会认为三个输出的结果分别是:20、11和21,理由是a++和b++都是后置++,那么传入的理应是10和20,求出较大值是20,然后a和b自增后分别是11和21,而实际结果是:
在这里插入图片描述
将参数替换后倒也很容易发现问题的产生原因:

//int max = MAX(a++, b++);
int max = ((a++) > (b++) ? (a++) : (b++));

(a++) > (b++)比较后肯定为假,但是a和b的值都要被自增为11和21,然后整个表达式的值是b++,b++这个表达式的结果也是b,所以整个表达式的结果是21,但是b++后b要自增为22。

所以在使用宏传参时,应该这么写更合适:

int max = MAX(a + 1, b + 1);

6. 宏与函数的优劣对比

先总结:宏通常被应用于执行简单的运算,比如这种:

#define MAX(a, b) ((a)>(b)?(a):(b))

那为什么不用函数来完成这个任务?

6.1 宏的优点

6.1.1 宏的执行速度更快

用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多,所以宏比函数在程序的规模和速度方面更胜一筹。

说简单点就是调用函数也需要时间,函数返回也需要时间,如果计算工作过于简单,可能函数调用和函数返回的时间都比计算过程花费的时间要长。

①这里使用反汇编直接看汇编代码的行数进行对比,在VS中对下面这段程序进行debug:

在这里插入图片描述

②然后右击鼠标,转到反汇编代码:

在这里插入图片描述

③查看执行这个宏的汇编代码:

在这里插入图片描述

④查看执行这个函数的汇编代码:

在这里插入图片描述

⑤这咋一看你会认为好像代码行数更少啊!但仔细看其中有个call指令,这是函数调用指令,所以这并不是真正的函数内部,我们执行到call这一行,f11进入函数内部:

在这里插入图片描述

⑥调用函数要进行跳转,这也是一行汇编代码!然后再f11就是真正的函数内部了:
在这里插入图片描述

⑦在这里你会发现还没执行到真正的计算,在计算前就存在着某些操作,这些操作实际上是:参数传递、栈空间的创建,然后才是真正的计算!计算的汇编代码才是和宏的汇编代码一样,但是这还没完,函数还有返回,这也是要执行的!

那从函数调用的汇编代码开始算,4行 + 函数跳转1行,+ 函数执行的21行,总共26行,这里实际计算工作其实只有9行。。。。。。

而宏只有十行汇编代码,可以算是只有计算,不存在其它的工作,所以比较高效。

小结一下宏和函数的执行过程:

宏的执行函数的执行
计算函数调用与跳转
-申请创建栈内存空间
-参数传递
-计算
-函数返回

6.1.2 宏不关心参数类型

函数的参数必须声明为特定的类型,而宏是类型无关的。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于比较操作符来比较的类型。

6.1.2 宏的参数可以出现数据类型

宏有时候可以做函数做不到的事情,比如:宏的参数可以出现类型,但是函数做不到。

例如stddef.h头文件中的宏:offsetof(求结构体成员相对于起始点的字节数)
在这里插入图片描述
offsetof宏的使用:

#include <stdio.h>
#include <stddef.h>struct Test {char c;int a;
};int main() {printf("%zd\n", offsetof(struct Test, c));printf("%zd\n", offsetof(struct Test, a));return 0;
}

在这里插入图片描述

利用宏简化malloc的使用:

#include <stdio.h>
#include <stdlib.h>#define MALLOC(num, type) (type*)malloc((num) * sizeof(type))int main() {int* pArr1 = (int*)malloc(10 * sizeof(int));// 对比一下int* pArr2 = MALLOC(10, int);return 0;
}

6.2 宏的缺点

  1. 每次使用宏的时候,一份宏定义的代码将插入到程序中,除非宏比较短,否则可能大幅度增加程序的长度。
  2. 宏是没法调试的,宏在预处理阶段就完成了,不在运行阶段,所以从肉眼上根本无法查看替换后的内容。
  3. 宏由于类型无关,也就不够严谨,这个既是优点也是缺点。
  4. 宏可能会带来运算符优先级的问题,导致程容易出现错。

7. 总结宏和函数的对比

角度函数
代码长度每次使用时,宏代码都会被插入到程序中,除了非常小的宏之外,程序的长度会大幅度增长。函数代码只出现于一个地方,每次使用这个函数时,都调用那个地方的同一份代码。√
执行速度更快。√存在函数的调用和返回的额外开销,所以相对慢一些
操作符优先级宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。函数参数只在函数调用的时候求值一次,它的结果值传递给函数,表达式的求值结果更容易预测。√
参数参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。函数参数只在传参的时候求值一次,结果更容易控制。√
参数类型宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型,更加灵活,但不够严谨。√函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是不同的。
调试宏是不方便调试的。函数是可以逐语句调试的。√
递归宏是不能递归的。函数是可以递归的。√

这么看下来,函数的优势更多,那么是否是直接无脑使用函数就行了呢?还是开头的那句话,如果执行的任务简单,一行代码就能解决的事,还是用宏好一些。

8. 宏的命名约定

一般来讲函数的宏的使用语法很相似,所以语言本身没法帮我们区分二者。

  1. 把宏名全部大写
  2. 函数名不要全部大写

函数名怎么命名,推荐看《高质量的C/C++编程》这本书!简单来说就是首字母大写,如AddXxx(windows编程风格);或全小写+下划线_,如add_xxx(Linux编程风格)。

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

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

相关文章

【Linux】进程间通信——共享内存

目录 一、什么是共享内存 二、共享内存的原理 三、使用共享内存实现进程间通信 3.1 shmget接口 3.1.1 key形参详解 3.2 释放共享内存 3.2.1 ipcs指令 3.2.2 ipcrm指令 3.2.3 shmctl接口 3.3 关联共享内存 3.4 去关联共享内存 3.5 使用共享内存进行进程间通信实例 …

Java基础-IO流

目录 1 File 类的使用 1.1 File类的概念 1.2 构造方法 1.3 常用方法 1.4 课后练习 2 IO流原理及流的分类 2.1 IO原理 2.2 流的分类 2.3 IO流体系 2.4 接口方法 2.4.1 InputStream & Reader相同点 2.4.2 InputStream方法详解 2.4.3 Reader方法详解 2.4.4 Outp…

ant javac任务的fork和executable属性

ant javac任务是用于编译源文件的。 它的fork属性表示是否用JDK编译器在外部执行javac&#xff0c;取值可以为"yes"、“no”&#xff0c;默认值为"no"。 当fork属性的取值为"yes"时&#xff0c;可以用executable属性指明javac可执行文件的完全…

sql高级教程-索引

文章目录 架构简介1.连接层2.服务层3.引擎层4.存储层 索引优化背景目的劣势分类基本语法索引结构和适用场景 性能分析MySq| Query Optimizerexplain 索引优化单表优化两表优化三表优化 索引失效原因 架构简介 1.连接层 最上层是一些客户端和连接服务&#xff0c;包含本地sock通…

汽车屏类产品(五):中控IVI车载信息娱乐系统

前言: 车载信息娱乐系统(IVI)的起源可以追溯到20世纪,按钮调幅收音机被认为是第一个功能。从那以后,IVI系统在创造壮观的车内体验方面变得不可或缺,以至于汽车被称为“车轮上的智能手机”。但随着包括自动驾驶汽车在内的汽车技术的进步,以及对个性化体验的需求不断增长…

Leetcode1839. 所有元音按顺序排布的最长子字符串

Every day a Leetcode 题目来源&#xff1a;1839. 所有元音按顺序排布的最长子字符串 解法1&#xff1a;滑动窗口 要找的是最长美丽子字符串的长度&#xff0c;我们可以用滑动窗口解决。 设窗口内的子字符串为 window&#xff0c;每当 word[right] > window.back() 时&…

最短路相关笔记

Floyd Floyd 算法&#xff0c;是一种在图中求任意两点间最短路径的算法。 Floyd 算法适用于求解无负边权回路的图。 时间复杂度为 O ( n 3 ) O(n^3) O(n3)&#xff0c;空间复杂度 O ( n 2 ) O(n^2) O(n2)。 对于两点 ( i , j ) (i,j) (i,j) 之间的最短路径&#xff0c;有…

算法刷题-链表

算法刷题-链表 203. 移除链表元素 给你一个链表的头节点 head 和一个整数 val &#xff0c;请你删除链表中所有满足 Node.val val 的节点&#xff0c;并返回 新的头节点 。 示例 1&#xff1a; 输入&#xff1a;head [1,2,6,3,4,5,6], val 6 输出&#xff1a;[1,2,3,4,5]…

asp.net社区医疗辅助诊断网站系统VS开发sqlserver数据库web结构c#编程

一、源码特点 asp.net社区医疗辅助诊断网站系统 是一套完善的web设计管理系统&#xff0c;系统采用mvc模式&#xff08;BLLDALENTITY&#xff09;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为vs2010&#xff0c;数据库为sqlserver200…

基于白鲸优化的BP神经网络(分类应用) - 附代码

基于白鲸优化的BP神经网络&#xff08;分类应用&#xff09; - 附代码 文章目录 基于白鲸优化的BP神经网络&#xff08;分类应用&#xff09; - 附代码1.鸢尾花iris数据介绍2.数据集整理3.白鲸优化BP神经网络3.1 BP神经网络参数设置3.2 白鲸算法应用 4.测试结果&#xff1a;5.M…

C语言指针

指针 文章目录 指针1.指针概念2.指针变量2.1 定义指针变量2.2 引用指针变量2.3 指针变量作为函数参数 3.通过指针引用数组3.1数组元素的指针3.2 在引用数组元素时指针的运算3.3通过指针引用数组元素3.4用数组名作函数参数3.5 通过指针引用多维数组 4.通过指针引用字符串4.1字符…

超详细 | 差分进化算法原理及其实现(Matlab/Python)

差分进化(Differential Evolution&#xff0c;DE)算法是由美国学者Storn和 Price在1995年为求解Chebyshev多项式拟合问题而提出的。算法主要通过基于差分形式的变异操作和基于概率选择的交叉操作进行优化搜索&#xff0c;虽然其操作名称和遗传算法相同&#xff0c;但实现方法有…

最新Tuxera NTFS2024破解版mac读写NTFS磁盘工具

Tuxera NTFS for Mac是一款Mac系统NTFS磁盘读写软件。在系统默认状态下&#xff0c;MacOSX只能实现对NTFS的读取功能&#xff0c;Tuxera NTFS可以帮助MacOS 系统的电脑顺利实现对NTFS分区的读/写功能。Tuxera NTFS 2024完美兼容最新版本的MacOS 11 Big Sur&#xff0c;在M1芯片…

Prometheus接入AlterManager配置邮件告警(基于K8S环境部署)

文章目录 一、配置AlterManager告警发送至邮箱二、Prometheus接入AlterManager配置三、部署PrometheusAlterManager(放到一个Pod中)四、测试告警 注意&#xff1a;请基于 PrometheusGrafana监控K8S集群(基于K8S环境部署)文章之上做本次实验。 一、配置AlterManager告警发送至邮…

EF执行迁移时提示provider: SSL Provider, error: 0 - 证书链是由不受信任的颁发机构颁发的

ef在执行时提示provider: SSL Provider, error: 0 - 证书链是由不受信任的颁发机构颁发的。 只需要在数据库链接字符串后增加EncryptTrue;TrustServerCertificateTrue;即可 再次执行

好用的办公软件有哪些

日常的工作难免和各种各样的软件打交道&#xff0c;除了传统的Office三件套&#xff0c;小编日常还在用着其他的办公软件&#xff0c;借此跟各位分享其中比较好用、堪称办公神器的8款软件&#xff01; 1.WPS office 2.office2007 3.EasyConnect 4.ToDesk 5.Photoshop 6.A…

​CUDA学习笔记(五)GPU架构

本篇博文转载于https://www.cnblogs.com/1024incn/tag/CUDA/&#xff0c;仅用于学习。 GPU架构 SM&#xff08;Streaming Multiprocessors&#xff09;是GPU架构中非常重要的部分&#xff0c;GPU硬件的并行性就是由SM决定的。 以Fermi架构为例&#xff0c;其包含以下主要组成…

Git 安装和基础命令、IDEA 基础操作

目录 总结命令&#xff1a;1、安装&#xff1a;1、安装2、配置环境变量&#xff1a; 2、Git操作&#xff1a;1、初始化&#xff1a;1、姓名邮箱&#xff1a;2、初始化仓库&#xff1a;3、工作区和暂存区分析 2、提交文件3、查看版本库状态4、安装小乌龟git不显示图标 5、查看提…

H3C SecParh堡垒机 get_detail_view.php 任意用户登录漏洞

与齐治堡垒机出现的漏洞不能说毫不相关&#xff0c;只能说一模一样 POC验证的url为&#xff1a; /audit/gui_detail_view.php?token1&id%5C&uid%2Cchr(97))%20or%201:%20print%20chr(121)%2bchr(101)%2bchr(115)%0d%0a%23&loginadmin成功获取admin权限 文笔生疏…

智慧公厕系列产品:为您提供更便捷、更卫生的厕所体验

智慧公厕系列产品致力于改善公共厕所的管理和使用体验&#xff0c;通过引入先进的科技和智能设备&#xff0c;提升厕所的安全、卫生、舒适性。这些产品涵盖了从厕位监测到环境调控&#xff0c;从安全防范到能耗监测的各个方面&#xff0c;为用户提供了一个更加方便、舒适、卫生…