C语言之详解预处理

前言:

预处理也叫预编译,是编译代码时的第一步,经过预处理后生成一个.i文件,如果不明白编译与链接作用的小伙伴可以先看看博主的上一篇博客—— ,不然知识连贯性可能会显得很差哦。

正文目录:

  1. 预定义符号
  2. #define定义常量
  3. #define定义宏
  4. 带有副作用的宏参数
  5. 宏替换的规则
  6. 宏与函数的对比和命名约定
  7. #和##
  8. #undef
  9. 条件编译
  10. 头文件的包含
  11. 其他预处理指令......

1.预定义符号

如下为c语言中的预定义符号:

  • __FILE__   //进⾏编译的源⽂件
  • __LINE__   //⽂件当前的⾏号
  • __DATE__   //⽂件被编译的⽇期
  • __TIME__   //⽂件被编译的时间
  • __STDC__   //如果编译器遵循ANSI C,其值为1,否则未定义

这些都是我们可以直接使用、在预处理阶段就已经处理了的。

我们举个例子:

2.#define定义常量

基本语法形式如下:

#define name stuff        //其中name表示名字, stuff表示(被替换的)内容

当我们用该语法后,stuff就被name给替换了。

示例如下:

此时100就被M替换了,因此a输出结果为100。

在预处理阶段,会将#define定义的“名字”替换为它所表示的常量,比如说上图中的a = M ,经预处理后实际的代码形式为a = 100

3.#define定义宏

如下为宏的申明方式:

#define name( parament-list ) stuff   //parament-list为参数列表,stuff为内容。

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

示例如下:

但是需要注意,#define定义宏可能会带来运算级优先级的问题。

就跟上面同样的题目,我们换种写法如下:

可以发现,我们M(a)中的a本来为10,但当我们写成9 + 1后结果与我们所期待的不符。

这是为什么呢?——上文讲过,“在预处理阶段,会将#define定义的“名字”替换为它所表示的常量”,此处的“名字”就相当于M(9 + 1)。当替换后,该行代码就变为了“a = 9 + 1 * 9 + 1”。

容易发现跟我们的要求不符,这就是运算符优先级的问题。

我们可以这样修改:

如上图所示,我们加个小括号就可以了。因此我们用#define定义宏时,一定不要吝啬括号

4.带有副作用的宏参数

简单来讲副作用就是说会导致参数发生改变

严谨点的就是下面的说法:

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

例如:

x + 1;      //不带副作⽤
x++;       //带有副作⽤

具体示例如下:

当我们后续再使用代码中的a、b时,就可能不是我们所期望的值了。

5.宏替换的规则

这个就是偏概念性的东西了,规则如下:

在程序中扩展#define定义符号和宏时,需要涉及⼏个步骤。

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

我们给出一个示例,按照宏替换的规则进行替换,如下图所示:

然后我们就将第二个宏定义的内容替换到主函数中的相应宏中,

因此最终主函数的第一行代码被替换为了:int m = ((a) > (b) ? (a) : (b));

6.宏与函数的对比和命名约定

我们先讲二者的命名约定:

容易发现,函数的宏的使⽤语法很相似。所以语言本⾝没法帮我们区分⼆者。
因此我们平时的⼀个习惯是:
把宏名全部⼤写
函数名不要全部⼤写

宏与函数的对比

宏一般用于简单的运算,因为如果简单的运算就使用函数的话会加大我们的计算时间。

因此宏相对于函数:

宏在程序的规模和速度更胜一筹(规模更小,运算更快)

不仅如此,宏的参数与类型无关

而宏较于函数的劣势处:

  1. 每次使⽤宏的时候,⼀份宏定义的代码将插⼊到程序中。除⾮宏⽐较短,否则可能⼤幅度增加程序的⻓度。
  2. 宏是无法调试的
  3. 参数与类型无关,因此不够严谨
  4. 可能带来运算符优先级问题,容易出错

但是宏却可以做到一些函数永远做不到的事——比如说参数中出现类型。

两者具体差异如下表所示:

7.#和##

#运算符

#可以理解为“字符串化

#运算符将宏的⼀个参数转换为字符串字⾯量。它仅允许出现在带参数的宏的替换列表中。

示例如下:

##运算符

##可以理解为“联结符号

## 被称为记号粘合,它可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离的文本片段创建标识符。 这样的连接必须产⽣⼀个合法的标识符,否则其结果就是未定。

示例如下:

但是这样就有点麻烦了 一次求值就要写一个函数 因此我们可以换种方式写👇:

//假设我们要求两个数中的较大值
#define GENERIC_MAX(type) \
type type##_max(type x, type y)\
{ \
return (x > y ? x : y); \
}GENERIC_MAX(int)   //替换到宏体内后int##_max 生成了新的符号 int_max做函数名
GENERIC_MAX(float) //替换到宏体内后float##_max 生成了新的符号 float_max做函数名int main()
{//调用函数int m = int_max(2, 3);printf("%d\n", m);float f = float_max(3.5f, 4.5f);printf("%f\n", f);return 0;
}

这样子 我们就可以直接通过宏来求 而不用每次求不同类型的数据时都要写不同的函数。

8.#undef

移除一条宏定义

示例如下:

9.条件编译

简单来讲就是“满足条件就编译,不满足条件就不编译”。


像调试性的代码,删除可惜,保留⼜碍事,我们就可以选择性的编译。

示例如下:

其中#if#endif就是条件编译语句。易知1 != 2 因此没有打印hehe。

10.头文件的包含

头文件的包含一般分为两种形式:

  1. <······>   (如#include <stdio.h>)
  2. "······"   (如#include "filename")

前者库文件包含,一般指标准库中头文件的包含。

查找头文件直接去标准路径下去查找,如果找不到就提⽰编译错误。
这样是不是可以说,对于库⽂件也可以使⽤" "的形式包含呢?——
答案是肯定的,可以是可以,但是这样做查找的效率就低些,而且这样也不容易区分是库⽂件还是本地文件了。

后者本地文件包含,一般指自己创建的头文件的包含。

查找策略:先在源文件所在⽬录下查找,如果该头文件未找到,编译器就像查找库函数头文件⼀样在标准位置查找头⽂件。
如果还找不到就提⽰编译错误。

除了上述两种外,其实还有一种情况——嵌套文件包含

#include 指令可以使另外⼀个⽂件被编译,就像它实际出现于 #include 指令的
地⽅⼀样。
这种替换的⽅式很简单:预处理器先删除这条指令,并用包含⽂件的内容替换
⼀个头⽂件被包含10次,那就实际被编译10次,如果重复包含,对编译的压⼒就⽐较⼤。

比如下面的代码:

#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
int main()
{return 0;
}

其中的十条#include "test.h",在编译时都被test.h头文件中包含的内容替换了,导致编译压力较大。 这种情况就是嵌套文件包含。

那么我们该如何解决这些问题呢?——自然是用刚刚学的条件编译了。

1.每个头文件的开头写:

#ifndef __TEST_H__
#define __TEST_H__
//头⽂件的内容
#endif 
//__TEST_H__

2.或者

 #pragma once

这样就可以有效避免头文件的重复引入了。

11.其他预处理指令

#error
#pragma
#line
...
...

#pragma pack()  

我们还有很多其他的预处理指令,本文自然不可能给大家一 一讲完,大家可以自行去了解哦~



创作不易,如果作者写的还行的话给个免费的三连吧亲😙😙

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

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

相关文章

font-spider按需生成字体文件

font-spider可以全局安装,也可以单个项目内安装,使用npm run xxxx的形式 npm i font-spider "dev": "font-spider ./*.html" <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name&…

Android测量

最大模式&#xff08;MeasureSpec.AT_MOST&#xff09; 这个也就是父组件&#xff0c;能够给出的最大的空间&#xff0c;当前组件的长或宽最大只能为这么大&#xff0c;当然也可以比这个小。 最高两位是11的时候表示”最大模式”。即MeasureSpec.AT_MOST未指定模式&#xff08;…

1996年-2023年 全国298个地级市-外商直接投资FDI(数据收集)

外商直接投资&#xff08;FDI&#xff09;是一种跨国界的经济活动&#xff0c;它涉及外国投资者在中国境内进行的直接投资行为。这种投资行为不仅包括以货币、实物、技术等形式的资本投入&#xff0c;还可能包括开办独资企业、合资企业、合作企业&#xff0c;以及参与资源开发等…

微型操作系统内核源码详解系列五(四):cm3下svc启动任务

系列一&#xff1a;微型操作系统内核源码详解系列一&#xff1a;rtos内核源码概论篇&#xff08;以freertos为例&#xff09;-CSDN博客 系列二&#xff1a;微型操作系统内核源码详解系列二&#xff1a;数据结构和对象篇&#xff08;以freertos为例&#xff09;-CSDN博客 系列…

开发者配置项、开发者选项自定义

devOptions.vue源码 <!-- 开发者选项 &#xff08;CtrlAltShiftD&#xff09;--> <template><div :class"$options.name" v-if"visible"><el-dialog:custom-class"sg-el-dialog":append-to-body"true":close-on…

Flutter 如何发布安卓应用?

android:hardwareAccelerated“true” android:windowSoftInputMode“adjustResize”> <meta-data android:name“flutterEmbedding” android:value“2” /> Flutter生成的文件建议是大部分内容可以保留不动&#xff0c;但是可以根据需要进行修改。 具体可能要修…

STM32读写备份寄存器和实时时钟

文章目录 1. 硬件电路 2. RTC操作注意事项 操作步骤 3. 代码实现 3.1 读写备份寄存器 3.1.1 main.c 3.2 实时时钟 3.2.1 MyRTC.c 3.2.2 MyRTC.h 3.2.3 main.c 1. 硬件电路 对于BKP备份寄存器和RTC实时时钟的详细解析可以看下面这篇文章&#xff1a; STM32单片机BKP备…

Linux查看公网IP的解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

字节大牛耗时八个月又一力作,Android性能调优秘籍:设计思想与代码质量优化+程序性能优化+开发效率优化(全网疯传)

第一章、设计思想与代码质量优化 一、六大原则 二、设计模式 三、数据结构 四、算法 第二章、 程序性能优化 一、启动速度与执行效率优化 二、 布局检测与优化 三、 内存优化 四、耗电优化 五、网络传输与数据存储优化 六、APK 大小优化 第三章、 开发效率优化 一、…

双叒叕-一个-Android-MVVM-组件化架构框架?

LifecycleViewModelLiveDataViewBindingAndroid KTXOkHttp:网络请求Retrofit:网络请求MMKV:腾讯基于 mmap 内存映射的 key-value 本地存储组件Glide:快速高效的Android图片加载库ARoute:阿里用于帮助 Android App 进行组件化改造的框架 —— 支持模块间的路由、通信、解耦BaseR…

【学习笔记】CSS

CSS 1、 基础篇 1.1、选择器 1.2、长度单位 1.3、CSS2 常用属性 1.4、盒模型 1.5、浮动 1.6、定位 position2、 CSS3 2.1、新增长度单位 2.2、新增颜色表示 2.3、新增选择器 2.4、新增盒子属性 2.5、新增背景属性 …

STM32单片机BKP备份寄存器和RTC实时时钟详解

文章目录 1. Unix时间戳 2. UTC/GMT 3. 时间戳转换 4. BKP简介 5. BKP基本结构 6. RTC简介 7. RTC框架图 8. RTC基本结构 9. 代码示例 1. Unix时间戳 实时时钟&#xff0c;本质上是一个定时器&#xff0c;专门用来产生年月日时分秒。 Unix 时间戳&#xff08;Unix T…

CausalMMM:基于因果结构学习的营销组合建模

1. 摘要 在线广告中&#xff0c;营销组合建模&#xff08;Marketing Mix Modeling&#xff0c;MMM&#xff09; 被用于预测广告商家的总商品交易量&#xff08;GMV&#xff09;&#xff0c;并帮助决策者调整各种广告渠道的预算分配。传统的基于回归技术的MMM方法在复杂营销场景…

Windows10中端口被占用处理方法

前言 在Windows 10中&#xff0c;查看端口被占用情况的方法主要依赖于命令行工具netstat。以下是详细步骤&#xff0c;以及必要的解释和归纳&#xff1a; 打开命令提示符 方法1&#xff1a;使用快捷键Win R&#xff0c;打开“运行”对话框&#xff0c;输入cmd&#xff0c;然…

大疆炸机后MOV修复方法(DJI Inspire 3)

dji大疆可以说是无人机中的华为&#xff0c;产品线之广性能之高让高傲的美国人侧面&#xff0c;质量和性价比才是王道。另外产品线的细分也是制胜法宝&#xff0c;无论是手持、农用机、特殊无人机还是影视级产品DJI都有涉及&#xff0c;给人的感觉就是在无人机细分方面它已经无…

java泛型学习

没有java泛型会存在的问题 假设我们有一个方法&#xff0c;希望通过传递不同类型的参数&#xff0c;输出不同类型的对象值。正常情况下我们可能会写不同的方法来实现&#xff0c;但是这样会导致类不断增加&#xff0c;并且类方法很相似&#xff0c;不能够复用。进而导致类爆炸…

基于顺序存储的环形队列算法库构建

学习贺利坚老师基于数组的环形队列 数据结构之自建算法库——顺序环形队列_下空队列q中依次入队列数据元素abc-CSDN博客文章浏览阅读5.2k次&#xff0c;点赞6次&#xff0c;收藏6次。本文针对数据结构基础系列网络课程(3)&#xff1a;栈和队列中第9课时环形队列的存储及基本操…

华为---理解OSPF Route-ID(五)

9.5 理解OSPF Route-ID 9.5.1 原理概述 一些动态路由协议要求使用Router-ID作为路由器的身份标示&#xff0c;如果在启动这些路由协议时没有指定Router-ID,则默认使用路由器全局下的路由管理Router-ID。 Router-ID选举规则为&#xff0c;如果通过Router-ID命令配置了Router-…

1.22 LeetCode总结(基本算法)_位运算

进制的概念 进制即进位计数制&#xff0c;是利用固定的数字符号和统一的规则的带进位的计数方法。 任何一种进位计数制都有一个基数&#xff0c;基数为 X 的进位计数制称为 X 进制&#xff0c;表示每一个数位上的数运算时都是逢 X 进一。 504. 七进制数 手法1&#xff1a;当…

APP启动流程解析

简单概括启动微信的流程就是&#xff1a; 1.Launcher通知AMS 要启动微信了&#xff0c;并且告诉AMS要启动的是哪个页面也就是首页是哪个页面 2.AMS收到消息告诉Launcher知道了&#xff0c;并且把要启动的页面记下来 3.Launcher进入Paused状态&#xff0c;告诉AMS&#xff0c…