【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定义符号和宏时,需要涉及几个步骤。
- 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
- 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
- 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
其它:
- 宏参数和#define 定义中可以出现其他#define定义的符号。
#define N 10
#define CAL(x) ((x) + N)
- 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
#define N 10
#define STR "NOT"
printf("No");
"NOT"中的N并不会被替换,"No"中的N也不会被替换。
- 对于宏,不能出现递归。
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 宏的缺点
- 每次使用宏的时候,一份宏定义的代码将插入到程序中,除非宏比较短,否则可能大幅度增加程序的长度。
- 宏是没法调试的,宏在预处理阶段就完成了,不在运行阶段,所以从肉眼上根本无法查看替换后的内容。
- 宏由于类型无关,也就不够严谨,这个既是优点也是缺点。
- 宏可能会带来运算符优先级的问题,导致程容易出现错。
7. 总结宏和函数的对比
| 角度 | 宏 | 函数 |
|---|---|---|
| 代码长度 | 每次使用时,宏代码都会被插入到程序中,除了非常小的宏之外,程序的长度会大幅度增长。 | 函数代码只出现于一个地方,每次使用这个函数时,都调用那个地方的同一份代码。√ |
| 执行速度 | 更快。√ | 存在函数的调用和返回的额外开销,所以相对慢一些 |
| 操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。 | 函数参数只在函数调用的时候求值一次,它的结果值传递给函数,表达式的求值结果更容易预测。√ |
| 参数 | 参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。 | 函数参数只在传参的时候求值一次,结果更容易控制。√ |
| 参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型,更加灵活,但不够严谨。√ | 函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是不同的。 |
| 调试 | 宏是不方便调试的。 | 函数是可以逐语句调试的。√ |
| 递归 | 宏是不能递归的。 | 函数是可以递归的。√ |
这么看下来,函数的优势更多,那么是否是直接无脑使用函数就行了呢?还是开头的那句话,如果执行的任务简单,一行代码就能解决的事,还是用宏好一些。
8. 宏的命名约定
一般来讲函数的宏的使用语法很相似,所以语言本身没法帮我们区分二者。
- 把宏名全部大写
- 函数名不要全部大写
函数名怎么命名,推荐看《高质量的C/C++编程》这本书!简单来说就是首字母大写,如AddXxx(windows编程风格);或全小写+下划线_,如add_xxx(Linux编程风格)。
相关文章:
【C语言】#define宏与函数的优劣对比
本篇文章目录 1. 预处理指令#define宏2. #define定义标识符或宏,要不要最后加上分号?3.宏的参数替换后产生的运算符优先级问题3.1 问题产生3.2 不太完美的解决办法3.3 完美的解决办法 4.#define的替换规则5. 有副作用的宏参数6. 宏与函数的优劣对比6.1 宏…...
flask基础开发知识学习
之前做了一些LLM的demo,接口用flask写的,但是涉及到后端的一些业务就感觉逻辑写的很乱,代码变成屎山,于是借助官方文档和GPT迅速补了一些知识,总结一下一个很小的模板 于是决定边学边重构之前的代码… 文章目录 代码结…...
内网和热点同时连接使用配置
解决如标题问题 查看当前永久路由信息 route print截图保存(重要) 截图保存(重要)查出来的永久路由,以防配置不成功时回退,回退方法就是下面的“添加永久路由” 删除当前的路由 0.0.0.0 是上面查出的网络地址 route delete 0.0.0.0内网IP信息 添加永久…...
C语言 形参、实参
定义 形参 形式上的参数,没有确定的值 实参 实际存在的,已经确定的参数,常量,变量,表达式,都是实参 区别 实参的值不随形参的变化而变化 在C语言中,数据传送是单向的,即只能把实…...
linux入门到精通-第四章-gcc编译器
目录 参考gcc概述gcc的工作流程 参考 gcc编译器 gcc概述 编辑器vi、记事本)是指我用它来写程序的 (编辑码),而我们写的代码语句,电脑是不懂的,我们需要把它转成电脑能懂的语句,编译器就是这样的转化工具。就是说,我…...
HCIP静态路由综合实验
题目: 步骤: 第一步:搭建上图所示拓扑; 第二步:为路由器接口配置IP地址; R1: [R1]display current-configuration intinterface GigabitEthernet0/0/0ip address 192.168.1.1 255.255.255.252 interfa…...
nginx前端配置(新)
基础配置 server {listen 80;server_name your-frontend-domain.com;# 根目录为前端网页文件所在目录root /path/to/your/frontend/files;# 默认文档(例如 index.html)index index.html;location / {try_files $uri $uri/ /index.html; #try_files 指…...
js,jquery,vue设置html标签隐藏不显示
前端 <p id"myElement"> </p>使用js将idmyElemnt的标签隐藏 使用 style.display 属性: 通过设置 style.display 属性为 "none",可以隐藏标签。 var element document.getElementById("myElement");element…...
口袋参谋:如何实时监控对手数据?
在如此激烈的淘宝天猫上开店,如何才能获取对手的数据呢? 俗话说的好,知己知彼百战百胜,那么这句话同样也适用于淘宝天猫上。 只有掌握对手推广策略以及数据,我们才有机会反超,因此做好竞品监控是运营店…...
Q-learning如何与ABC等一些元启发式算法能够结合在一起?
1、出现的问题 Q-learning能和元启发式算法(如ABC、PSO、GA、SSA等)结合在一起,实现工作流调度问题? Q-learning和ABC (Artificial Bee Colony) 等元启发式算法可以结合在一起以解决特定类型的问题。Q-learning是一种强化学习算法…...
mysql 过滤多列重复的值(保留其中一条),对单列或者多列重复的值去重
建立测试数据表 CREATE TABLE test (id int(11) NOT NULL AUTO_INCREMENT,account varchar(255) DEFAULT NULL,password varchar(255) DEFAULT NULL,deviceId varchar(255) DEFAULT NULL,PRIMARY KEY (id) ) ENGINEInnoDB AUTO_INCREMENT11 DEFAULT CHARSETutf8mb4;INSERT INT…...
面向红队的自动化引擎工具
gogo 介绍 面向红队的、高度可控的可拓展的自动化引擎。特征如下: 自由的端口配置 支持主动/主动指纹识别 关键信息提取,如标题、证书以及自定义提取信息的正则 支持nuclei poc,poc目录:https://chainreactors.github.io/wiki/…...
Python库学习(十):Matplotlib绘画库
1. 介绍 Matplotlib 是一个用于绘制图表和可视化数据的 Python 库。它提供了丰富的绘图工具,可以用于生成各种静态、交互式和动画图表。Matplotlib 是数据科学、机器学习和科学计算领域中最流行的绘图库之一。 1.1 关键特性 以下是 Matplotlib 的一些关键特性&…...
coverity工具 代码审计
第39篇:Coverity代码审计/代码扫描工具的使用教程_希潭实验室ABC123的博客-CSDN博客...
女鹅冬天的第一件羽绒服,当然要时尚经典的
90白鸭绒,高密度充绒量和蓬松度 让这件羽绒服更加饱满更有型 三防工艺,立领连帽设计 下摆抽绳,帽子上的魔术贴设计 无一不将保暖落实在实处 宽松版型立体感很强,对身材的包容性也是非常不错...
智慧渔业方案:AI渔政视频智能监管平台助力水域禁渔执法
一、方案背景 国内有很多水库及河流设立了禁渔期,加强渔政执法监管对保障国家渔业权益、维护渔业生产秩序、保护渔民群众生命财产安全、推进水域生态文明建设具有重要意义。目前,部分地区的监管手段信息化水平低下,存在人员少、职责多、任务…...
LXC、Docker、 Kubernetes 容器以及Hypervisor的区别
LXC、Docker、 Kubernetes 容器以及Hypervisor的区别 SaaS: Software-as-a-Service(软件即服务) PaaS: Platform-as-a-Service(平台即服务) IaaS: Infrastructure-as-a-Service(基础设施即服务) 1、Docke…...
电子杂志制作不求人:简单易用的工具推荐
如果你想要制作一份精美的电子杂志,但是又不想花费太多的时间和金钱,也不想求及朋友帮忙制作,那么可以试试这个网站制作电子杂志,展现出的效果跟专业级设计师的效果没什么区别哦 赶快收藏吧-------FLBOOK在线制作电子杂志平台&a…...
Excel冻结窗格
1、冻结表格首行 点击菜单栏中的“视图”,选择“窗口”选项卡中的“冻结窗格”下的小三角,再选择“冻结首行”; 2.冻结表格首列 点击菜单栏中的“视图”,选择“窗口”选项卡中的“冻结窗格”下的小三角,再选择“冻结…...
Flink自定义sink并支持insert overwrite 功能
前言 自定义flink sink,批模式下,有insert overwrite 需求或需要启动任务或任务完成后时,只执行一次某些操作时,则可参考此文章 组件: flink: 1.15 参考文档:https://nightlies.apache.org/flink/flink-docs-release-1.15/docs/dev/table/sourcessinks/ 分析 inser…...
生成xcframework
打包 XCFramework 的方法 XCFramework 是苹果推出的一种多平台二进制分发格式,可以包含多个架构和平台的代码。打包 XCFramework 通常用于分发库或框架。 使用 Xcode 命令行工具打包 通过 xcodebuild 命令可以打包 XCFramework。确保项目已经配置好需要支持的平台…...
CTF show Web 红包题第六弹
提示 1.不是SQL注入 2.需要找关键源码 思路 进入页面发现是一个登录框,很难让人不联想到SQL注入,但提示都说了不是SQL注入,所以就不往这方面想了 先查看一下网页源码,发现一段JavaScript代码,有一个关键类ctfs…...
线程与协程
1. 线程与协程 1.1. “函数调用级别”的切换、上下文切换 1. 函数调用级别的切换 “函数调用级别的切换”是指:像函数调用/返回一样轻量地完成任务切换。 举例说明: 当你在程序中写一个函数调用: funcA() 然后 funcA 执行完后返回&…...
BCS 2025|百度副总裁陈洋:智能体在安全领域的应用实践
6月5日,2025全球数字经济大会数字安全主论坛暨北京网络安全大会在国家会议中心隆重开幕。百度副总裁陈洋受邀出席,并作《智能体在安全领域的应用实践》主题演讲,分享了在智能体在安全领域的突破性实践。他指出,百度通过将安全能力…...
EtherNet/IP转DeviceNet协议网关详解
一,设备主要功能 疆鸿智能JH-DVN-EIP本产品是自主研发的一款EtherNet/IP从站功能的通讯网关。该产品主要功能是连接DeviceNet总线和EtherNet/IP网络,本网关连接到EtherNet/IP总线中做为从站使用,连接到DeviceNet总线中做为从站使用。 在自动…...
微信小程序云开发平台MySQL的连接方式
注:微信小程序云开发平台指的是腾讯云开发 先给结论:微信小程序云开发平台的MySQL,无法通过获取数据库连接信息的方式进行连接,连接只能通过云开发的SDK连接,具体要参考官方文档: 为什么? 因为…...
自然语言处理——Transformer
自然语言处理——Transformer 自注意力机制多头注意力机制Transformer 虽然循环神经网络可以对具有序列特性的数据非常有效,它能挖掘数据中的时序信息以及语义信息,但是它有一个很大的缺陷——很难并行化。 我们可以考虑用CNN来替代RNN,但是…...
Java面试专项一-准备篇
一、企业简历筛选规则 一般企业的简历筛选流程:首先由HR先筛选一部分简历后,在将简历给到对应的项目负责人后再进行下一步的操作。 HR如何筛选简历 例如:Boss直聘(招聘方平台) 直接按照条件进行筛选 例如:…...
项目部署到Linux上时遇到的错误(Redis,MySQL,无法正确连接,地址占用问题)
Redis无法正确连接 在运行jar包时出现了这样的错误 查询得知问题核心在于Redis连接失败,具体原因是客户端发送了密码认证请求,但Redis服务器未设置密码 1.为Redis设置密码(匹配客户端配置) 步骤: 1).修…...
ABAP设计模式之---“简单设计原则(Simple Design)”
“Simple Design”(简单设计)是软件开发中的一个重要理念,倡导以最简单的方式实现软件功能,以确保代码清晰易懂、易维护,并在项目需求变化时能够快速适应。 其核心目标是避免复杂和过度设计,遵循“让事情保…...
