【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…...
渲染学进阶内容——模型
最近在写模组的时候发现渲染器里面离不开模型的定义,在渲染的第二篇文章中简单的讲解了一下关于模型部分的内容,其实不管是方块还是方块实体,都离不开模型的内容 🧱 一、CubeListBuilder 功能解析 CubeListBuilder 是 Minecraft Java 版模型系统的核心构建器,用于动态创…...
工程地质软件市场:发展现状、趋势与策略建议
一、引言 在工程建设领域,准确把握地质条件是确保项目顺利推进和安全运营的关键。工程地质软件作为处理、分析、模拟和展示工程地质数据的重要工具,正发挥着日益重要的作用。它凭借强大的数据处理能力、三维建模功能、空间分析工具和可视化展示手段&…...
剑指offer20_链表中环的入口节点
链表中环的入口节点 给定一个链表,若其中包含环,则输出环的入口节点。 若其中不包含环,则输出null。 数据范围 节点 val 值取值范围 [ 1 , 1000 ] [1,1000] [1,1000]。 节点 val 值各不相同。 链表长度 [ 0 , 500 ] [0,500] [0,500]。 …...
让AI看见世界:MCP协议与服务器的工作原理
让AI看见世界:MCP协议与服务器的工作原理 MCP(Model Context Protocol)是一种创新的通信协议,旨在让大型语言模型能够安全、高效地与外部资源进行交互。在AI技术快速发展的今天,MCP正成为连接AI与现实世界的重要桥梁。…...
Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...
使用Matplotlib创建炫酷的3D散点图:数据可视化的新维度
文章目录 基础实现代码代码解析进阶技巧1. 自定义点的大小和颜色2. 添加图例和样式美化3. 真实数据应用示例实用技巧与注意事项完整示例(带样式)应用场景在数据科学和可视化领域,三维图形能为我们提供更丰富的数据洞察。本文将手把手教你如何使用Python的Matplotlib库创建引…...
2025季度云服务器排行榜
在全球云服务器市场,各厂商的排名和地位并非一成不变,而是由其独特的优势、战略布局和市场适应性共同决定的。以下是根据2025年市场趋势,对主要云服务器厂商在排行榜中占据重要位置的原因和优势进行深度分析: 一、全球“三巨头”…...
视频行为标注工具BehaviLabel(源码+使用介绍+Windows.Exe版本)
前言: 最近在做行为检测相关的模型,用的是时空图卷积网络(STGCN),但原有kinetic-400数据集数据质量较低,需要进行细粒度的标注,同时粗略搜了下已有开源工具基本都集中于图像分割这块,…...
iOS性能调优实战:借助克魔(KeyMob)与常用工具深度洞察App瓶颈
在日常iOS开发过程中,性能问题往往是最令人头疼的一类Bug。尤其是在App上线前的压测阶段或是处理用户反馈的高发期,开发者往往需要面对卡顿、崩溃、能耗异常、日志混乱等一系列问题。这些问题表面上看似偶发,但背后往往隐藏着系统资源调度不当…...
【从零学习JVM|第三篇】类的生命周期(高频面试题)
前言: 在Java编程中,类的生命周期是指类从被加载到内存中开始,到被卸载出内存为止的整个过程。了解类的生命周期对于理解Java程序的运行机制以及性能优化非常重要。本文会深入探寻类的生命周期,让读者对此有深刻印象。 目录 …...
