预处理详解

在上一节我们学习了编译,链接的相关知识。这节我们来详细学习一下编译中的预处理

文章目录


前言

预处理主要由预处理器完成,预处理器是编译器的一部分,它在编译之前对源代码进行处理。预处理的主要任务包括宏替换、文件包含、条件编译和一些其他的源代码操作。


一、#define 

在预处理中#define扮演者一个重要的角色,它主要在主函数之前就开始设置一些值或者变量表达式。

1.1 #define 定义常量

我们有时候在代码中可能重复使用一个数值,但是我们多次输入相同的值可能会出错(对于一些数值较大,位数较多的数),那么这时候咱们就可以尝试定义一个常量。

其实,#define的作用不仅仅如此,它还可以定义其它的内容,如果一个关键字太长太繁琐,我们就可以将其用#define定义为一个简单的字符。

1.2 #define 定义宏

#define中包含一种机制:允许参数插入到文本中,这个操作叫做宏,也称为宏定义,如下是宏的声明原型

parament-list是一个由逗号隔开的符号表,它最终会出现在stuff中,stuff就是我们想要定义的宏。除此之外,我们还需要注意:(parament-list)中的括号与name之间不能有空隙,否则#deifne会将name后面所有的内容都是将要定义之后的内容

上述代码是我定义的一个宏(求一个数的平方数),我们定义宏的时候不能简单的将想要的内容直接输入上去,由于#define中的宏会在预处理中展开,并将主函数中的替换掉(原封不动地替换,不会加入什么括号),因此如果我们在使用宏的表达式中还有其他的符号,那么可能因为运算符的优先级不同导致其产生的不是我们想要的结果,所以我们在定义宏的时候,一定不要吝啬括号,有时候可能就是因为一个小小的括号导致满盘皆输。

1.3 有副作用参数的宏

我们知道,宏是带有参数的。有时候我们开始定义的宏,在后续代码中可能会使用多次,中间可能会因为一些运算,而导致最初的参数而发生变化,比如下面两个参数的对比

我们用下面的代码进行演示,其产生的副作用:

对于后置自增来说,我们首先使用那个值,使用完之后再进行自增1,在上述代码中,我们进行一个比较大小的程序,由于我们多次使用参数,而导致参数不断发生变化,最后导致我们意想不到的结果。

1.4 宏替换的规则及注意事项

在预处理中,我们所写的宏会被替换掉,这其中也有一些规则需要注意:

1.在调用宏时,我们要先进行检查,是否包含有任何#define定义的符号。如果有,我们则先把它们替换掉;

2.替换文本中随后会被插入程序原有的位置。对于宏和参数名则被它们的值所替换;

3.最后对文件进行扫描,如果仍然存在#define定义的符号,再次进行上述操作。

另外我们还有一些注意事项:
1.我们可以在#define定义的内容与宏中使用#define定义的内容,但是对于宏,不可以进行递归;

2.当预处理搜索器搜索#define定义的常量时,字符串常量的内容是无法被搜索的。

二、宏与函数

2.1 宏与函数的对比

目前为止,我们介绍的宏是不是与我们之前所学的函数有一点相似呢?都是在主函数之前定义一下,然后再在主函数中调用。但是这两者只是形似神不似。

和函数相比宏的优势: 

1.在调用一个简单的代码时候,宏更加快捷。因为调用一个函数的时候,我们在使用调用的函数之前,我们要先进行函数栈帧的创建与销毁,这个所花的时间可能比实际执行函数的时间更多。因此,宏在程序的规模与速度比函数更胜一筹

2.在定义函数的时候,我们在写参数的时候,我们必须明确参数的数据类型,如果是不同的数据类型,我们可能就需要不同的函数。但是我们定义宏的时候,可以无需考虑参数的数据类型,因为宏的参数是类型无关的

和函数相比宏的劣势

1.每次我们在调用宏的时候,我们会在预处理中将宏展开,所以如果定义的宏过长,那么整个代码会变得冗长复杂。

2.宏是无法进行调试的。

3.宏由于没有参数的类型,所以可能不太严谨。

4.宏在调用的时候要注意相邻的符号,可能会导致代码中的运算符优先级改变。

从上面的讲解,我们可以看出宏与函数之间还是有所差异的,除此之外,有时候函数无法实现的,宏能够轻松完成,如下代码所示:

下图是宏与函数对比图解:

三、预处理中的其他内容

3.1 #与##

3.1.1 #运算符

#是一个将宏的一个参数转化为字符串字面量的运算符,它仅允许出现在带有参数的宏的替换列表中。#运算符就相当于一个”字符面化符号“

这里我们还要知道一个知识点:

在上述代码中#n,在预处理中会变成"n"

3.1.2 ##运算符

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

## 被称 为记号粘合 这样的连接必须产生⼀个合法的标识符。否则其结果就是未定义的。

举个例子,如果我们想要求好几对不同数据类型的大小比较,如果我们使用函数的方法,那么我们就要定义好几个不同的函数,因为一个函数对应着一种数据类型。这时候,我们可以试试宏的方法。

3.2 宏与函数的定义习惯

我们定义一个宏的时候,一般全使用大写字母作为宏的名字,而函数我们一般把第一个字母写成大写作为函数名。这是我们一般默认成俗的。

 3.3 #undef

这个与#define的作用恰好相反,这条指令可以将我们之前定义的一个宏定义移除。

3.4 条件编译

在编译⼀个程序的时候我们如果要将⼀条语句(⼀组语句)编译或者放弃是很方便的。因为我们有条件编译指令。

除此之外,还有一些其他的条件编译指令:

条件编译我认为和注释差不多,不过它能够根据咱们的想法来使用或放弃,是不是使用上限更高了呢?

3.5 头文件的包含

我们在敲代码之前总是#include起步,这条指令就是来包含头文件的。而包含头文件的方法又分为两种:一种使用<>,另外一种使用” “

使用<>, 一般包含的头文件是库函数里面的头文件,而在编译的时候,会直接去标准路径来查找头文件,如果没有找到,则发生报错。

使用” “,一般包含的头文件是自己写的头文件,比如我们写了好几个文件,如果我们想要将一个文件中的代码在另一个文件中使用,那么我们就可以将一个文件名的后缀改为.h,在另一个文件中写一个#include"xxx.h",就ok了。我们先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件⼀样在标准位置查找头文件。

有时候一个新手在书写代码的时候,可能会认为包含头文件的次数不影响,其实并非如此,我们实际写几个,在预处理的时候就会展开几个。故一般我们在创建一个头文件的时候,编译器会自动生成一个代码:

这个代码能够确保头文件仅使用一次。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值