【C语言预处理详解(上)】--预定义符号,#define定义常量,#define定义宏,带有副作用的宏参数,宏替换的规则,宏和函数的对比

article/2025/8/10 8:50:08

目录

一.预定义符号

 二.#define定义常量

三.#define定义宏

3.1--定义宏的方法和注意事项

3.2--带有副作用的宏参数

3.3--宏替换的规则

四.宏与函数的对比 


🔥个人主页:@草莓熊Lotso的个人主页

🎬作者简介:C++研发方向学习者

📖个人专栏:《C语言》

⭐️人生格言:生活是默默的坚持,毅力是永久的享受。


一.预定义符号

--C语言中设置了一些预定义符号,可以直接使用,预定义符号也是在预处理期间处理的。

1.  __FILE__ // 进行编译的源文件
2.  __LINE__ //文 件当前的行号
3.  __DATE__ //文 件被编译的日期
4.  __TIME__ //文 件被编译的时间
5.  __STDC__ // 如果编译器遵循 ANSI C ,其值为 1 ,否则未定义

--我们来使用一下这些预定义符号,观察它们打印出来的结果

#include<stdio.h>int main()
{printf("%s\n", __FILE__);printf("%d\n", __LINE__);printf("%s\n", __DATE__);printf("%s\n", __TIME__);return 0;
}


 二.#define定义常量

基本语法形式:

1.  #define name stuff

实际举例: 

1.  # define MAX 1000
2.  # define reg register // register 这个关键字,创建一个简短的名字
3.   # define do_forever for(;;) //用 更形象的符号来替换⼀种实现,这里是替换死循环
4.  # define CASE break;case // 在写 case 语句的时候自动把 break 写上。
  // 如果定义的 stuff 过长,可以分成几行写,除了最后⼀行外,每行的后面都加⼀个反斜杠 ( 续行 )
5.  # define DEBUG_PRINT printf( "file:%s\t  line:%d\t  date:%s\t  time:%s\n"   \
                                            ,__FILE__,
                                            __LINE__ , \
                                            __DATE__,\ 
                                             __TIME__ )

 我们看一下第4个和第5个的使用吧~

演示4:

#include<stdio.h>
#define CASE break;caseint main()
{int num = 0;scanf("%d", &num);switch (num){case 1:printf("hehe");//正常来说这里应该加break// 但是我定义了CASE为 break;caseCASE 2:printf("haha");//这里同理CASE 3:printf("呵呵");}return 0;
}

演示5: 

#include<stdio.h>
#define DEBUG_PRINT printf("file:%s\t line:%d\t date:%s\t time:%s\n" \,__FILE__, \__LINE__ , \__DATE__,   \__TIME__ )
int main()
{DEBUG_PRINT;return 0;
}

 

我们再来思考一个问题:在define定义标识符的时候,要不要在最后加上 ; 呢? 

比如:

  # define MAX 1000;
  # define MAX 1000

这当然是不行的,很容易导致一些问题,我们直接通过两个错误场景来直观的感受一下吧~

错误场景一:

#include<stdio.h>
#define MAX 1000;int main()
{printf("%d", MAX);//实际上会被解析为1000;;会出现两个分号return 0;
}

错误场景二:

#include<stdio.h>
#define MAX 1000;int main()
{int max = 0;int a = 3;int b = 4;if (a>b)max = MAX;elsemax = 0;return 0;
}

 

如果这里加了分号,等到替换之后,if和else之间实际上是两条语句,在没有大括号时,if后边只能控制一条语句。所以这里会出现语法错误。 


三.#define定义宏

3.1--定义宏的方法和注意事项

--#define机制包括一个规定,允许将参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。

下面是宏的申明方式:

1.  #define name( parament-list ) stuff

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

注意:

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

错误举例:

# define SQUARE( x ) x * x
这个宏接收⼀个参数 x .如果在上述声明之后,你把 SQUARE( 10 ); 置于程序中,预处理器就会⽤
下⾯这个表达式替换上⾯的表达式: 10  * 10。
但是这个代码存在一个问题,比如:
#include<stdio.h>
#define SQUARE(x) x*xint main()
{int a = 10;int r = SQUARE(a+1);printf("%d", r);return 0;
}

这个代码我们简单的来看很可能会认为结果是11*11=121,但其实并不是,它的实际结果是21。

 因为替换文本时,实际上是参数x被替换成a+1,所以这条语句应该被解析为:

int r  = a+1*a+1 ;//即为10+1*10+1=21;
printf ( "%d\n" ,r );

这样我们就可以清晰的看出替换后表达式所求的值了,那么我们该如何解决呢?

其实只需要在宏定义加上两个括号,这个问题就得以解决了

 #define SQUARE( x ) (x) * (x) 

这样改完后预处理完就可以得到你想要的结果了,变成了(10+1)*(10+1)。

但是呢,这样还不能完全解决所有问题,我们再来看看下面这个宏定义:

# define DOUBLE(x) (x) + (x)

这里我们可以看到定义中我们使用了括号,想避免之前出现的问题,但是这个宏可能会出现新的错误,我们来看如下代码:

#include<stdio.h>
#define DOUBLE(x) (x)+(x)int main()
{int a = 10;printf("%d", 5*DOUBLE(a));return 0;
}

这串代码打印出什么值呢?看上去好像很可能是100,但是实际上是60。

我们替换完可以发现:

printf ( "%d\n" ,5   * ( 10 ) + ( 10 ));

乘法的优先级先于宏定义后的加法,所有会出现60 ,这个问题我们只需要在宏定义表达式两边加上一对括号就可以了。

# define DOUBLE(x) ( ( x ) + ( x ) )

重要知识点:

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

3.2--带有副作用的宏参数

当宏参数在宏的定义中出现超过⼀次的时候,如果参数带有副作⽤,那么你在使⽤这个宏的时候就可能出现危险,导致不可预测的后果。副作⽤就是表达式求值的时候出现的永久性效果。
例如:
1.  x+1;//不带副作用
2.  x++;//带有副作用

我们可以通过一个MAX宏来直观体会一下这个错误:

#include<stdio.h>
#define MAX(a,b) ((a)>(b)?(a):(b))int main()
{int x = 5;int y = 8;int z = MAX(x++, y++);printf("x=%d y=%d z=%d", x, y, z);return 0;
}

 我们来分析一下这串代码的结果会是什么吧

1. 先把z换成预处理之后的结果,即为z=( (x++) > (y++) ? (x++) : (y++));

2. 先看第一个表达式,后置加加,先比先x>y为假,所以表达式的结果会表达式3的结果,但是这里的x++后变成6,y++后变成9

3. 计算第三个表达式,此时y再++变成了10,但是因为是后置++所有在++之前把y之前为9的值赋给了z

所以x=6,y=10,z=9;

3.3--宏替换的规则

在程序中扩展#define定义符号和宏时,需要涉及⼏个步骤:
  1. 在调⽤宏时,⾸先对参数进⾏检查,看看是否包含任何由#define定义的符号。如果是,它们⾸先被替换。
  2. 替换⽂本随后被插⼊到程序中原来⽂本的位置。对于宏,参数名被他们的值所替换。
  3. 最后,再次对结果⽂件进⾏扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上 述处理过程。

注意:

  1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

四.宏与函数的对比 

--宏通常被应用于执行简单的运算。

比如在两个数中我们想要找较大的一个数时,写成下面的宏的形式,更有优势一些

# define MAX(a, b) ((a)>(b)?(a):(b))
这里为什么用宏比函数更优呢?
原因如下:
  1. ⽤于调⽤函数和从函数返回的代码可能⽐实际执⾏这个⼩型计算⼯作所需要的时间更多。所以宏⽐ 函数在程序的规模和速度⽅⾯更胜⼀筹。
  2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使⽤。反之这个宏怎可以适⽤于整形、⻓整型、浮点型等可以⽤于 > 来⽐较的类型。宏的参数是类型⽆关的。

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

# define MALLOC(num, type)\
(type )malloc(num sizeof(type))
...
// 使⽤
MALLOC( 10 , int ); // 类型作为参数
// 预处理器替换之后是:
( int * ) malloc ( 10 sizeof ( int ));

 但和函数相比宏也有它的劣势,如下:

  1. 每次使⽤宏的时候,⼀份宏定义的代码将插⼊到程序中。除⾮宏⽐较短,否则可能⼤幅度增加程序的⻓度。
  2. 宏是没法调试的。
  3. 宏由于类型⽆关,也就不够严谨。
  4. 宏可能会带来运算符优先级的问题,导致程容易出现错,(比如上面的自增++)

 宏和函数的对比关系表:

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

往期回顾:

【C语言编译与链接】--翻译环境和运行环境,预处理,编译,汇编,链接

【通关文件操作(上)】--文件的意义和概念,二进制文件和文本文件,文件的打开和关闭,文件的顺序读写

【通关文件操作(下)】--文件的顺序读写(续),sprintf和sscanf函数,文件的随机读写,文件缓冲区,更新文件

结语:本篇文章就到此结束了,继前面一篇文章后,在此篇文章中给大家分享了预处理详解中的部分知识点,如预定义符号,#define定义常量,#define定义宏,带有副作用的宏参数,宏替换的规则,宏和函数的对比等,下篇文章会接着为大家分享预处理详解的剩余知识点。如果文章对你有帮助的话,欢迎评论,点赞,收藏加关注,感谢大家的支持。


http://www.hkcw.cn/article/yhsxtZTCox.shtml

相关文章

CppCon 2014 学习:C++ Memory Model Meets High-Update-Rate Data Structures

这段内容是对一个主题的概览&#xff08;Overview&#xff09;&#xff0c;涉及并行更新的问题&#xff0c;特别是“Issaquah Challenge”这个具体案例。详细解读如下&#xff1a; Overview&#xff08;概览&#xff09; The Issaquah Challenge 这是一个特定的挑战或问题&am…

如何用利用deepseek的API能力来搭建属于自己的智能体-优雅草卓伊凡

如何用利用deepseek的API能力来搭建属于自己的智能体-优雅草卓伊凡 上一篇文章我们已经介绍了智能体和大模型AI的区别&#xff0c;现在我们开始搭建自己的智能体进行工作 1. 了解 DeepSeek 提供的 AI 能力 DeepSeek 提供强大的 大语言模型&#xff08;LLM&#xff09;&#x…

智能制造之精读——RPA制造行业常见场景【附全文阅读】

RPA 在制造行业应用广泛&#xff0c;为企业带来显著价值&#xff0c;是极具潜力的智能化解决方案。它能节省成本&#xff0c;降低人力与管理成本&#xff1b;提升运营效率&#xff0c;减少人机交互损耗&#xff1b;提高质量&#xff0c;保障流程准确性&#xff1b;还能增强合规…

【2025.06】jupyter notebook 7+ 新手安装、配置、扩展应用(windows篇)

本文目录 前述一、安装二、配置2.1 jupyter_notebook_config.py生成配置文件2.2 服务器与网络设置a. 修改端口号b. 允许远程访问c. 设置工作目录 2.3 安全与认证a. 禁用密码登录&#xff08;仅限本地安全环境&#xff09;b. 设置登录密码c. 启用SSL加密&#xff08;HTTPS&#…

ASP.NET Core SignalR 身份认证集成指南(Identity + JWT)

文章目录 前言一、完整解决方案架构二、实现步骤1.配置 Identity 和 JWT 认证2. SignalR JWT配置3.SignalR Hub 集成认证和授权4.控制器5.客户端集成 (JavaScript)6.配置 appsettings.json 三、认证流程详解1.用户登录&#xff1a;2.SignalR 连接&#xff1a;3.JWT 验证&#x…

Redis最佳实践——性能优化技巧之数据结构选择

Redis在电商应用中的数据结构选择与性能优化技巧 一、电商核心场景与数据结构选型矩阵 应用场景推荐数据结构内存占用读写复杂度典型操作商品详情缓存Hash低O(1)HGETALL, HMSET购物车管理Hash中O(1)HINCRBY, HDEL用户会话管理Hash低O(1)HSETEX, HGET商品分类目录Sorted Set高O…

【Tauri2】049——upload

前言 这篇就看看一个简单地插件——upload Upload | Taurihttps://tauri.app/plugin/upload/upload的英文意思是“上传&#xff08;程序或信息&#xff09;”。 看来是用来上传文件的。 支持移动端 正文 安装 pnpm tauri add upload 在前后端都会安装&#xff0c;即 .plug…

《深度解构现代云原生微服务架构的七大支柱》

☁️《深度解构现代云原生微服务架构的七大支柱》 一线架构师实战总结&#xff0c;系统性拆解现代微服务架构中最核心的 7 大支柱模块&#xff0c;涵盖通信协议、容器编排、服务网格、弹性伸缩、安全治理、可观测性、CI/CD 等。文内附架构图、实操路径与真实案例&#xff0c;适…

ADAS概述

一、ADAS的概念 1.1 ADAS功能概述、架构方案、控制器、传感器 核心概念&#xff1a;ADAS(Advanced Driving Assistance System)是高级驾驶辅助系统的总称&#xff0c;包含三大类功能&#xff1a; 舒适体验类&#xff1a;如自适应巡航(ACC)、高速公路辅助(HWA)、车道居中控制&…

深入探讨redis:万字讲解集群

什么是集群 广义的集群&#xff1a;多个机器&#xff0c;构成了分布式系统&#xff0c;就可以称为“集群”。 狭义的集群&#xff1a;redis提供的集群模式&#xff0c;这个集群模式之下&#xff0c;主要解决的是存储空间不足的问题(拓展存储空间) 随着数据量的增多一台机器的…

一键开关机电路分析

左边电源9V为输入电源&#xff0c;中间有一个LDO&#xff0c;输出5V给右侧的芯片供电。 Q1是PNP三极管&#xff0c;Q2和Q3是NPN三极管。 初始状态下&#xff0c;按键断开&#xff0c;Q3截止&#xff0c;故Q1的基极为高电平&#xff0c;电压为9V&#xff0c;be间没有电流流过&am…

输入ifconfig,发现ens33不见了,无法连接至虚拟机

输入ifconfig&#xff0c;发现ens33不见了&#xff0c;无法连接至虚拟机 输入ifconfig&#xff0c;发现ens33不见了&#xff0c;无法连接至虚拟机 输入ifconfig&#xff0c;发现ens33不见了&#xff0c;无法连接至虚拟机 当输入ifconfig&#xff0c;发现少了ens33&#xff0c;无…

c++学习值---模版

目录 一、函数模板&#xff1a; 1、基本定义格式&#xff1a; 2、模版函数的优先匹配原则&#xff1a; 二、类模板&#xff1a; 1、基本定义格式&#xff1a; 2、类模版的优先匹配原则&#xff08;有坑哦&#xff09;&#xff1a; 3、缺省值的设置&#xff1a; 4、ty…

day62—DFS—太平洋大西洋水流问题(LeetCode-417)

题目描述 有一个 m n 的矩形岛屿&#xff0c;与 太平洋 和 大西洋 相邻。 “太平洋” 处于大陆的左边界和上边界&#xff0c;而 “大西洋” 处于大陆的右边界和下边界。 这个岛被分割成一个由若干方形单元格组成的网格。给定一个 m x n 的整数矩阵 heights &#xff0c; hei…

LeetCode第240题_搜索二维矩阵II

LeetCode 第240题&#xff1a;搜索二维矩阵 II 题目描述 编写一个高效的算法来搜索 m n 矩阵 matrix 中的一个目标值 target。该矩阵具有以下特性&#xff1a; 每行的元素从左到右升序排列。每列的元素从上到下升序排列。 难度 中等 题目链接 点击在LeetCode中查看题目…

开始通信之旅-----话题通信

1. 话题通信的流程 话题通信主要涉及到三个对象 管理者发布者订阅者 其主要流程如下图 详细解释一下&#xff1a;1.发布者向管理者发送发布话题等相关信息&#xff0c;在管理者处注册 2.订阅者向管理者发布订阅话题等相关信息&#xff0c;在管理者处注册 &#xff08;注意…

Ansible自动化运维工具全面指南:从安装到实战应用

目录 1 Ansible核心介绍 1.1 什么是Ansible&#xff1f; 1.2 Ansible核心特点解析 1.2.1 基于Python生态 1.2.2 无代理架构优势 1.2.3 幂等性实现原理 2 Ansible离线安装指南 2.1 内网环境安装准备 2.2 分步安装过程 2.2.1 安装依赖包 2.2.2 安装Ansible主包 2.2.3…

设计模式——模版方法设计模式(行为型)

摘要 模版方法设计模式是一种行为型设计模式&#xff0c;定义了算法的步骤顺序和整体结构&#xff0c;将某些步骤的具体实现延迟到子类中。它通过抽象类定义模板方法&#xff0c;子类实现抽象步骤&#xff0c;实现代码复用和算法流程控制。该模式适用于有固定流程但部分步骤可…

ACL基础配置

文章目录 基本ACL配置组网需求组网拓扑实验步骤测试结果配置文件 高级ACL配置组网需求组网拓扑实验步骤测试结果配置文件 基本ACL配置 组网需求 现组网结构如下&#xff0c;VPC充当服务器&#xff0c;PC3与PC4是两个不同的网段&#xff0c;实现拒绝192.168.1.0/24访问VPC 组…

Redis最佳实践——热点数据缓存详解

Redis在电商热点数据缓存中的最佳实践 一、热点数据定义与识别 1. 热点数据特征 高频访问&#xff08;QPS > 1000&#xff09;数据规模适中&#xff08;单条 < 10KB&#xff09;数据变化频率低&#xff08;更新间隔 > 5分钟&#xff09;业务关键性高&#xff08;直接…