c:预处理指令(#include、#define、#if等)

环境:

  • window11
  • x86_64-8.1.0-release-win32-seh-rt_v6-rev0.7z (gcc8.1.0)

环境准备参考:《c/c++: window下安装mingw-w64》

一、从一个test.c文件的编译说起

首先,我们要知道一个 test.c 文件是如何一步步编译成 test.exe的。
总体来说:

  • 第一步:预处理器先处理c文件中的预处理指令,如: #include#if...等,得到一个没有预处理指令的代码文件(txt格式) test.i
  • 第二步:编译器进行词法/语法分析、优化、编译上一步生成的test.i,得到汇编文件(txt格式)test.s
  • 第三步:汇编器将上一步得到的test.s翻译成二进制格式文件test.o
  • 第四步:链接器将上一步得到的test.o和引用的资源做链接生成test.exe

如下面的示例:

file:test.c

#include <stdio.h>

int main()
{	
	printf("ok");
	return 0;
}

编译如下:
在这里插入图片描述
我们注意到:预处理器是在编译器开始之前工作的,预处理器的工作内容包含:

摘自:《C程序设计(第四版)学习辅导》
在预处理阶段,预处理器把程序中的注释全部删除;
对预处理指令进行处理,如:把#include指令指定的头文件(如:stdio.h)的内容复制到#include指令处;
#define指令,进行指定的字符替换(如:将程序中的符号常量用指定的字符串代替),同时删除预处理指令。

当预处理器处理完后,生成的test.i将不再包含预处理指令了。

二、预处理指令有哪些

在c语言中主要有以下三种预处理指令:

  • 文件包含:#include
  • 宏定义:#define
  • 条件编译:#if...

下面,我们一一讲解:

注意:在后面的test.i文件中我们可能看到#pragma,但它不是预处理指令,#pragma是用来指导编译器行为的。。

三、预处理之文件包含

#include:简单来讲,它就是将指定的文件拷贝到这个指令的地方,并删除这个#include指令。

打开,上面我们生成的test.i文件:
在这里插入图片描述
这里,因为牵扯到系统库,有很多级联的东西,我们可以改下test.c的代码:
在这里插入图片描述
然后进行编译:
在这里插入图片描述
然后,我们再来观察生成的 test.i
在这里插入图片描述

这下我们能一目了然了吧:

  • #include 做文件内容的拷贝和替换,同时删除所有注释。
  • 头文件可以嵌套引用,替换时做遍历,直到把所有引用的都找到;

另外,C语言不允许头文件之间循环引用,如果你在c.h中再引用a.h,那么 gcc test.c -E -o test.i将会死循环:
在这里插入图片描述

另外,我们注意到:test.c中使用了printf()函数,但是我们并没有#include <stdio.h> 也没有报错,这就证明 预处理器只是做了源代码文件的整理和替换,并没有涉及到编译。

还有几项问题:

  • <stdio.h>"stdio.h" 有设么区别?标准库的路径在哪里,我们能不能指定头文件的寻找目录?

    前者表示直接从标准库里找这个头文件,后者表示先在c文件同目录下寻找,找不到再去系统目录下寻找。
    我们从上面第一次编译输出的test.i里能清晰的看到gcc寻找标准库的目录。
    一般我们约定,使用C语言的标准库就是<>这种形式,而其他第三方库或自己写的都用""这种形式。
    .
    如果,我们的头文件和C文件不在一块,可以添加gcc参数,如下:
    在这里插入图片描述
    如果我们用的是vs,可以在工程中配置:
    在这里插入图片描述

四、预处理之宏定义

4.1 简单宏定义

直接看示例:
在这里插入图片描述
我们可以看到,预处理器处理完后,代码中不再有 PI这个字符串,它已经被替换成 3.1415926了,就连 #define PI 3.1415926这行也没有了。
这就是最简单的宏定义了,它的本质和#include一样,还是字符串替换。

不过,预处理器也并不是无脑的替换,比如,当PI出现在字符串位置时就不会被替换:
在这里插入图片描述
另外,宏定义是可以嵌套使用的,如下:
在这里插入图片描述
注意:虽然可以嵌套使用,但我们不要死循环了(就像 #include 一样)!!!

4.2 带参数的宏定义

宏定义也可以带参数的,就像定义函数一样。。。直接看示例吧:
在这里插入图片描述
不过,有几点我们需要注意下:

  • 带参数的宏定义虽然可以实现函数的功能,但它本质还是字符串替换,使用时尤其小心;
  • 带参数的宏定义要求宏名和(之间不能有空格,否则,,,看示例:

    在这里插入图片描述

  • 带参数的宏最好将参数用括号包裹起来,否则,,,看示例:
    在这里插入图片描述

4.3 取消宏定义

我们可以使用#undef 取消宏定义,看如下示例:
在这里插入图片描述

五、预处理之条件编译

5.1 语法规则

一般情况下,C文件中的所有行都会参与编译,但有时希望程序中的一部分只在满足一定条件时才能参与编译。

对应的语法为:

#ifdef 标识符
	程序段1
#elif 标识符
	程序段2
#else
	程序段3
#endif

还有

#if 标识符
	程序段1
#elif 标识符
	程序段2
#else
	程序段3
#endif

直接看示例:
在这里插入图片描述
上面是我模拟不同平台的代码,实际上,有个经典的例子:

在window和linux下调用sleep函数让程序睡眠是不一样的,如果我们代码想同时兼容linux和window的话,我们可以像下面这样写:

在这里插入图片描述
我们注意到,#if#ifdef有点像啊,但它们是不同的,#if 后面可以跟常量表达式,而 #ifdef后面只能跟宏名:
在这里插入图片描述
在这里插入图片描述
另外,补充下,还有 #ifndef ,这个表示某个宏不被定义时,如stdio.h中的使用:
在这里插入图片描述

5.2 从命令行参数控制条件编译

我们最常用的场景应该是像 __WIN32__linux__ 一样,代码本身不用定义宏,但可以根据环境传入的值进行选择性编译。

那么,我们如何从环境传入宏定义呢?我们可以使用-D定义宏,使用-U取消宏定义,如:
-DMY_MACRO=123-DMY_MACRO-UMY_MACRO

看下面示例:
在这里插入图片描述

六、关于预处理的一些思考

6.1 “篡改”源代码

预处理的存在让我们有机会在编译之前去修整我们的代码(本质是字符串替换),但它实际上也算是"篡改"我们的源代码了。

正因为如此,我们可以在上面test.i中看到会有专门标识各个原文件行号的地方,并且预处理指令删除的地方仍然保留空行,这就是为了在实际编译报错的时候能准确定位到test.c中的位置,而非是test.i中的。

在这里插入图片描述

6.1 头文件的作用?

经过,#include 指令的讲解,我们知道,头文件的内容会被拷贝到源文件中,那么,既然如此,我们手写一个mystdio.h代替stdio.h会不会时一样的效果呢(内容同 stdio.h)?或者说,我们直接在 test.c的上方声明 prinf函数是不是就可以不用 #include <stdio.h> 了??

我们直接来看下面示例:
在这里插入图片描述
看了这个例子,是不是对 头文件#include 的认识又多了些。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jackletter

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值