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 宏的时候,我们用到了很多圆括号,这是加上就是为了防止本意被程序曲解,所以有以下「两个原则」:

  1. 如果宏的 「替换列表中有运算符」,那么整个替换列表需要放在括号中
  2. 如果 「宏有参数」,每个参数在替换列表中出现的时候都需要放在圆括号中

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里有很多已经帮你定义好的宏,可以直接调用:

名 字 描 述

__LINE__

被编译的文件中的行号
__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 中新增的预定义宏

名    字 描   述

__STDC_HOSTED__

如果是托管式实现,值为一;如果是独立式实现,值为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_CONTRACTCX_LIMITED_RANGEFENV_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语言:预处理器的主要内容,如果未能解决你的问题,请参考以下文章

C预处理器

gcc 参数

片段着色器是不是处理来自顶点着色器的所有像素?

C语言:预处理器

C语言程序代码的125个建议

是否有用于 Octave 和 Scilab 的 C 类预处理器指令用于互兼容代码?