预处理【详解】

news2025/1/13 2:57:24

在这里插入图片描述

本期介绍🍖

主要介绍:#define如何定义宏,宏替换的规则,为什么宏定义时不要吝啬我们的括号,为什么宏的参数不能带有副作用,宏和函数的区别。还讲解了预处理符号#和##,#undef指令,众多条件编译指令,以及文件包含的方式,与如何解决文件的重复包含👀。


文章目录

  • 一、预定义符号🍖
  • 二、#define🍖
    • 2.1 #define 定义标识符🍖
    • 2.2 #define 定义宏🍖
      • 2.2.1 宏应尽量多用括号🍖
      • 2.2.2 宏替换规则🍖
      • 2.2.3 带副作用的宏参数🍖
      • 2.2.4 宏与函数的对比🍖
      • 2.2.5 命名约定🍖
  • 三、预处理操作符#和##🍖
    • 3.1 #的用法🍖
    • 3.2 ##的用法🍖
    • 3.3 嵌套使用宏🍖
  • 四、#undef 指令🍖
  • 五、命令行定义🍖
  • 六、条件编译🍖
  • 七、文件包含🍖
    • 7.1 头文件被包含的方式🍖
    • 7.2 重复头文件包含的解决办法🍖


一、预定义符号🍖

  C语言在设计之初就预先定义了下面这些可以使用,但不能修改的宏。

  1. __FILE__:返回当前正在编译文件的文件名。
  2. __LINE__:返回当前的行号。
  3. __DATE__:返回文件编译日期。
  4. __TIME__:返回文件编译时间。
  5. __STDC__:如果编译器遵循ANSI C,其值为1,否则未定义。

  下面就可以
在这里插入图片描述


二、#define🍖

2.1 #define 定义标识符🍖

  大多数时候我会像这样#define MAX 100来使用#define,也就是定义一个数字常量。但如果你了解过#define的替换逻辑,就会发现它的用法可不仅于此。就譬如:

  1. 定义字符串常量:#define name "xiaoming"
  2. 简化关键字:#define reg register
  3. 替换语句:#define forever for(i = 0; i < 10; i++)

  注意:如果#define定义的内容过长可以换行继续写,但需要在存了最后一行外,每一行后面加一个反斜杠(续行符),如下所示。

#define DEBUG_PRINT printf("file:%s\tline:%d\t \
                          date:%s\ttime:%s\n" ,\
						__FILE__,__LINE__ ,\
						__DATE__,__TIME__ ) 

2.2 #define 定义宏🍖

  #define除了可以定义标识符之外,还有另外一种机制,可以像函数那样传递参数,这种实现被称为:定义宏

语法:#define name( parament-list ) stuff
其中name是宏名,parament-list是参数列表,需要用逗号隔开,stuff为宏体,也就是宏的内容。

  注意:宏是把参数直接替换到宏内容中去的,与函数传递参数有本质的区别。函数在传递参数前会先把值求出来,然后在传递过去。可宏不是这样,参数是什么样,替换过去还是什么样。下面举个例子:

在这里插入图片描述

2.2.1 宏应尽量多用括号🍖

  我们在定义宏的时候,应尽量不吝啬括号。为什么呢?要知道宏会把参数原封不动的替换到宏体中,这就导致了一问题的出现:如果参数中具有操作符,并且宏体中也有操作符,那么当参数替换完成后,可能就会因为操作符的优先级,而使得表达式的计算顺序达不到我们的预期。下面来举个例子:

在这里插入图片描述

  可以见得由于习惯,我们通常会默认参数部分为一个整体,传参后也为一个整体。所以,就会导致上例的错误出现。那该怎么解决呢?既然你认为它是一个整体,那就把它真的变成一个整体。我们只需在定义宏时,给每个参数加上括号即可。如下所示:

在这里插入图片描述

  既然想到参数与宏体会因为操作符的优先级而导致计算顺序出现偏差,那么也该联想到宏最终会替换到代码中原先调用宏的位置,如若被替换的宏原先就存在于一个表达式中,那么必然也会因为操作符优先级的关系而导致运算顺序的问题出现。所以我们因该把宏体看成一个整体,给它加上一个括号#define POWER(x) ((x) * (x))


2.2.2 宏替换规则🍖

  1. 调用宏时,首先会对参数部分进行检查,若参数部分包含#define定义的符号,则优先进行替换处理。
  2. 参数替换到文本中,随后插入到程序中原来调用宏的位置。
  3. 最后,在对结果文件进行扫描,看看是否包含任何#define定义的符号。如果是,就重复上述处理过程。

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


2.2.3 带副作用的宏参数🍖

  首先,我来解释一下什么叫带副作用。举个例子:我定义了两个参数int a = 1;int b = 0;,想把a的值+1后赋给b,一般会写成b = a + 1;,但还有一种写法b = a++;。不可否认,这种写法确实可以达到我们目的,但会让变量a自增1,可我们并不想让a自增啊,所以我们就称这种写法为带副作用的。

  了解什么是带副作用后,思考一下:如果宏的参数也带有副作用,会引起什么样的后果? 我们知道宏的参数会原封不动的替换到宏体中的,如若此时宏定义中多次包含参数,并且参数还带有副作用,就会重复多次产生副作用,从而导致不可预测的后果。所以我们在使用宏时应该尽量然参数不带有副作用。其实,函数也是可能带有副作用的,这一点需要注意。下面来举个例子:

在这里插入图片描述


2.2.4 宏与函数的对比🍖

  我们可以通过上面这个例子来比较宏与函数。例子:分别用函数和宏来实现比较两个数的大小。下面是实现:

//宏的实现
#define MAX(x, y) ((x)>(y) ? (x):(y)) 

//函数的实现
int Max(int x, int y)
{
	return (x > y ? x : y);
}

  可以看出,两者在代码长度是差不多的。那对于这个例子来说,到底是使用宏好还是函数好呢?在我看来,使用宏比较好。为什么?

  首先,使用宏绝对比使用函数的开销要小得多,我们知道函数的调用是非常复杂的,需要传参、压栈、开辟函数栈帧空间、计算、出栈等等一系列操作。反观宏,它只需执行行文本操作小型的计算工作就行。所以,宏比函数在程序的规模和速度方面更胜一筹

   其次,函数具有严格的类型限定。就算两个函数内容完全一样,仅参数的类型不同,那也是两个完全不同的函数,无法合并成一个的。但宏就比较灵活,在这个例子当中,只要能通过>符号进行比较的类型,都可以用这个宏。可见,宏比函数灵活,函数比宏严谨。

  除此之外,宏还有几个缺点我们也是要知道的:

  1. 宏定义较长,会大幅增加程序的长度
    因为每一次使用宏都会在代码中插入一份宏定义。如果宏定义较长,必然会大幅增长代码的长度。你想啊,如果宏定义了1000行代码,每一次使用该宏,都会向程序中插入一份1000行的代码,必然会严重增加程序的开销。但如果使用函数来定义这1000行的代码,程序中只需要存在一份代码,每次调用这个函数即可。可以看出,这是函数的一个有优点。
  2. 宏无法调试
    我们知道C源代码需要经过预编译、编译、汇编、链接阶段后,才能生成可执行程序,真正的运行起来。调试其实是运行阶段干的事,而早在预编译阶段,宏就已经完成了替换。使得调试时内存中运行的是一套代码,而肉眼所见的又是另一套代码。无法调试,无法调试,不是不能进行调试,而是对于宏来说调试是无意义的,我们无法观测到在内存中真正执行的代码的。
    一旦替换完成后的代码与我们所想的逻辑有所出入的话,你就会发现明明代码没什么问题,但调试的结果却不尽人意,这时你就会开始怀疑是不是编译器出bug了啊。我想告诉你的时:想多啦同学。当然函数是可以进行调试的,这是函数的一个优点。
  3. 宏可能会带来运算符优先级的问题,导致程容易出现错
    这个之前讲过,我们需要在宏定义的时候尽量不吝啬括号。

  宏有时还可以做到函数无法做到的事,比如:宏的参数可以是类型。举个例子:

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

int main()
{
	//开辟一块4个int型大小的动态内存空间
	int* p = MALLOC(4, int);
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}
	return 0;
}

2.2.5 命名约定🍖

  一般来说,函数与宏使用的语法很相似,故语言本身无法帮我们区分二者,所以我们一般的使用习惯是:宏名全部大写,函数名不全部大写


三、预处理操作符#和##🍖

3.1 #的用法🍖

在宏参数前面使用#号,则此参数会变成字符串

在这里插入图片描述

  当我们想要同时打印变量的值与变量名,在只传一个参数的前提下,函数是无法实现的。但宏却可以,我们只需在传过去的参数加上一个#号就行。如下所示:

在这里插入图片描述


3.2 ##的用法🍖

##可以把位于其两端的符号合成一个符号

在这里插入图片描述


3.3 嵌套使用宏🍖

  如果一个宏的参数是另一个宏的话,由于宏的替换规则,会先对参数中#define定义的符号进行替换操作。但凡事都有例外,在使用了###的宏中,如果其参数为另外一个宏,则会阻止另这个宏的展开。如下所示:

在这里插入图片描述
在这里插入图片描述

  为了保证宏的参数优先展开,我们可以使得带有###的宏多嵌套一层宏定义,这样参数就可以优先展开了。如下所示:

在这里插入图片描述


四、#undef 指令🍖

#undef指令用于移除一个宏定义

#define MAX(x, y) ((x)>(y)?(x):(y))

int main()
{
#undef MAX
	//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
#define MAX 30
	printf("%d\n", MAX);
	
	return 0;
}

五、命令行定义🍖

  许C的编译器提供了一种能力,可以在命令行定义变量。这样就可以使得同一份源文件编译出不同的版本,这样我们就可以根据不同的环境进行适配了。就譬如说:假如程序中定义了一个数组,长度为未定义的标识符。我们就可以使得这个程序,根据不同的不同的设备进行适配。如果这太机子内存比较小,我们就可以使得定义的数组长度更小,反之则数组长度可以更大一点。


六、条件编译🍖

  条件编译是指预处理器可以根据条件编译指令,选取源程序中我们想要进行编译的代码进行编译。这样就能使得我们的程序,根据各种不同的情况来进行适配。如此可以看出,条件编译指令能够解决跨平台性问题

条件编译指令说明
#if如果条件为真,则执行下面的操作
#elif如果前面的条件为假,而该条件为真,则执行下面的操作
#else如果前面的条件为假,则执行下面的操作
#endif用于结束条件编译指令
#ifdef如果宏已经定义,则执行下面的操作
#endif如果宏没有定义,则执行下面的操作
#if defined( )如果宏已经定义,则执行下面的操作
#if !defined( )如果宏没有定义,则执行下面的操作

七、文件包含🍖

7.1 头文件被包含的方式🍖

  包含头文件的方式有两种#include<stdio.h>#include"Add.h",一个是用尖括号<>来包含的,另一个是用双引号""来包含的。两者的区别在于查找的策略不同

  1. <>会直接去编译器的库目录底下查找
  2. ""会先去代码所在路径底下去查找,如果没找到,再去库目录底下去查找。

  所以如果包含的是库函数,直接用<>就行了,而包含我们自己写的头文件,用""来包含。


7.2 重复头文件包含的解决办法🍖

  一个团队在开发一款软件的时候,每一个成员都有自己所需负责的模块。实现各自的模块时,必然会引用团队的公共库函数。当最后合并所有模块后,必然多次重复包含公共库函数,使得代码大量冗余。

  那怎么解决这个问题呢?其实可以通过条件编译指令来实现。如下所示:

  方法一:

#ifndef __TEST_H__//如果没有定义过__TEST_H__,则执行下面这些
#define __TEST_H__//定义__TEST_H__

//……头文件内容

#endif

  方法二:

# pragma once
//在头文件开头加上一句这个就不会被重复包含了

在这里插入图片描述

这份博客👍如果对你有帮助,给博主一个免费的点赞以示鼓励欢迎各位🔎点赞👍评论收藏⭐️,谢谢!!!
如果有什么疑问或不同的见解,欢迎评论区留言欧👀。

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

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

相关文章

Swift(4)

目录 Dictionary集合 组合赋值符号 区间运算符 字符串字面量里的特殊字符 操作字符 Dictionary集合 相当于java中的Map集合 函数与闭包 函数也可以内嵌 函数其实就是闭包的一种特殊形式&#xff1a;一段可以被随后调用的代码块。闭包中的代码可以访问其生效范围内的变量和…

SpringMVC的@RequestMapping注解

SpringMVC的RequestMapping注解RequestMapping注解的功能RequestMapping注解的位置RequestMapping注解的value属性RequestMapping注解的method属性RequestMapping注解的params属性RequestMapping注解的headers属性什么是headers属性RequestMapping注解的功能 从注解名称上我们…

二叉树23:验证二叉搜索树

主要是我自己刷题的一些记录过程。如果有错可以指出哦&#xff0c;大家一起进步。 转载代码随想录 原文链接&#xff1a; 代码随想录 leetcode链接&#xff1a;344. 反转字符串 题目&#xff1a; 给你一个二叉树的根节点 root &#xff0c;判断其是否是一个有效的二叉搜索树。…

26. 命名空间

前言&#xff1a;如果你接触过c/c/c#/java&#xff0c;那么对于python中的命名空间也是如此&#xff0c;只不过在些许地方存在细微差异&#xff0c;不过倒无伤大雅。 1. 定义 命名空间(Namespace)是从名称到对象的映射&#xff0c;大部分的命名空间都是通过 python 字典来实现…

lamda表达式

lamda表达式可以看作是一个匿名函数。编译器在编译的时候&#xff0c;会将lamda表达式处理成一个仿函数类&#xff0c;类名是类名是不重复的随机名称&#xff08;因为一个作用域中可能存在多个仿函数&#xff09;&#xff0c;返回该仿函数的对象。 lamda既然可以看作是一个匿名…

文件操作(C语言)

目录 1、为什么使用文件 2、什么是文件 程序文件 数据文件 文件名 3、文件的打开和关闭 文件指针 文件的打开和关闭 4、文件的顺序读写 文件读写介绍 文件读写函数 fputc&#xff08;字符 输出/写 函数&#xff09; fgetc&#xff08;字符 输入/读 函数&#xff09; fputs&…

VMware Workstation安装:与 Device/Credential Guard 不兼容

VMware Workstation安装&#xff1a;与 Device/Credential Guard 不兼容 1、快速解决 安装最新版VMware Workstation&#xff0c;例如我安装的是VMware Workstation Pro 17&#xff1a; 是的&#xff0c;解决报错的办法&#xff0c;就是安装/升级VMware Workstation版本。 参…

Aspose.PDF 23.1.0 for .NET Crack

Aspose.PDF for .NET可以在 .NET 程序中生成、屏蔽、编辑甚至将 PDF 文件转换为多种格式&#xff0c;而无需依赖 Adob​​e Acrobat。它是.Net核心对PDF的创新处理&#xff0c;可以在跨平台软件中执行文档操作甚至任务管理。借助 API&#xff0c;用户可以创建、更改、呈现、保护…

Spring Cloud:网关Gateway

✨ Spring Cloud:网关Gateway微服务网关概述为什么需要微服务网关服务网关的介绍Getway基本介绍微服务架构中网关所处位置三大核心概念工作流程入门案例搭建新建模块**cloud-gateway-gateway9527**导入依赖application.yml主启动类实现动态路由测试Predicate 断言基本介绍Route…

详解信奥一本通1290:采药

1290&#xff1a;采药【题目描述】辰辰是个很有潜能、天资聪颖的孩子&#xff0c;他的梦想是称为世界上最伟大的医师。为此&#xff0c;他想拜附近最有威望的医师为师。医师为了判断他的资质&#xff0c;给他出了一个难题。医师把他带到个到处都是草药的山洞里对他说&#xff1…

Fisco Bcos区块链三(webase中间件平台一键部署)

文章目录区块链开荒技术文档&#xff1a;https://fisco-bcos-documentation.readthedocs.io/zh_CN/latest/index.html4. Webase一键部署Java环境变量配置MySQL安装Ubuntu安装mysql数据库安装后设置密码&#xff1a;Python部署PyMySQL部署&#xff08;Python3.6&#xff09;拉取…

SpringBoot-过滤器的使用(在访问页面时过滤掉未登录的用户使其不能访问相应页面)

目录 概述 前端编写 页面展示 后端编写 编写接口 过滤器的编写 过滤器功能实验 概述 解决需求&#xff1a;在用户未登录的情况下访问未登录不可访问的页面时&#xff0c;请求将被过滤到&#xff0c;将用户退回登录页面。 技术选型 前端&#xff1a;Vue ElementUI 后端…

MySQL事务的四大特性以及并发事务问题

事务的四大特性ACID 【原子性&#xff08;Atomicity&#xff09;】&#xff1a;事务是不可分割的最小单元&#xff0c;要么全部成功&#xff0c;要么全部失败。&#xff08;eg&#xff1a;转账案例&#xff0c;转账过程中任何一步操作失败了&#xff0c;那么整个事务就失败了&a…

Python装饰器使用方法详解

文章目录1 装饰器背景知识1.1 基本概念1.2 应用场景2 简单的装饰器代码3 使用装饰器记录函数执行次数4 带参数的装饰器5 装饰器处理有返回值的函数1 装饰器背景知识 1.1 基本概念 装饰器&#xff08;Decorator&#xff09;是 Python 中一种函数或类&#xff0c;用来修饰其他函…

RabbitMQ部署

RabbitMQ部署1.单机部署1.1.下载镜像1.2.安装MQ1.3访问管理端2.集群部署2.1.集群分类2.2.设置网络1.单机部署 我们在Centos7虚拟机中使用Docker来安装&#xff0c;如未安装dockr&#xff0c;请参考《Centos7安装Docker》 1.1.下载镜像 方式一&#xff1a;在线拉取 docker …

C语言linux线程库pthread的简单使用教程

POSIX线程&#xff08;pthread&#xff09;库 POSIX线程库是用于C/C的基于标准的线程API。它允许产生一个新的并发流程。它在多处理器或多核系统上最为有效&#xff0c;在这些系统中&#xff0c;可以将流程安排在另一个处理器上运行&#xff0c;从而通过并行或分布式处理提高速…

flask框架全解

文章目录简介wsgiref安装配置文件方式一&#xff08;debug方式配置&#xff09;方式二&#xff08;环境变量方式&#xff0c;很少见&#xff09;方式三&#xff08;配置文件方式&#xff09;其他配置方式flask app路由组成写法动态路由的过滤查询字符串传参json和form等数据反向…

deepin系统如何安装惠普打印机

deepin系统如何安装惠普打印机 导读 想必现在有很多小伙伴对于deepin系统如何安装惠普打印机 安装惠普打印机的方法方面的知识都比较想要了解&#xff0c;那么今天小好小编就为大家… 想必现在有很多小伙伴对于deepin系统如何安装惠普p1007打印机 安装惠普打印机的方法方面的知…

【图卷积神经网络】02-谱域图卷积介绍

注&#xff1a;本文为第2章谱域图卷积介绍视频笔记&#xff0c;仅供个人学习使用 目录1、图卷积简介1.1 图卷积网络的迅猛发展1.2 回顾&#xff0c;经典卷积神经网络已在多个领域取得成功1.3 两大类数据1.4 经典卷积神经网络的局限&#xff1a;无法处理图数据结构1.5 将卷积扩展…

Python学习基础之快速入门

目录 首先我们下载最新的python版本&#xff1a;3.0 编写一个hello world 什么是python里面的IPO python运行有几种模式 编写一个温度转换器 在windows上执行python程序 官网&#xff1a;Welcome to Python.org Python 是一门易于学习、功能强大的编程语言。它提供了高效的…