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

news/2024/9/17 1:38:30/ 标签: c语言

文章目录

前言

一、预定义符号

二、#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.ppmy.cn/news/1522685.html

相关文章

【双指针】N数之和

N数之和 两数之和题目题目解析暴力思路双指针优化 三数之和题目题目解析暴力思路双指针优化 四数之和题目题目解析暴力思路双指针优化 两数之和 题目 题目链接: 查找总价格为目标值的两个商品 虽然题目名字不是两数之和, 但是由于和后面的三数之和, 四数之和是连起来的, 于是…

【持续更新】Adobe Audition 2024 (v24.4.1.003)最新免费修改版

Adobe Audition是一款专为录音、编辑和掌握音频素材设计的专业解决方案。此编辑器支持从MP3、AAC到AIFF等多种重要格式&#xff0c;并能从CD中导入音轨。 其多轨编辑功能使您可以在任意数量的轨道上混合音乐、语音和声音片段&#xff0c;运用丰富的工作室动态效果&#xff0c;如…

nginx配置中的服务器名称

通常&#xff0c;在nginx的配置节中&#xff1a; server {listen 80;server_name example.org www.example.org;... } server_name(服务器名称) 指令定义确定哪个服务器块用于给定请求。可以使用确切名称、通配符名称、ip地址或正则表达式来定义它们&#xff1a; se…

如何在 AWS S3 中设置跨区域复制

如何在 AWS S3 中设置跨区域复制 概述 欢迎来到雲闪世界。 Amazon Simple Storage Service (S3) 是一种可扩展的对象存储服务&#xff0c;广泛用于存储和检索数据。其主要功能之一是跨区域复制 (CRR)&#xff0c;允许跨不同的 AWS 区域自动异步复制对象。此功能对于灾难恢复、…

二手手机回收小程序搭建,小程序功能特点

随着社会生活水平的提高&#xff0c;对手机的更新换代的速度也在逐渐加快&#xff0c;出现了大量的闲置手机&#xff0c;而这也给手机回收市场带来了巨大的发展空间&#xff01; 目前&#xff0c;手机回收市场进入到了发展快速期&#xff0c;吸引了越来越多的企业加入大市场中…

4.7 Sensors -- useScroll

4.7 Sensors – useScroll https://vueuse.org/core/useScroll/ 作用 响应式的监听滚动位置和状态。 官方示例 <script setup lang"ts"> import { useScroll } from vueuse/coreconst el ref<HTMLElement | null>(null) const { x, y, isScrolling…

Spring常用中间件

1. 数据库中间件 &#xff08;1&#xff09;MySQL: 常用的关系型数据库&#xff0c;支持JDBC和JPA。 &#xff08;2&#xff09;PostgreSQL: 功能强大的开源关系型数据库&#xff0c;支持复杂查询。 &#xff08;3&#xff09;MongoDB: NoSQL数据库&#xff0c;适合存储非结构化…

【Rust练习】13.数组

练习题来自&#xff1a;https://practice-zh.course.rs/compound-types/array.html 1 fn main() {// 使用合适的类型填空let arr: __ [1, 2, 3, 4, 5];// 修改以下代码&#xff0c;让它顺利运行assert!(arr.len() 4); }显然这个数组的长度是5. fn main() {// 使用合适的类…

ELK学习笔记(三)——使用Filebeat8.15.0收集日志

使用Filebeat收集日志 前面教程已经把ElasticSearch和Kibana部署完毕&#xff0c;接着我们就要使用filebeat去收集我们的java服务日志&#xff0c;这里首先介绍一下ELK和EFK的区别。 一、ELK和EFK的区别 在收集和处理日志时&#xff0c;使用 ELK&#xff08;Elasticsearch, …

8. GIS数据分析师岗位职责、技术要求和常见面试题

本系列文章目录&#xff1a; 1. GIS开发工程师岗位职责、技术要求和常见面试题 2. GIS数据工程师岗位职责、技术要求和常见面试题 3. GIS后端工程师岗位职责、技术要求和常见面试题 4. GIS前端工程师岗位职责、技术要求和常见面试题 5. GIS工程师岗位职责、技术要求和常见面试…

tabBar设置底部菜单选项以及iconfont图标

tabBartabBar属性:设置底部 tab 的表现 ​ ​ ​ ​ 首先在pages.json页面写一个tabBar对象,里面放入list对象数组,里面至少要有2个、最多5个 tab, 如果只有一个tab的话,H5(浏览器)依然可以显示底部有一个导航栏,如果没有,需要重启后才有,小程序则报错,只有2个以上才可以…

C# 窗口页面布局

1.Groupbox 单机鼠标右键&#xff0c;置于底层 2.Label 在右方属性中修改名称 3.ComboBox 点击属性中的集合&#xff0c;可以添加选择项 4.CheckBox 在属性中修改名称 5.RichTextBox 富文本 在属性中修改名称与区域 6.StatusStrip 状态栏 将AutoSize改成false就可以修改…

基于Java的宿舍报修管理系统的设计与实现(论文+源码)_kaic

基于Java的宿舍报修管理系统的设计与实现(论文源码)_kaic 摘  要 随着教育改革‎‏的不断‎‏深入&#xff0c;‎‏学校宿‎‏舍的管‎‏理体系‎‏也在不‎‏断地完‎‏善&#xff0c;校园后勤服务是学校管理的重要工作&#xff0c;学校提供优秀的后勤服务&#xff0c;能提…

C语言代码练习(第十七天)

今日练习&#xff1a; 45、输出100-1000之间所有的“水仙花数”&#xff0c;所为的水仙花数是一个三位数&#xff0c;其各位数字立方和等于该数本身。例如153是一个水仙花数。因为1*1*15*5*53*3*3 46、一个数如果恰好等于它的因子之和&#xff0c;这个数就称为"完数"…

AI视频百万播放,用这个免费的AI工具,3步教你制作爆款治愈系视频!(附完整教程)

大家好&#xff0c;我是程序员X小鹿&#xff0c;前互联网大厂程序员&#xff0c;自由职业2年&#xff0c;也一名 AIGC 爱好者&#xff0c;持续分享更多前沿的「AI 工具」和「AI副业玩法」&#xff0c;欢迎一起交流~ 今天一位粉丝发了一个视频链接&#xff0c;问这类治愈系风景的…

centos基本命令

当前登录用户&#xff08;root&#xff09; 用户组 其它用户 rwxr-xr-x cd 后加/目录名/子目录 切换到目录 cd .. 切换到父目录 CentOS Windows $>ls 查看某个目录有哪文件和目录 cmd>dir …

机器学习:多种算法处理填充后的数据

在机器学习中&#xff0c;填充数据&#xff08;即处理缺失值&#xff09;后&#xff0c;选择合适的算法并优化模型以提高召回率是一个常见的任务。召回率是指模型正确识别的正例占所有实际正例的比例。 代码思路&#xff1a; 数据预处理&#xff1a; 导入填充后的数据 …

Python | Leetcode Python题解之第386题字典序排数

题目&#xff1a; 题解&#xff1a; class Solution:def lexicalOrder(self, n: int) -> List[int]:ans [0] * nnum 1for i in range(n):ans[i] numif num * 10 < n:num * 10else:while num % 10 9 or num 1 > n:num // 10num 1return ans

太速科技-1路万兆光纤SFP+和1路千兆网络 FMC子卡模块

1路万兆光纤SFP和1路千兆网络 FMC子卡模块 一、概述 该板卡是基于kc705和ml605的fmc 10g万兆光纤扩展板设计&#xff0c;提供了1路万兆光纤SFP和1路千兆网络接口。可搭配我公司开发的FPGA载卡使用。载卡可参考&#xff1a;ID204 SFP&#xff08;10 Gigabit Small…

1-10 图像增强对比度 opencv树莓派4B 入门系列笔记

目录 一、提前准备 二、代码详解 enhanced_image cv2.convertScaleAbs(image, alpha1.5, beta0) 三、运行现象 四、完整工程贴出 一、提前准备 1、树莓派4B 及 64位系统 2、提前安装opencv库 以及 numpy库 3、保存一张图片 二、代码详解 import cv2 # 增强图像的对比度 …