写在前面
很抱歉,之前说了两天左右一篇的,却拖到现在。这篇博客零零散散耗费的大概十天时间,我一直想和大家分享一个更加完整的博客,同时也提高一下我的编写能力。我看了《C语言程序设计现代方法》中的这一章大概有三遍,其中粗读一遍,精度两遍。有用了一天时间看看比特蛋哥的讲解视频,最终出了这篇博客。我的能力有限,很多时候都是重复观看,也不知道从哪个角度很大家分享。这里面可能会存在一些错误,还请各位原谅。
预处理器
什么是预处理器
我们可能会感觉到很疑惑,什么是预处理器,它和编译器有什么区别?都先不要着急,请听我慢慢道来。预处理器是一个小软件,它在程序编译前处理程序,比如说展开头文件,宏定义中等等,这些就是它的功能。
预处理器的原理
谈到原理,我们就不得不说一下,代码是如何变成一个可执行程序的?由于前面我们已经分享过相关的内容,这里就不浪费大家的时间了。我们在之前的学习种看到了这两种情况 #include <stdio.h> 和 #define ,这两个才是我们今天的主菜。我们会发现,无论是那种情况,我们都会看到==#==,它就是预处理指令的开头。
- #include 是告诉编译器打开一个特定的文件,把它的内容包含到我们的代码中
- #define 是定义一个 宏
预处理指令
多数预处理指令都从属下面三种类型,我们先来看看。后面我都会说到
- 宏定义 #define
- 条件编译 #ifndef 、#if等等
- 文件包含 #include
宏定义
宏定义是一个很简单的东西,我们先来了解一下什么是宏,所谓的宏就是一种文本替换的模式,没有什么神秘的。我们分两个模块说一下宏的知识点。
为何要有宏
这个就是十分简单了,有时候我们会写很多一摸一样的数据,但是修改这些数据时要一个一个寻找,代码要是多了,我们不可能每次都保证准确无误的修改完比,还有这个情况,我们写圆周率,有可能有会敲出呢给3.14156926、3.1415681…等等,这也是常见的,所以我们与需要宏。
简单的宏
说了这么多,我们来正式看看吧,很简单。
#define PI 3.14159f
int main()
{
printf("%f\n", PI);
return 0;
}
注意事项
宏很好使用,但是我们也需要看看他的一些要求,
- 和 define 之间可以存在任意个空格字符
# define PI 3.14159f
- 要是我们定义的宏过于长,定义时需要换行的话必须加上 \
#define PI \
3.14159f
- 我们允许使用一个宏来定义另一个宏,下面是允许的
#define PI 3.14159f
#define TWOPI 2*PI
- 允许只定义宏,不给它替换的结果
#define A // 允许
- 不建议在宏后面加上==;==,预处理器会把分号当成宏对额一部分
#define A 10; // 禁止
int arr[A]; == int arr[10;]; //报错
- 我定义的的宏的名字和大部分程序员一样,都使用了大写,建议大家使用,这样更加醒目
宏的作用
谈起宏的作用,永远逃不过注释和替换的关系,后面我要先谈一谈什么是注释。
替换
宏的替换是很简单的,就是一个简单的文本替换,发生在预处理阶段。这一点我们没有什么可以疑惑的,不过我还是用图片表示一下吧,方便大家理解。
注释
所谓的注释就是我们给代码一个解释,解释变量的命名或者函数的作用等等,这些我们是都知道的,在C语言中存在两种注释的方法
- 行注释 // C++风格
- 块注释 / /
这里我用图片表示一下就可以了,下面也是他们的区别
- 行注释只能注释一行
- 块注释可以注释多行
注释的内容经过预处理后变成了什么?
这个才是我们希望关注的问题,究竟是被删除了?还是有其他的方法?我们是不是可以按照下面的写代码呢?,这都是我们需要解决的问题。我们在Linux环境下演示。
从这里我们就可以看出,所谓的注释,就是把我们解释的内容变成一个空格,这一点是非常重要的。
注释和宏替替换哪个先开始
我们可能会有一些疑惑,既然注释和宏都会发生替换,那么他们哪个是先执行的呢?
这里我们直接给出结论,是先发生去注释 ,后执行宏替换,下面都是举例子来证明这个结论的
- 宏BSC 发生替换,变成 //
- 第一行hello word由 BSC printf(“Hello word\n”); 变成 // printf(“Hello word\n”);
- 发生注释,注释被替换成一个空格
所以这个方法的结论,和我们我们得到结果不一样,那么只能是我们所说的结论"先发生注释替换 后执行宏替换"是正确的,有些人可能还会对这个感到疑惑,我们按照这个结论再来一遍.
- #define BSC // 发生注释,//,后面的注释是空的而已,BSC是一个不带替换结果的宏,这是允许的
- 后面发生宏替换,结果就和我们的一样了
眼尖的我们一眼就可以看出,是先发生了去注释
带参数的宏
带参数的宏就有些麻烦了,我们先来看看例子。
我们对宏的部分有自己的命名
注意事项
带参数的宏也有一些要注意的地方,我们要来看看.
- 标识符和替换列表之间一定要有空格
- 可以没有参数,
- 标识符最好带上括号,我下面重点说
运算符的优先级一直是一个大问题,有时候我们不能确定用户传入什么样的参数,优先级的高低可能会造成巨大为错误,而且编译器还检测不出来
宏定义中的运算符
这里我来简绍一下宏定义中的两个运算符,这两运算符有很大的作用
- # 字符串化运算符
- ## 粘合运算符
# 运算符
#是字符串化运算符,它可以将数据字符串化,在宏中我们使用#1 可以把1转化成字符串 “1”.下面有一个例子
要是我们要求的数据多了,一点一点写可能不是一个好办法,函数也完不成我们想要的任务,但是借助 # 我们是可以的
## 运算符
名如其人,##就是一个可以粘合多个字符的运算符
#define A(n) n##n
int main()
{
printf("%d", A (A(1)) );
return 0;
}
#define N(a,b,c) a##b##c
int main()
{
printf("%d \n", N(1, 1, 1));
printf("%d \n", N(1,,1));
printf("%d \n", N(1,,));
printf("===============\n");
printf("%d \n", N(1));
printf("%d \n", N(1,1));
printf("%d \n", N(1,1,1));
return 0;
}
宏的优缺点
我虽然很推崇宏,但是很少使用宏的特点,一般就是就是定义一个数组的容量.不过大家还是要知道这些优点和缺点,以便我们未来可能会使用到
- 相对于函数,宏的速度可能更快一点,函数需要开辟栈帧
- 宏容易修改数据
- 宏可以出现在代码的任何位置
- 宏不像变量一样有作用域,他的作用范围是在被定义之后
int main()
{
#define A 10
printf("%d", A);
return 0;
}
- 编译后代码可能会变大,每次使用宏,都会发生替换
- 无法使用一个指针指向宏
- 宏没有参数类型检查
- 宏可能会不止一次计算它的参数
是不是很奇怪,我们得到了最大值,但是i的值却不是我们想要的,实际上代码经过预处理后面,编程了下面的代码
i被错误的执行了两次++
int main()
{
int i = 2;
int j = 1;
int max = (i++) > (j) ? (i++) : (j);
printf("max = %d\n", max);
printf("i = %d\n", i);
return 0;
}
预定义宏
C语言也自己定义了一部分宏,这些我们是可以直接使用的
名字 | 说明 |
---|---|
_FILE_ | 打印文件位置 |
_LINE_ | 打印这一行代码所在的行数 |
_DATE_ | 编译的日期 |
_TIME_ | 编译的时间 |
_STDC_ | 如果编译器符合C标准(C89/C99) ,就是 1,否则就是 就是未定义 |
int main()
{
printf("%s\n", __FILE__);
printf("%d\n", __LINE__);
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
//printf("%d\n", __STDC__); // VS2020 支持部分C语言
return 0;
}
C99增加的几个预定义宏
除此之外C99标准下有增加了几个宏,简单简绍一下,了解就可以了。
例题
我们要是知道这些代码会出现什么现象,就可以理解宏了
条件编译
总算来到这里,要是你看累了,可以稍微休息你会,我下面接着分享,关于条件编译我们还要说很多.
什么是条件编译
所谓的条件编译就是我们根据预处理的结果来修改代码,就像我们手持一把手术刀,看到哪里由顽疾,就把它切除一样.
为何要有条件编译
我们通过条件编译的本质是可以完成代码的裁剪工作,这就意味着我们可以通过裁剪代码来使程序可以在不同的环境下运行,也可以使一些功能开放给用户,一些开放给付费用户
- 可以只保留当前最需要的代码逻辑,其他去掉。可以减少生成的代码大小
- 可以写出跨平台的代码,让一个具体的业务,在不同平台编译的时候,可以有同样的表现
指令
我们大该会有两组指令要谈.
- #if 和 #endif
- #ifdef 和 #ifndef
#if 和 #endif
#if 常量表达式1
<代码块1>
#elif 常量表达式2
<代码块2>
#else
<代码块2>
#endif
我们直接来看看它的作用吧,一目了然.
#include <stdio.h>
#define M 10
int main()
{
#if M
printf("M已经被定义\n");
#endif
printf("Hello word");
return 0;
}
作用
**这里和 if else判断语句作用差不多,我们也可以添加#elif, #else等指令,使用的方法和作用都是和 if else一样.**不过它们是在预处理阶段进行的,我先翻译一下.
我们假设一个不存定义的宏
运算符 defined
我们前面说了# 和##运算符,不过那是在宏中使用的,现在这个运算符defined,是在条件编译中作用.我们一起俩看看他的作用.我们上面的指令是有一定的缺陷的,他的本质是替换,而我们下面所所说就可以很好的区别开来
它会判断我们的宏是不是定义了,不管有没有值,也不管值是多少,只要定义了就是1,否则就是0
#ifdef 和 #ifndef
这是另一组条件编译,我们可以认为它是上面的简写,我翻译一下就可以了
- ifdef M 如果定义了宏 M 保留它该保留的代码
- #ifndef 如果没有定义宏 M, 保留它该保留的代码.这个主要用于 防止头文件被重复引用
题外话
- 宏也可以在命令行中定义
- 条件编译支持嵌套,规则和if else一样
其他指令
除了上面的一些,我们还有其他的一些指令,我们来来了解一下,这些指令都是为了预处理服务的,我们平常很少使用
#error
核心作用是可以进行自定义编译报错
int main()
{
#ifndef __CPP
#error 老铁,你用的不是C++的编译器哦
#endif
return 0;
}
#line
本质其实是可以定制化你的文件名称和代码行号,很少使用
int main()
{
printf("%s, %d\n", __FILE__, __LINE__);
#line 60 "hehe.h" //定制化完成
printf("%s, %d\n", __FILE__, __LINE__);
return 0;
}
#pragma
#pragma message()作用:可以用来进行对代码中特定的符号(比如其他宏定义)进行是否存在进行编译时消息提醒
//#define M 10
int main()
{
#ifdef M
#pragma message("M宏已经被定义了")
#endif
return 0;
}
#pragma和#error的区别
- #pragma只是提示,程序仍旧执行
- #error是报错,程序中断
文件包含
这个挺简单的,就是代码在经过预处理后将文件纳入代码中,关于文件我们有时会出现==<>====“”==这两种情况,我们希望能够了解他们的不同
- <> 直接去标准库中寻找头文件 适合标准库
- “” 先去当前目录下寻找,找不到的话再去标准库中寻找 适合自己定义的头文件
防止头文件被重复引用
我们避免多次引用头文件,代码块变大,所以使用条件编译来防止头文件被重复引用
[外链图片转存中…(img-MPN8Z3UZ-1648899254515)]
//#define M 10
int main()
{
#ifdef M
#pragma message("M宏已经被定义了")
#endif
return 0;
}
[外链图片转存中…(img-W2NQ2374-1648899254515)]
#pragma和#error的区别
- #pragma只是提示,程序仍旧执行
- #error是报错,程序中断
文件包含
这个挺简单的,就是代码在经过预处理后将文件纳入代码中,关于文件我们有时会出现==<>====“”==这两种情况,我们希望能够了解他们的不同
- <> 直接去标准库中寻找头文件 适合标准库
- “” 先去当前目录下寻找,找不到的话再去标准库中寻找 适合自己定义的头文件
防止头文件被重复引用
我们避免多次引用头文件,代码块变大,所以使用条件编译来防止头文件被重复引用