C语言:预处理器
Posted 一个自由人
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言:预处理器相关的知识,希望对你有一定的参考价值。
预处理器
1.0 什么是预处理器
我们编写的C程序在编译之前通常需要经过预处理器处理变成处理后的C程序,然后再进入编译器。
预处理器通常会对我们所写的程序中的预处理指令进行替换,我们最为熟悉的就是 #define
这个预处理指令,举个例子:
#include <stdio.h>
#define MAX 10
int main()
{
printf("The max is %d.\n", MAX);
return 0;
}
将会被处理为:
/* stdio.h引入的行 */
/* 空行 */
int main()
{
printf("The max is %d.\n", 10);
return 0;
}
预处理器将我们的预处理指令执行或者在程序之内替换之后删除了,但是行依然保留;而且程序内调用宏定义的地方都替换为宏所代表的东西
2.0 预处理指令
预处理指令主要有3种类型:
-
「宏定义」:也就是 define
与undef
对宏指令的定义与删除 -
「文件包含」:熟悉的 #include
指令将一个文件包含到程序中 -
「条件编译」:诸如 #if
、#endif
、#ifdef
与#elif
等等根据条件来判断是否将某段代码包含到程序中
此外还有不是很常用的 #error
、 #line
和 #pragma
这篇文章都会讲到
对于预处理器指令,有如下特征:
-
「它们都以 # 开头」,要求就是#之前有空白字符即可认为它是一个预处理指令的开始 -
「预处理指令内部可以添加任意数量的空格与制表符」,例如 # define N 5
是合法的 -
「预处理指令总是在第一个换行符的地方结束」,如果要延续,在本行末尾使用 \
即可 -
「预处理指令可以出现在程序的任何地方」,作用范围则是从出现出到整个程序结束
3.0 宏定义
下面就开始讨论三种预处理指令中的第一种——宏定义
3.1 简单的宏
最常用的:「#define 标识符 替换列表
」
我们的替换列表可以替换包括标识符、关键字、数值常量、字符常量、字符串字面量、操作符和排列在内的内容,在经预处理器处理时,将会把标识符替换为替换列表中的内容。
对于定义宏表示的方法有几个显著的优点:
-
使程序更易读,不至于出现很多莫名其妙的“magic number” -
程序的修改与可移植性会有所提高 -
对类型重命名,就像 #define BOOL int
这样 -
简便的控制条件编译(后面会提到)
3.2 带参数的宏
「#define 标识符(x1, x2, ..., xn) 替换列表
」
「需要注意的是」:在标识符与左括号之间不可以有空白字符,如果有将会被认为是一个简单的宏,后面的整体将会被认作替换列表;
其实相对于普通的宏的不同之处就是带参数的宏可以在调用时给替换列表内部的参数赋值,从而更加灵活,因此也就常用于定义一些类函数的宏。用它来代替真正的函数有这几种优点:
-
程序的速度会稍微快一些 -
宏相较于函数更加通用,因为宏没有指定数值的类型
同时也存在一些缺点:
-
宏没有类型检查,这是优点的同时也是一种缺点 -
指针不可以指向宏 -
「宏可能会不止一次地计算它的参数」,例如:
#define MAX(x,y) ((x) > (y) ? (x) : (y))
在调用时:
n = MAX(i++, j); ---> n = ((i++) > (j)) ? (i++) : (j);
可见实际上变量 i 自增了两次。这就是一种副作用
3.2.1 当参数为空时(C99)
C99 允许了宏的参数为空的情况。在调用时在某个参数的位置不填任何字符即可,但是需要的逗号不能少。
正常的空参数情况就是这个参数的位置就被略过了,什么都没有, 例如:
#define MULTI(x,y) ((x) * (y))
调用时:
i = MULTI(,5); ---> i = * (5)
当空参数是#与##运算符(3.4节)的时候:
-
当是#的操作数,则其为空字符串 ""
只有\0
而已。 -
当是##的操作数,结果就是那个空参数的位置会被略过,剩余的其他参数结合形成结果
#define JOIN(x,y,z) x##y##z
int JOIN(a,b,c), JOIN(a,,c), JOIN(,b,c);
结果就是:abc、ac、bc
3.2.2 参数可变的宏(C99)
在C99中加入了特性宏的参数个数是可变的,也就是形如如下形式:
#define TEST(condition, ...) ((condition)? \
printf("Passed the test: %s\n", #condition) : \
printf(__VA_ARGS__))
我们使用省略号 ...
放在参数列表的最后,用来作为调用的时候输入的除了第一个实参的参数(们)的形参,因此调用这个宏至少需要两个参数,但是省略号代替的参数可以为空;而 __VA_ARGS__
是一个专用的标识符,只能出现在具有可变个数参数的宏的替换列表内,代表所有与省略号对应的参数。
在调用的时候:
TEST(power <= max_power,
"Power %d exceeds %d\n", power, max_power);
预处理之后就是:
((power <= max_power) ?
printf("Passed the test: %s\n", "power <= max_power") :
printf("Power %d exceeds %d\n", power, max_power);
也就是当力大小在允许的范围之内的时候,输出“符合条件:实际力 <= 许用力”;在力超出范围的时候,输出力的大小值超过了许用力的大小。
3.3 宏定义中的圆括号
你可能已经观察到了,在我们定义 MAX
宏的时候,我们用到了很多圆括号,这是加上就是为了防止本意被程序曲解,所以有以下「两个原则」:
-
如果宏的 「替换列表中有运算符」,那么整个替换列表需要放在括号中 -
如果 「宏有参数」,每个参数在替换列表中出现的时候都需要放在圆括号中
3.4 #运算符与##运算符
我们在使用宏的时候可以使用这两种运算符(它们是「宏专用的」)
「#运算符」的作用就是将宏的一个参数转化为字符串字面量,仅允许出现在红的替换列表之内,而且对于对象中的 "
和 \
将会自动转化为 \"
与 \\
以适应字符串字面量
例如:
#define PRINT_INT(n) printf(#n " = %d\n", n)
调用时
PRINT_INT(i/j);
相当于:
printf("i/j" " = %d\n", i/j);
中定义的这个宏,就是很巧妙地达到了用途。
「##运算符」的作用是将两个记号粘合在一起形成一个记号,如果其中一个操作数是一个宏参数,那么粘合动作将会在形式参数被实际值替换之后发生。
例如我们可以创建一个可以**计算不同数据类型的数值的最大值的宏:**由于C程序不允许创建多个函数名相同的函数,我们可以使用##运算符对在宏内定义的函数名做一些修改从而达到这个目的:
#define GET_MAX(type) \
type type##_max(type x, type y) \
{ \
return x > y ? x : y; \
}
需要注意的是:#运算符与##运算符都有一个缺点,是一件怪异的事:
如果有宏定义 #define CONTACT(x,y) x##y
,当我们调用的时候如果是 CONTACT(a,CONTACT(b,c))
结果不会是你所想的 abc
而是 aCONTACT(x,y)
这是一件很奇怪的事,你不是说“如果其中一个操作数是一个宏参数,那么粘合动作将会在形式参数被实际值替换之后发生”吗?
实际上,位于##与#运算符之前和之后的宏参数再替换时不被扩展,在使用他们的时候尽量使用简单的宏作为操作数,但是这也有破解的方法:就是再定义一个宏去调用这个宏 #define CONTACT2(x,y) CONTACT(x,y)
然后再去进行 CONTACT2(a,CONTACT2(b,c))
即可,这是由于 CONTACT2
的替换列表不包含##运算符,所有的##运算都是在调用 CONTACT
宏而已。
3.5 创建较长的宏
在创建较长的宏的方面,「逗号运算符」是非常有用的,因为虽然输出的值是一个语句的,但是逗号左右的两条指令都执行了,例如:
#define ECHO(str) (gets(str), puts(str))
还有另一种方法,就是把「需要执行的语句都放在一个 do-while 循环里」,条件设为假保证其只执行一次,相较于逗号运算符,这样做的好处是我们不仅可以用几条命令,还可以添加一些结构:
#define ECHO(s) \
do { \
gets(s); \
puts(s); \
} while(0)
有一点需要注意:
在宏的结尾如果有分号,再在调用的时候就不要再写分号了,最好宏的结尾不要加分号(如上面那个),可以看一个分号导致出错的例子:
#define ECHO(str) {gets(str); puts(str);}
预处理
调用时: ---> 处理后:
if (i > 0) | if (i > 0)
ECHO(s); | {gets(str); puts(str);};
else | else
gets(s); | gets(s);
可以看出来。由于第五行的结尾有两个分号。导致编译器认为这个选择结构已经结束了,所以剩下的 else 没有从属从而报错。
3.6 预定义宏
在C里有很多已经帮你定义好的宏,可以直接调用:
名 字 | 描 述 |
---|---|
|
被编译的文件中的行号 |
__FILE__ |
被编译的文件名 |
__DATE__ |
编译的时间(格式“mm dd yyyy”) |
__TIME__ |
编译的日期(格式“hh : mm : ss”) |
__STDC__ |
如果编译器符合C标准(C89或C99),则值为1 |
「注意」:前后都是两个下划线!
3.6.1 时间:__DATE__
和 __TIME__
这两个宏可以指明编译的时间:
printf("Microsoft Windows(c) 2020 Microsoft software, Inc.\n");
printf("Compiled on %s at %s\n", __DATE__, __TIME__);
会输出:
Microsoft Windows(c) 2020 Microsoft software, Inc.
Compiled on Jun 18 2020 at 15:18:48
可见他们输出的形式都是字符串类型的。
3.6.2 寻找:__FILE__
与 __LINE__
我们通常可以使用这两个宏去寻找在程序中出错的地方,例如在使用除法的时候加上下面的程序就可以检测每次的分母是否为零:
#define CHECK_ZERO(divisor) \
if (divisor == 0) \
printf("*** Attempt yo divide by zero on line %d " \
"of file %s ***", __LINE__, __FILE__)
调用时:
CHECK_ZERO(divisor);
k = i / divisor;
3.6.3 C99 中新增的预定义宏
名 字 | 描 述 |
---|---|
|
如果是托管式实现,值为一;如果是独立式实现,值为0 |
__STDC_VERSION__ |
支持的C标准版本 |
__STDC_IEC_559__ |
如果支持IEC 60559浮点数运算,则值为1 |
__STDC_IEC_559_COMPLEX__ |
如果支持IEC 60559复数运算,则值为1 |
__STDC_ISO_10646__ |
如果 wchar_t 的值与指定年月的ISO 10646标准相匹配,则值为yyyymmL |
对于什么是C的托管式实现与独立式实现,书上有如下描述:
❝C 的实现(mplementation)包括编译器和执行C程序所需要的其他软件。C99将实现分为两种:托管式(hosted)和独立式(freestanding)。托管式实现(hosted implementation)能够接受任何符合C99标准的程序,而独立式实现(freestanding implementation)除了几个最基本的以外,不一定要能够编译使用复数类型或标准头的程序。(·特别是,独立式实现不需要支持
❞<stdio.h>
头)如果编译器是托管式实现__STDC_HOSTED__
宏代表常数1,否则值为0。
❝大多数程序(包括本书中的程序)都需要托管式实现,这些程序需要底层的操作系统来提供输入输出和其他基本服务。C的独立式实现用于不需要操作系统(或只需要很小的操作系统)的程序。例如,编写操作系统内核时需要用到独立式实现(这时不需要传统的输入/输出,因而不需要<stdio.h>)。独立式实现还可用于为嵌入式系统编写软件。
❞
__STDC_VERSION__
宏为我们提供了一种查看编译器所识别出的标准版本的方法。这个宏第一次出现在C89标准的Amendment1中,该文档指明宏的值为长整数常量199409L(代表修订的年月)。如果编译器符合C99标准,其值为199901L。对于标准的每一个后续版本(以及每一次后续修订),宏的值都有所变化,用 %d 转义符来调出它就好。
其他的了解就好
4.0 条件编译
条件编译就是通过预处理器的所执行的测试结果来确定是否包含某个代码片段。
4.1 #if
和 #endif
在我们找bug的过程中,通常会需要一段测试代码检测在某个范围内的数值值看其是否正确,但是用完了之后防止以后再需要用一般会注释起来,但是用的时候一段段去注释有点麻烦,我们可以用这种条件编译的方法:
#define DEBUG 0
#if DEBUG
printf("a = %d\n", a);
printf("b = %d\n", b);
#endif
在需要调试的时候,把 DEBUG
的值调为1,不调试的时候设为0即可
准确地讲,DEBUG
所在的位置是一个常量表达式,和选择结构的判断条件相同。但是请记住:这个「位置上只能是一个宏」,因为这个计算进行在预处理阶段,是不接触程序内的参数的!
需要关注的是,「未定义的宏如果处在这个位置,相当于 0」
4.2 defined
运算符
这也是一个预处理器专用的运算符,他的操作数是标识符,如果是一个已经定义过的宏则返回1,否则返回0
#if defined(HELLO)
... ...
#endif
4.3 #ifdef
和 #ifndef
其实 #ifdef
效果和 #if
加上 defined
效果相同,都是检测宏是否被定义过,有则包含此段代码,无则不包含#ifndef
是一样的只不过它是未定义时包含,已经定义时不包含
#ifdef HELLO #ifndef !HELLO #if defined(HELLO)
... 等价于 ... 等价于 ...
#endif #endif #endif
4.4 #elif
与 #else
和级联的 if - else 结构相似,#elif
就相当于 else if()
,而 #else
就相当于 else
#if 常量表达式
...
#elif 常量表达式1
...
#else
...
#endif
4.5 条件编译的优点
-
**可移植性增强,**请看如下代码
#define WIN32
#if defined(WIN32)
Windows适用程序
#elif defined(LINUX)
Linux适用程序
#elif defined(MACOS)
Mac适用程序
#endif
-
在程序内可以 「检测是否定义了某个宏」,没有可以为它增加定义
#ifndef HELLO
#define HELLO hi
#endif
5.0 其他预处理指令
5.1 #error
指令
使用的格式为:「#error 消息
」
我们用到 #error
命令的时候一定是遇到严重错误的时候,也就是在编译阶段就发现了错误,从而终止编译,并且发送一条错误指令
通常与条件编译指令一起使用,当满足某个条件的时候,触发 #error
5.2 #line
指令
我们通常编写程序的时候,行号都是“1,2,3,... ...”,而使用 #error
指令可以改变接下来的行号:
用法1:「#line n
」
此时「不包括指令本身这一行」,它的下一行会被命名为第n行,n是介于1和32767(C99为2147483647)之间的整数
用法2:「#line n 文件
」
此时「不包括指令本身这一行」,它的下一行会认为是来源于被指定文件的第n行,但实际上编译的代码仍然是本身的
可以认为 #line
指令改变了预定义宏 __LINE__
和 __FILE__
的值
5.3 #pragma
指令
#pragma
指令为要求编译器执行某些特殊操作提供了一种方法。这条指令对非常大的程序或需要使用特定编译器的特殊功能的程序非常有用。
格式为:「#pragma 记号
」
其中,记号是任意记号。#pragma
指令可以很简单(只跟着一个记号),也可以很复杂:
#pragma data(heap_size=>1000,stack_size =>2000)
#pragma
指令中出现的命令集在不同的编译器上是不一样的。你必须通过查阅你所使用的编译器的文档来了解可以使用哪些命令,以及这些命令的功能。顺便提一下,如果 #pragma
指令包含了无法识别的命令,预处理器必须忽略这些 #pragma
指令,不允许给出出错消息
C89中没有标准的编译提示(pragma),它们都是在实现中定义的。C99有3个标准的编译提示,都使用 STDC
作为 #pragma
之后的第一个记号。这些编译提示是 FP_CONTRACT
、CX_LIMITED_RANGE
和 FENV_ACCESS
其实这个我不太懂,请参考这个吧:http://c.biancheng.net/cpp/html/469.html
5.4 _Prama
运算符(C99)
C99引入了与 #prama
一起使用的 _Prama
运算符:
格式:「_Prama (字符串字面量)
」
遇到该表达式时,预处理器通过移除字符串两端的双引号并分别用字符 "
和 \
代替转义序列 \"
和 \\
来实现对字符串字面量(C99标准中的术语)的“去字符串化”。表达式的结果是一系列的记号,这些记号被视为出现在pragma指令中。例如:
_Pragma("data(heap_size => 1000,stack_size => 2000)")
等价于
#pragma data(heap_size=>1000,stack_size =>2000)
_Pragma
运算符使我们摆脱了预处理器的局限性:预处理指令不能产生其他指令。由于 _Pragma
是运算符而不是指令,所以可以出现在宏定义中。这使得我们能够在 #pragma
指令后面进行宏的扩展。
以上是关于C语言:预处理器的主要内容,如果未能解决你的问题,请参考以下文章