前言:
预处理也叫预编译,是编译代码时的第一步,经过预处理后生成一个.i文件,如果不明白编译与链接作用的小伙伴可以先看看博主的上一篇博客—— ,不然知识连贯性可能会显得很差哦。
正文目录:
- 预定义符号
- #define定义常量
- #define定义宏
- 带有副作用的宏参数
- 宏替换的规则
- 宏与函数的对比和命名约定
- #和##
- #undef
- 条件编译
- 头文件的包含
- 其他预处理指令......
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定义符号和宏时,需要涉及⼏个步骤。
- 在调⽤宏时,⾸先对参数进⾏检查,看看是否包含任何由#define定义的符号。如果是,它们⾸先被替换。
- 替换⽂本随后被插⼊到程序中原来⽂本的位置。对于宏,参数名被他们的值所替换。
- 最后,再次对结果⽂件进⾏扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
我们给出一个示例,按照宏替换的规则进行替换,如下图所示:
然后我们就将第二个宏定义的内容替换到主函数中的相应宏中,
因此最终主函数的第一行代码被替换为了:int m = ((a) > (b) ? (a) : (b));
6.宏与函数的对比和命名约定
我们先讲二者的命名约定:
容易发现,函数的宏的使⽤语法很相似。所以语言本⾝没法帮我们区分⼆者。
因此我们平时的⼀个习惯是:
把宏名全部⼤写
函数名不要全部⼤写
宏与函数的对比
宏一般用于简单的运算,因为如果简单的运算就使用函数的话会加大我们的计算时间。
因此宏相对于函数:
宏在程序的规模和速度更胜一筹(规模更小,运算更快)
不仅如此,宏的参数与类型无关
而宏较于函数的劣势处:
- 每次使⽤宏的时候,⼀份宏定义的代码将插⼊到程序中。除⾮宏⽐较短,否则可能⼤幅度增加程序的⻓度。
- 宏是无法调试的
- 参数与类型无关,因此不够严谨
- 可能带来运算符优先级问题,容易出错
但是宏却可以做到一些函数永远做不到的事——比如说参数中出现类型。
两者具体差异如下表所示:
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.头文件的包含
头文件的包含一般分为两种形式:
- <······> (如#include <stdio.h>)
- "······" (如#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()
我们还有很多其他的预处理指令,本文自然不可能给大家一 一讲完,大家可以自行去了解哦~
创作不易,如果作者写的还行的话给个免费的三连吧亲😙😙