第 24 讲:预处理指令
编译器指令,或者叫做预处理指令(Preprocessor),指的是在程序用编译器翻译为机器可以识别和执行的指令前,所做的指令操作。它可以控制程序在不同执行状态下执行不同的结果。为区分其他编程语句,我们使用井号 #
进行开头。
#include
指令
这个指令用于导入头文件。什么是头文件呢?在使用特定的编程函数,诸如 printf
、scanf
等时,由于每一个编程函数都有不同的功能,所以为了方便管理,它们各自被放在不同的头文件下,在使用时,需要根据它本身属于哪个头文件,先要导入,才能使用。否则编译器找不到这个函数。
它有两种导入方式:引号导入和尖括号导入。如果写成这样:
则表示它是一个系统自带的编程函数。它会先找系统自带的函数库,发现文件存在就可以打开。如果不存在,再找到你建立这个C语言程序代码下,文件夹下是否有这个头文件。
反之,如果要先找当前文件夹下的头文件的话,我们使用引号导入。
注意,编译器指令都是不用分号结尾的。
#define
指令
真·常量定义
它又被称为宏定义(Macro Defining),是将代码里所有代码的写法替换成宏定义过的写法。比如,在代码里写很多计算圆的公式,它会大量使用到圆周率的值(约 3.14159),可如果代码内直接写值的话,如果需要对代码作改动,就需要挨个改掉它们,就相对麻烦,这个时候我们可以使用 #define
指令替换它们,然后改值时,只需要改 #define
一处的值即可。
另外,一般而言,像是这样的指令写的东西(3.14159)是一个常数,所以我们一般将常数全部大写变量名,即 PI 的形式,不过这只是一般而言,也可以不大写。
骚操作
不过,宏定义下的写法是直接替换的,也就是说,你这么写完全可以:
只是,一部分 IDE,例如 DevCpp 的编译器不能执行中文字符的宏定义替换罢了。不过,#define
定义语句中间的空格是严格规定的,只能在 #define
后和定义名称后有空格,这样来控制替换,否则系统会无法判别。
宏定义有时候也会背锅
另外,#define
有一处需要注意的替换。它甚至可以替换函数,但函数的替换方式可能有一些恶心:
它们是不同的。
看懂了吗?上面的 a
和 b
都有括号,所以下面替换后,也带括号;上面没有括号,下面也没有括号。于是根据优先级的不同,计算结果分别是 4 * 6 = 24 和 2 + 6 + 3 = 11。
#undef
指令
甚至还可以使用 #undef 定义名
的方式来撤销结束定义。比如
#define
符号指令
我们也可以在定义符号的时候不为其赋值:
这样定义的符号在本文件里的任意位置都可以用,或你把它写进头文件里,然后对其使用了 #include
引入了这个头文件时可用。
这种用法需要配合下面要说到的 #if
指令才能发挥作用。
#if
、#else
、#elif
和 #endif
指令
这四个指令单纯就是为了和 C++ 语言配合使用得比较多。在 C++ 里,有些常量的定义数值是和 C 不一致的,好在它提供了 #define
的宏指令指定了语言类别。
在 C++ 里,系统定义了 __cplusplus
符号。如果我们查看 __cplusplus
符号是否存在,就可以判断当前使用的文件和语言是 C 语言还是 C++,进而去区分程序的执行代码。
例如上面的 5 行指令,先查询当前代码环境是否是 C++。如果是 C++,则会执行第 2 行的 #define
定义语句,让 NULL
常量定义为 0(这是 C++ 里的定义方式。C++ 的空是用 0 表示而不是 (void *)0
表示的;否则,没有查到 __cplusplus
,说明是 C 语言环境,那么 NULL
的定义方式就是(void *)0
了。
另外,#if defined
可以简写为 #ifdef
指令:
另外,你对条件取反,也是被允许的:
#ifndef
NULL
由于头文件 *.h
是不区分 C 还是 C++ 语言的,所以保证程序允许正常,这些指令和赋值都是不可少的。#elif
指令类似于 else if
条件判断,这里就不讲了,因为用的机会很少。
#pragma
杂注指令
最后来说一下杂注(Pragma)。杂注是预处理指令里最复杂的一种,这是因为它的名字“杂注”就表示了所有乱七八糟的指令,不好取名或用得比较少的时候,就直接用 #pragma
指令来搞了,所以它的分支也非常多。我们这里来看一些常用的。至于不常用的,如果你碰到了,直接查询网上给出的文档即可。
#pragma warning
指令
#pragma warning
指令用于忽略、恢复和只提示一次警告信息。假如你的代码里出现了警告错误,例如下面代码
这是最基础的使用 scanf
函数的方法了。不过编译器会提出警告 C6031,提示你忽略了 scanf
的返回值。scanf
的返回值表示正确输入到变量里的变量总个数。如果你输入了一个合适的数字,a
正常赋值,那么这个返回值就是 1。
它提示这个信息,提示你最好不要忽略返回值的使用,而是这样用:
但是,显然我们没有必要这么去处理,因为我们输入一个整数数值是预期的,所以我们可以忽略烦人的警告信息,用法是这样的:
这样的话,你就看不到后续使用的所有有关 scanf
上输入信息的返回值未使用的报错了。
如果你把代码改为
表示从此处开始往下的代码,重新又开始对 C6031 忽略返回值的警告报错。
这样则是仅报告一次这样的警告信息,其它的都忽略。
#pragma region
和 #pragma endregion
指令
忘了这个了吗?这个就是最开始介绍的,用于分割语义的指令。这两个指令是配合使用的,用于分割代码段,这一段代码里的所有执行内容表示某个含义。另外,写了这样的指令后,IDE 就可以识别这一块代码是我们指定好的代码块,甚至可以允许我们手动折叠和展开它们。
#pragma once
指令
最后一个指令仅用于头文件里,表示这个头文件只会被编译一次。这是因为,很多时候,相同的指令会被分散放到不同的头文件里;而反之也可能存在多个相同名称的头文件。这样的话,如果我们使用 #pragma once
指令可以保证这个文件整体只会被编译一次,防止多次反复编译出现隐藏的 bug 和错误。
当然,如果执行逻辑都不同的同名头文件最好还是不要用一样的头文件名,防止这些错误的出现。