C语言 - 预处理详解(一)#预定义符号 ##define #undef

news2024/9/17 7:09:31

文章目录

前言

一、预定义符号

二、#define 

(一)、#define 定义的标识符

(二)、#define 定义的宏

(三)、#define 替换规则

(四)、# 和 ##

1、 # 的作用

2、## 的作用

(五)、带副作用的宏参数

(六)、宏和函数的对比

(七)、命名约定

三、#undef

总结


前言

路漫漫其修远兮,吾将上下而求索;


一、预定义符号

在C语言本身便预定义了一些符号,这些符号是可以直接使用的;

__FILE__       //进行编译的源文件(文件名:路径+主干名+后缀) %s

__LINE__       //文件当前的行号  %d

__DATE__      //文件被编译的日期  %s

__TIME__       //文件被编译的时间  %s

__STDC__      //如果编译器遵循 ANSI C,其值为1,否则未定义 %d

注:这些预定义符号在预处理阶段C语言本就定义好了的(这些预定义符号均是语言内置),是可以直接使用;当然,预定义符号出了上述列出来的几个,还有其他的,只不过这几个最常使用;

这些预定义符号有什么用呢?

例如,下面的代码便是在屏幕上打印出了目标数字:

但是,“打印”究竟是在文件的那个地方、其代码在哪一行、什么时候进行打印的呢?我们是否能记录一下呢?此时便可以用到 __FILE__、__LINE__、__DATE__、__TIME__使用如下:

__STDC__: 如果编译器遵循ANCI C,其返回值为1,否则未定义;故,倘若你想要知道VS编译器是否遵循ANSI C标准便可以用__STDC__ 来测试一下;此预定义符号的具体使用如下:

注:Linux 环境下的 gcc 编译器是遵循ANSI C标准的,这也就是为什么有些语法在测试的时候,在VS下的结果会与在gcc 编译器下的结果不同,当出现这一情况的时候,要以 gcc 编译器的结果为准,因为gcc 编译器才是严格符合ANSI C标准的编译器;

看了上述的注解,你可能会问:DevC++呢?

  • DevC++ 对于标准的支持不严谨,故而你会发现倘若在DevC++ 中写的代码是非常随意的,体现在:你写的语法很糟糕但是该编译器识别不出错误;所以在OJ网站上,一般就是要么使用gcc ,要么使用 clang (苹果公司所维护的编译器);

想必你会有疑问,这些预定义符号有什么用呢有什么用呢?

  • 显然,当我们想知道当前代码在哪个文件哪一行什么时间运行的时候,便可以利用这些预定于符号,故而也不会对获取其行号而发愁;

未来在哪里可以用到这些预定于符号?

  • 记录日志(即将这些信息写入文件之中)

二、#define 

(一)、#define 定义的标识符

语法: #define name stuff

使用如下:

#define MAX 1000     //定义了一个标识符常量MAX

#define reg register     //为register 这个关键字创建一个简短的名字 reg

#define do_forever for(;;)     //用更加形象的符号来替代一种实现(甚至可以是一段代码)

#define CASE break;case    //在写case 语句的时候自动把break 写上

#define DEBUG_PRINT printf("file: %s line=%d  \

                                               date:%s time:%s \n",                                                                                                                  __FILE__,__LINE__,  \

                                                 __DATE__,__TIME__)

注:如果定义的stuff 过长,可以分成几段,除了最后一行外可以在每一行的后面都添一个反斜杠(续行符)并且在此续行符后面不可以再添加其他的东西;

续行符的作用?

  • 相当于转义了回车,让回车不再是回车;如果在续行符后面添加了一个空格,那么此续行符转义的便不再是回车,而是其后的空格 --> 没有转义回车而将一条语句分成了多段--> 报错;

#define 定义的标识符究竟是如何操作的?

  • #define 定义的标识符是在预处理阶段被替换掉,同时会删除该符号;

注:在 test.i 文件中不难发现在我们编写的代码前面有很多行代码,显然这是<stdio.h> 中的文件,故而不要频繁多次地包含头文件

在#define 定义的标识符后面可不可以添加 ?

例如这样:

#define DEBUG_PRINT printf("file: %s line=%d  \

                                               date:%s time:%s \n",                                                                                                                  __FILE__,__LINE__,  \

                                                 __DATE__,__TIME__);

#define MAX 100;

这样写是不推荐的,因为容易写出 bug ;

为什么?

纵使 #define 定义的是一条语句,也不要加 , 因为当你在调用此条语句的时候还会在其后面加 ; (利于代码的可读性)

(二)、#define 定义的宏

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

宏的申明方式: #define name(parament-list)  stuff

注:

  • 其中的parament-list 是一个由逗号隔开的符号表,它们可能出现在 stuff 中;
  • 参数左括号必须与name 紧挨;倘若参数左括号与name 之间有任何的空白存在,均会将该参数列表被解释为stuff 的一部分;

使用如下:

#define  SQUARE(X)  X*X

#define 定义的宏与#define 定义的标识符有什么区别:

  • #define 定义的宏是有参数的,而#define 定义的标识符没有参数;

宏的使用如下:

#define 定义的宏仍然是替换;

正式由于宏的本质是替换,那么就极容易出现操作符优先级先后的问题,上述代码存在的问题如下图所示:

特别注意,宏的参数不是计算传入宏体的,而是替换进行的;故而,倘若没有使用 () 来保证宏体中的操作符的优先级,而宏参数不一定只是单单的一个数字,如若也是表达式,那么极易出现操作符优先级带来实际的计算顺序与预期的计算顺序不相符的情况

所以此处就得将宏中的参数当作一个整体,修改如下:

同理,既然宏参数与宏体之间存在操作符的优先级关系,那么宏也不单单是单独使用,即宏体与其外面的数字可能也会存在操作优先级的问题,例如:

所以也要将宏体当作一个整体

核心在于,宏的本质是替换,要考虑到宏参数、宏体以及宏参与计算时与其周围操作符的优先级关系,所以就要利用括号将宏参数与宏体括起来,以保证其计算的顺序;

(三)、#define 替换规则

在程序中扩展 #define 定义的符号和宏时,需要涉及以下三个步骤:

  • 在调用宏时,首先要对其参数进行检查,看看是否包含任何由 #define 定义的符号,如果是,它们首先被替换
  • 所要替换的宏体会被插入到程序中原本使用宏的位置,而对于宏、参数名被宏体所替换;
  • 再次对结果文件进行扫描,看看它是否包含了任何对 #define 定义的符号,如果是便就重复上述操作;

上述步骤图解如下:

注:

1、宏参数和 #define 定义中可以出现其他 #define 定义的符号。但是对于宏,不能出现递归;

2、当预处理器搜索 #define 定义的符号的时候,字符串常量的中内容并不会被搜索

为什么宏不可以递归?

  • 因为宏是完成替换的,它与函数不一样;并不是因为在宏中不能写递归的原因是在于其没有限制条件,而是因为在语法上硬性不支持宏写递归

(四)、# 和 ##

1、 # 的作用

在讲述 # 的作用之前,我们先来了解一下字符串的特性

在C语言中,如若你想打印 "hello world" 可以这样写:,当然你还可以这样写: --> 实际上是在函数printf 中放了两个字符串,但最终会合并为一个字符串,字符串具有自动连接的特点

基于此原理,我们便产生了一个想法:

经过思考,你会发现,此处只能封装成宏,而非函数;

为什么不能封装成函数呢?

  • 因为如若你要封装成一个函数,那么此函数的内部功能要统一才行,而上图中是针对不同的变量而输出不同的对象名称,所以此处不能用函数;

既然如此,变量名如何传入?换句话说,如何将参数插入字符串中?

可能你首先会想到这么写:,但实际上认真思考会发现,N放在字符串中,而宏无法在字符串中被预处理器搜索,故而字符串中的N并不会替换,例子如下:

此时的字符串中的N成了一个普普通通的字符;

联系到,前面将两条字符串放在printf 函数中但最后合并成了一个字符串,你可能会说将"the value of" 与"is %d\n" 分为两条字符串,然后中间放宏参数,具体实现如下:

毫无疑问,这也是不行的,因为只有字符串相邻放在一起才能合成为一条字符串,N 单独放在两字符串中间肯定会出错况且函数printf 也不允许这么操作;

此时便会用到 #

#  --> 将一个宏的参数变成对应的字符串

那么此时 #N 便已然是个字符串,便就可以和字符串相邻放在一起而合并成一条字符串;

使用如下:

还可以利用宏来处理打印不同类型数据的问题;因为不同类型的数据对应着不同的占位符,所以此时的宏会有两个参数:变量以及此数据所对应的占位符;使用如下:

2、## 的作用

## 可以把位于它两边的符号合并成一个符号,并且允许宏定义从分离的文本片段创建标识符

#define ADD_TO_SUM(num , value)  sum##num+=value

ADD_TO_SUM (10,20);  -->  sum10+=20; 即让 sum10 增加20;

注:这样的连接必须产生的是一个合法的标识符否则其结果就是未定义的

注:在预处理之中的 ## 可以将两个符号合并成一个符号;并且允许宏定义从分离的文本片段创建标识符;

(五)、带副作用的宏参数

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

什么叫做副作用?

  • 在现实生活中,以生病为例子,倘若一个人生病了,医生给他开的药治疗他的病的同时可能会给他带来副作用(副作用即是产生不良的反应);在代码之中,便体现为,我"帮助"了别人,结果改变了自己,例如 int a  = 2 ; int b = ++a ;  --> 此处的b 确实能得到值3,但是在这过程中 a 的值变了;于是乎此式便带有副作用;存在两个作用:为b 赋值、 更改a 的值;其中更改a 的值便为副作用;

对于宏而言,如下图:

显然,当宏参数有副作用的时候,所得到的结果脱离了我们设计该宏的目的

上述的问题是什么出来的呢?

我们先来回顾一下三目操作符(条件操作符),计算规则:从左到右依次计算, 其具体实现细节如下图所示:

分析上述代码计算的过程:

从上述代码中,你可以发现,像 a++ , b++ 这种带有副作用的宏参数,并不是单单地只执行一次,当 (a++ > b++ )为真的时候,便会让 a++ 执行两次,而当 (a++ > b++ )为假的时候,便会让 b++ 执行两次;这样的代码时十分危险的,因为其结果难以预测;

(六)、宏和函数的对比

宏能完成的任务同样函数也可以,二者究竟有何区别?

例,就上图中求取最大值而言,宏与函数哪个更好?

于此例中,从参数类型的角度来看,宏没有参数类型的检查,可适用于很多的类型,故而显得非常灵活;而函数对参数类型的要求很严格

从执行速度来看,相较于函数,宏的速度更快。为什么呢?

因为宏的本质是替换,还是以上述例子为例;

上述代码利用宏而比函数好的原因:

  • 1、用于调用函数(传参、函数栈帧的开辟)和从函数返回(函数可能会返回数据)的代码可能比实际执行这个小型计算工作所需要的时间长,所以宏比函数在程序的规模和速度方面更胜一筹;
  • 2、函数的参数必须声明为特定的类型宏的参数与类型无关;函数只能在类型合适的表达式上使用,而宏适用于可用于计算该式的所有类型;

但是宏也不是万能的,他也存在缺点:

  • 每次使用宏的时候,一份宏定义的代码插入到,倘若其代码很长,而又多次使用到该宏,那么便会大幅度地增加代码的长度;
  • 宏是不可以进行调试的;
  • 宏的参数与类型无关,显得不严谨
  • 宏可能会带来运算符优先级的问题,而导致程序容易出错;
  • 不可以递归

注:宏的处理是在预处理阶段进行的,而调试调试的是编译、链接产生的可执行程序;

看了上文,你可能会有疑问,宏能实现的,函数也能实现,那么有没有宏能实而函数不能实现的情况呢?

  • 总所周知,函数的参数不能单单是类型,但是介于宏的本质实现是替换,所以宏参数可以是类型;

在前面学习动态开辟的时候,是否有这样的感觉,例如利用malloc 开辟空间,那么就得计算所要开辟的空间的字节数,针对不同类型的数据、存放此数据的个数来计算开辟的空间的大小,其计算过程便容易出现问题,为了减少bug 的出现,那么此时便可以利用宏来实现,代码如下:

#include<stdio.h>

#define MALLOC(num,type) (type *)malloc((num)*sizeof(type))

int main()
{
	short* p1 = MALLOC(10,short);
	if (p1 == NULL)
	{
		perror("malloc short");
		return;
	}
	int* p2 = MALLOC(20, int);
	if (p2 == NULL)
	{
		perror("malloc int");
		return;
	}
	
	return 0;
}

宏与函数的对比:

属性#define 定义宏函数
代码长度每次使用宏的时候,宏代码都会被插入到程序中;倘若宏的代码行很多,多次使用该宏便会使得该程序的代码行大幅度增长函数具体实现的代码只会出现在一个地方;每次使用这个函数的时候,都会那个地方的函数;不会因多次调用函数而大幅度地增加代码行
执行速度宏的本质是替换,且在预处理阶段完成的,只有执行该代码的时间开销,故而其执行速度会更快存在函数的调用返回结果时间上的开销,故而相对来说会慢一些
操作符的优先级宏的本质是替换;宏参数求值是在所有周围表达式的上下文环境里,故而其邻近操作符的优先级可能会影响宏体中的实现,进而导致实际结果会与预期结果不相符的情况;所以在写宏的时候,要为宏参数宏体加上括号,以保证其计算的顺序;函数参数只在函数调用的时候求值一次,它的结果值传递给函数;表达式求值的结果更容易预测;
带有副作用的参数宏参数可能会被替换到宏体中的多个位置,倘若此宏参数带有副作用,那么便会进行多次计算,从而导致所得到的结果会偏离使用的目的,即产生不可预测的结果;函数传参只在传参的时候求值一次,即无论是传址还是传值,本质均是函数针对所传递过来的数据进行操作,所以结果更容易控制;
参数类型宏的参数与类型无关,只要对参数的操作合法,那么此宏便可以适用于任何类型的参数。函数的参数是与类型有关的,如果参数的类型不同,就需要不同的参数,即使他们执行的任务是相同的;
调试宏不能调试函数可以逐语句进行调试
递归宏不能递归函数可以递归

怎么判断在一个条件下该使用宏还是使用函数呢?

  • 如果说该代码足够简单,那我们便可利用宏来写;倘若该代码写出来很复杂、行数多并且容易出错,介于宏不能进行调试,便无法观察执行的细节,所以我们便可以使用函数来解决问题;当然,当学到 c++ 的时候便就可以不用纠结于到底使用宏还是函数了,因为在 c++ 之中提供了 inline(内联函数)inline 具有了函数的优点,又具有了函数的优点;

(七)、命名约定

如此看来函数与宏在使用的语法上很相似,既然无法利用语言来帮我们区分二者,那么有什么办法能帮助我们区分二者呢?

  • 将宏名全部大写
  • 函数名不全部大写

注:但是也不要以为全是小写的一定是函数,例如: offsetof 全是小写,咋一看以为是函数,其实offsetof 本质上是; 函数getchar 中有部分实现可能利用了宏;

此处只是个约定,总有人不按照套路来走,所以“全大写是宏” 这种判断是宏还是函数的方法只是一种参考,具体靠谱地判断还是得结合代码;

三、#undef

#define 可以用来定义标识符,也可以用来定义宏,那么其定义能否被取消呢?

  • 利用#undef 便可以实现

#undef   用于移除一个宏定义


总结

1、在C语言本身便预定义了一些符号,这些符号是可以直接使用的;

__FILE__       //进行编译的源文件(文件名:路径+主干名+后缀) %s

__LINE__       //文件当前的行号  %d

__DATE__      //文件被编译的日期  %s

__TIME__       //文件被编译的时间  %s

__STDC__      //如果编译器遵循 ANSI C,其值为1,否则未定义 %d

2、#define 定义的标识符

语法: #define name stuff

3、#define 机制包括了一个规定,允许把参数替换到文本之中,这种实现通常称为宏(macro)或者定义宏(define macro)

宏的申明方式: #define name(parament-list)  stuff

注:

  • 其中的parament-list 是一个由逗号隔开的符号表,它们可能出现在 stuff 中;
  • 参数左括号必须与name 紧挨;倘若参数左括号与name 之间有任何的空白存在,均会将该参数列表被解释为stuff 的一部分;

4、#  --> 将一个宏的参数变成对应的字符串

5、## 可以把位于它两边的符号合并成一个符号,并且允许宏定义从分离的文本片段创建标识符

6、#undef   用于移除一个宏定义

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C语言深度剖析--不定期更新的第五弹

const关键字 来看一段代码&#xff1a; #include <stdio.h> int main() {int a 10;a 20;printf("%d\n", a);return 0; }运行结果如下&#xff1a; 接下来我们在上面的代码做小小的修改&#xff1a; #include <stdio.h> int main() {const int a 1…

【QT】基础入门学习

文章目录 浅析Qt应用程序的主函数使用qDebug()函数常用快捷键Qt 编码风格信号槽连接模型实现方案 信号和槽的工作机制Qt对象树机制 浅析Qt应用程序的主函数 #include "mywindow.h"#include <QApplication>// 程序的入口 int main(int argc, char *argv[]) {//…

IDA的安装和使用

IDA Pro&#xff08;简称 IDA&#xff0c;官网地址为 https://www.hex-rays.com/products/ida/&#xff09;是一个反编译器&#xff0c;同时具备调试器的功能。IDA Pro 的功能非常强大&#xff0c;几乎所有的逆向题目都需要用到它&#xff0c;因而也被称为「逆向神器」 IDA安装…

MySQL入门到精通

一、创建数据库 CREATE DATABASE 数据库名称; 如果数据库存在&#xff0c;则会提示报错。 二、选择数据库 USE 数据库名称; 三、创建数据表 CREATE TABLE 数据表名称; 四、MySQL数据类型 MySQL支持多种类型&#xff0c;大致可以分为三类&#xff1a;数值、日期/时间和字符串…

Data Filtering Network论文浅析

time2023-09paperhttps://arxiv.org/abs/2309.17425codehttps://huggingface.co/apple/DFN5B-CLIP-ViT-H-14-378org.Apple个人博客地址http://myhz0606.com/article/dfn Motivation 训练一个好的CLIP模型依赖大规模&#xff0c;高质量的训练数据。通过爬虫&#xff0c;可以很…

S7-1200与G120变频器CU240E-2控制单元通过353报文实现PN通信的基本方法

S7-1200与G120变频器CU240E-2控制单元通过353报文实现PN通信的基本方法 西门子报文353 PKW+PZD-2/2, 6个字中前4个字是PKW用,后2个字是PZD用, 结合以上内容, 可以知道第5个字是STW1控制字,第6个字是转速给定值(PLC输出);第5个字是ZSW1是状态字,第6个字是当前转速值(P…

LeetCode:快乐数(202)

目录 题目 代码思路 双指针 代码实现 题目 202. 快乐数 - 力扣&#xff08;LeetCode&#xff09; 编写一个算法来判断一个数 n 是不是快乐数。 [ 快乐数 ] 定义为&#xff1a; 对于一个正整数&#xff0c;每一次将该数替换为它每个位置上的数字的平方和。然后重复这个过程…

ThinkPHP5 5.0.23-rce远程代码执行漏洞复现

漏洞描述 ThinkPHP是一款运用极广的PHP开发框架。其版本5中&#xff0c;由于框架对控制器名没有进行足够的检测&#xff0c;会导致在没有开启强制路由的情况下可执行任意方法&#xff0c;从而导致远程命令执行漏洞。 启动容器 docker-compose up -d 查看端口 docker ps 端口为…

【C++进阶】hash表的封装

文章目录 hash表哈希表的关键组成部分哈希表的优缺点优点&#xff1a;缺点&#xff1a; 常见应用场景 开放定址法实现hash表负载因子 (Load Factor)负载因子的意义负载因子的影响再散列 (Rehashing)示例 整体框架insertFinderasehash桶封装框架insertfinderase~HashTable() 总结…

从路径优化学习FastPlanner之B样条曲线平滑(二):FastPlanner中B样条曲线代码理解与解读

参考别人的博客学习 根据之前一章只是大致了解了B样条数学原理&#xff0c;实际读代码还有疑惑。 控制点是什么&#xff1f;和规划出的路径点什么关系&#xff1f; 控制点可以说我们规划出的路径点&#xff0c;即n等于轨迹点个数。也可以不是轨迹点&#xff0c;通过线性方程反解…

Einsum(Einstein summation convention)

Einsum&#xff08;Einstein summation convention&#xff09; 笔记来源&#xff1a; Permute和Reshape嫌麻烦&#xff1f;einsum来帮忙&#xff01; The Einstein summation convention is a notational shorthand used in tensor calculus, particularly in the fields of …

免费也能高质量!2024年免费录屏软件深度对比评测

我公司因为客户覆盖面广的原因经常会开远程会议&#xff0c;有时候说的内容比较广需要引用多份的数据&#xff0c;我记录起来有一定难度&#xff0c;所以一般都用录屏工具来记录会议内容。这次我们来一起探索有什么免费录屏工具可以提高我们的工作效率吧。 1.福晰录屏大师 链…

9.7(QT.Day 1)

一、自由发挥登录窗口的应用场景&#xff0c;实现一个登录窗口界面 要求&#xff1a;每行代码都有注释 【需要用到的图片或者动图&#xff0c;自己去网上找】 1.mywidget.h代码 #ifndef MYWIDGET_H #define MYWIDGET_H#include <QWidget> #include <QIcon> //图…

C++之格式化日期时间为字符串(精确到毫秒)

C11中提供了获取时间的chrono库&#xff0c;但是格式化显示太方便&#xff1b;C11还提供了格式化时间iomanip库&#xff0c;通过其put_time方法可以格式化时间到秒&#xff0c;要显示毫秒&#xff0c;就需要自己处理了。 #include <chrono> #include <string> #in…

css flex与inline-flex的区别

1、flex与inline-flex布局&#xff0c;都是弹性布局&#xff0c;盒状容器布局。 Flex 布局是什么&#xff1f;_w3cschool 2、下面一个实例来说明一下&#xff1a; 有一个要求&#xff0c;要求item增多的时候&#xff0c;不会换行&#xff0c;并且container容器也会随时item的…

【机器学习】我小学二年级妹妹都能理解的线性回归算法

什么是线性回归&#xff1f; 我小学二年级的妹妹想用压岁钱买房子&#xff0c;售楼广告上写着100万可以买100的房子&#xff0c;200万可以买200的房子&#xff0c;但是我的妹妹觉得那些房子都太小了&#xff0c;想买300的房子&#xff0c;那她应该要花多少钱呢&#xff1f; 对…

DBMS-2.2 数据库设计(2)——数据库规范化设计理论

本文章的素材与知识来自李国良老师和冠宇老师。 依赖理论 对于关系数据库中的依赖&#xff0c;分为函数依赖、多值依赖和连接依赖。 一.函数依赖 1.函数依赖 &#xff08;1&#xff09;定义&#xff1a; &#xff08;2&#xff09;理解&#xff1a; 通俗地讲&#xff0c;…

【Linux】借命令行参数的引导,探索环境变量的奥秘

目录 1.命令行参数 1.1.概念&#xff1a; 1.2.利用命令行参数打造计算器&#xff1a; 2.环境变量 2.1.环境变量是什么&#xff1f; 2.2.有什么方法可以不用带路径&#xff0c;直接就可以运行自己的程序呢&#xff1f; 法一&#xff1a; 法二&#xff1a; 2.3.通过代码…

PostgreSQL技术内幕9:PostgreSQL事务原理解析

文章目录 0.简介1.PG事务整体介绍1.1 事务类型介绍 1.2 事务模块介绍2. 代码分析 0.简介 有了上一篇数据库事务并发控制协议的介绍&#xff0c;对于数据库事务和并发控制有了基本的认识&#xff0c;本文将介绍PG事务模块&#xff0c;主要介绍PG支持的事务类型&#xff08;普通…

git为不同的项目设置不同的提交作者

方法1&#xff1a;找到项目的.git文件夹打开 打开config在下面添加自己作者信息 [user]name 作者名email 邮箱方法2&#xff1a;直接在.git文件夹设置作者名&#xff08;不使用–global参数&#xff09; git config user.name "xxxxx"如果想要修改之前提交的…