C预处理器


C预处理器是C语言的一个重要特性,在编译程序之前,先由预处理器检查程序。但其实预处理器并不能理解C,它一般只是接受一些文本并将其转换成其他文本。常见的C预处理器指令有:#define、#include、#ifdef、#else、#endif、#ifndef、#if、#elif、#line、#error、#pragma。本文一一介绍。

1. C预处理器指令的一些通用特性

预处理器指令从#开始,到其后第一个换行符\n为止。也就是说,指令的长度仅限于一行代码。但是,我们经常会看到我们可以使用反斜线\将指令扩展到多个物理行,由多个物理行组成一个逻辑行。就像下面这样:

#define LONG_SENTENCE   "This is a very very long sentence, and it will\ 
      be split into several lines."    /* 反斜线将上面的定义延续到这一行 */

但是这个并不是C预处理器的特性,而是C编译器的特性:在预处理开始前,编译器会查找反斜线和换行符的组合,并将其删掉。所以,其实在预处理的时候,使用反斜线和换行符分开的多个物理行其实已经变为一行了。但是,需要注意的是,编译器只会删除反斜线和换行符的组合,所以我们使用反斜线扩展的时候,后面一定马上要接换行符,不能有其他符号(比如空格或者换行符),否则就会报错。而且扩展行也要左对齐,如果有其他符号(比如空格或者制表符),那么这些符号也将作为字符串的一部分。 我们可以在预处理指令行中使用标准C的注释,这些注释也会在预处理开始前被编译器解析替换为一个空格。而且有的编译器会将多个空白字符序列(不包括换行符)替换为一个空格。 ANSI标准还允许#前有空格或者制表符,并且#与指令的其余部分之间也允许有空格。但旧版本的C一般要求指令在最左列开始,并且#和指令的其余部分之间不能有空格。本文以ANSI C以及C99作为标准进行介绍。所以,在最新的标准里面,下面的写法都是可以接受的:

# include <stdio.h>
    #include<unistd.h>
#include    <stdlib.h>

下面是一个完整的例子:

# include <stdio.h>
    #include<unistd.h>
#include    <stdlib.h>

#define /* a test example */ LONG_SENTENCE   "This is a very very long sentence, and it will\ 
      be split into several lines."     

int main()
{
    printf("Hello, C Preprocessor !\n");
    printf("%s\n", LONG_SENTENCE);
    return 0;
}

编译运行结果如下:

allan@ubuntu:temp$ ./a.out 
Hello, C Preprocessor !
This is a very very long sentence, and it will     be split into several lines.

 2. #define指令

2.1 一般用法介绍

#include#define应该是我们在C中使用最多的两个预处理指令,前者用法简单单一,后者功能比较强大且很灵活,但却是一把双刃剑。每个#define行由三部分组成:

#define  PI    3.1415926

第一部分为指令#define本身。 第二部分为一个缩略语,我们称这些缩略语为宏(macro)。宏一般分为两类:_类对象宏宏(object-like macro)_和_类函数宏(function-like macro)_。其实就是根据功能来分的,前者一般都是定义一些常量,而后者主要是定义一些类似函数功能的宏。宏的名字中不允许有空格,且必须遵循C变量的命名规则:只能使用数字、字母和下划线,第一个字符不能是数字。 第三部分称为替换列表(replacement list)或者主体(body)。预处理器在程序中发现宏实例以后,就会用实体代替该宏。我们称这个替换过程为宏展开(macro expansion)

  1. 宏定义中可以嵌套宏。注,有的编译器并不支持该特性。比如下面例子中的MACRO2将被替换为3+1+2:

    #define MACRO1   1+2
    #define MACRO2   3+MACRO1
  2. 双引号中的宏不会被替换。一般而言,预处理器发现程序中的宏以后,会用他的等价替换文本替换掉宏,如果该字符串中还包括宏,则继续替换该宏,但双引号中的宏例外。

  3. 预处理器把宏的主体当做语言符号(token)类型字符串,而不是字符型类型字符串。这两个概念是不同的。对于语言符号类型的字符串,一般用空白字符来分割语言符号,且多个空白字符会被系统用一个空格代替(前面已经介绍)。看下面例子:

    #define FOUR 2*2
    #define SIX  2  *  3
    #define SIX  2       *       3

如果是按照语言符号类型的字符串处理,那么2*2就是一个语言符号;而2 * 3会被识别为3个语言符号:2、、3;而2    *     3同样会被识别为3个语言符号:2、、3。所以后两者会被认为是相同的定义。但如果是按照字符型字符串处理,那么,2*2是一个字符串,2 * 3是一个字符串,2      *     3又是另外一个字符串,3个都不一样。也就是说:用字符型字符串的观点看,空格也是主体的一部分;而用语言符号字符串的观点看,空格只是分隔主体中语言符号的符号。

  1. 重定义常量。在ANSI C标准中,只允许新定义与旧定义完全相同,当然,不管什么情况下,总可以使用#undef指令重新定义宏。

2.2 类函数宏介绍

2.2.1 类函数宏使用注意点

类函数宏即外形和作用都与函数相似的宏,宏的参数也用圆括号括起来。比如:

#define SQUARE(X)   ((X)*(X))

对于这种用法,不再赘述,只是有两点需要特别注意:

  • 使用时必须要使用足够多的括号来保证宏展开后以正确的顺序进行结合和运算。

  • 不要在宏中使用增量或减量运算符。比如++和--。

下面介绍几个特殊的运算符。

2.2.2 #运算符

该运算符的功能是把语言符号转化为一个字符串。下面结合一个例子来看:

#include <stdio.h>

#define PSQR(x)  printf("The square of " #x " is %d.\n", ((x)*(x)))

int main()
{
    int y = 5;

    PSQR(y);
    PSQR(2 + 4);

    return 0;
}

编译后运行:

allan@ubuntu:temp$ ./a.out 
The square of y is 25.
The square of 2 + 4 is 36.

可以看到,在宏定义主体中#x连接了两个字符串:"The square of "和" is %d.\n"。整个转换分两步:第一步,宏展开,使用字符串"y"和字符串"2 + 4"替换#x。第二部,ANSI C的字符串连接功能将替换后的字符串与其他的字符串连接成一个字符串。该过程称为字符串化(stringizing)

2.2.3 ##运算符

##运算符的功能是把两个语言符号组合成单个语言符号。比如定义如下宏:

#define XNAME(n) x ## n

那么XNAME(4)这样的宏定义会被展开为x4 。 下面是一个综合###的例子:

#include <stdio.h>

#define XNAME(n) x ## n
#define PRINT_XN(n)  printf("x" #n " = %d\n", x ## n)

int main()
{
    int XNAME(1) = 10;    // 变为 int x1 = 10;
    int XNAME(2) = 30;    // 变为 int x2 = 30;

    PRINT_XN(1);     // 变为 printf("x1 = %d\n", x1);
    PRINT_XN(2);     // 变为 printf("x2 = %d\n", x2);

    return 0;
}

编译运行:

allan@ubuntu:temp$ ./a.out 
x1 = 10
x2 = 30

2.2.4 可变宏:...__VA_ARGS__

可变宏的实现思想是:宏定义参数列表的最后一个参数为省略号(三个英文句号)。然后将预定义宏__VA_ARGS__放在宏主体的替换部分,表明省略号代表什么。看下面例子:

#define PR(...)  printf(__VA__ARGS__)
  • PR("Hello");将被展开为:printf("Hello");

  • PR("weight = %d, shiping = $%.2f\n", wt, sp);将被展开为:printf("weight = %d, shiping = $%.2fn", wt, sp);

 3 其他预处理指令

其他预处理器指令相对简单且用的比较少,一并介绍。

3.1 #include指令

#include预处理器指令应该使我们使用最多的指令了,但它的使用相对简单,这里只说明两个方面的知识点:

  1. #include的两种结构:

    • #include <exp.h>:文件名放在尖括号里面。尖括号告诉预处理器在一个或多个标准系统目录中寻找文件。

    • #include "exp.h":文件名放在双引号里面。双引号告诉预处理器在当前目录(或文件名中指定的其他目录)中寻找文件,然后在标准位置寻找文件。

  2.  一般放在头文件中定义的量

    • 明显常量

    • 宏函数

    • 函数声明

    • 结构模板定义

    • 类型定义

3.2 #undef指令

#undef指令取消之前#define的一个宏定义。取消后,便可重新设置宏。但是C标准预定义的宏不能被取消,比如__DATE____FILE__等,后面介绍。

3.3 条件编译宏

3.3.1 #ifdef#ifndef)  #else #endif

看下面一个例子:

#ifdef A
 #include "a.h"
#else
 #include "b.h"
#endif

这几个宏使用时有以下几个注意点:

  • #ifdef#endif必须成对出现,但#else可选。

  • 如果预处理器已经定义过标示符A(必须是预处理器定义的),那么执行#ifdef后面的语句,否则执行#else后面的语句。

  • 这几个宏非常类似于C中的if ... else,主要差异在于预处理器不能识别标记代码块的花括号({ }),因此#ifdef和#endif必须成对出现来标识一个代码块。

  • #ifndef的用法与#ifdef相同,但含义相反。这里不再赘述。

很多时候,我们可以用预处理运算符defined,该运算符跟一个参数。如果该参数已经用#define定义过,那么defined返回1;否则返回0.他可以和其他宏指令配合使用。

3.3.2 #if#elif

#if指令更像常规的C中的if;#if后跟常量整数表达式。如果表达式为非零值,则表达式为真,在该表达式中可以使用C的关系运算符和逻辑运算符。

#if SYS == 1
 #include "ibm.h"
#elif SYS == 2
 #include "vax.h"
#else
 #include "mac.h"
#endif

3.3.3 #line#error

介绍这两个指令之前,需要先介绍一下标准C的预定义宏(这些宏不能使用#undef取消):

  • __DATE__:进行预处理的日期("Mmm dd yyyy"形式的字符串文字)。

  • __FILE__:当前源代码文件名的字符串。

  • __LINE__:当前源代码文件中行号的整数常量。

  • __STDC__:它设置为1时,表示该实现遵循C标准。

  • __STDC_HOSTED__:为本机环境设置为1,否则设为0。

  • __STDC__VERSION__:为C99时设置为199901L。

  • __TIME__:源文件编译时间,格式为"hh:mm:ss"。

看下面一个例子:

#include <stdio.h>

void why_me();

int main()
{
    printf("The file is %s.n", __FILE__);
    printf("The date is %s.n", __DATE__);
    printf("The time is %s.n", __TIME__);
    printf("The version is %ld.n", __STDC_VERSION__);
    printf("This is line %d.n", __LINE__);
    printf("This function is %sn", __func__);

    why_me();

    return 0;
}

void why_me()
{
    printf("This is line %d.n", __LINE__);
    printf("This function is %sn", __func__);
}

编译运行:

allan@ubuntu:temp$ gcc -std=c99 predef.c 
allan@ubuntu:temp$ ./a.out 
The file is predef.c.
The date is Jan 15 2015.
The time is 07:40:06.
The version is 199901.
This is line 11.
This function is main
This is line 21.
This function is why_me

下面来介绍#line#error

  • #line指令用于重置由__LINE____FILE__宏报告出的行号和文件名,比如:

    #line  1000            // 把当前行号重置为1000
    #line  10  "cool.c"    // 把行号重置为10,文件名重置为cool.c</pre>
  • #error指令使预处理器发出一条错误消息,该消息包含指令中的文本。可能的话,编译过程应该中断。比如:

    #if  __STDC_VERSION__ != 199901L
    #error Not C99
    #endif

其实还有一个#warning,用法同#error,但级别不同。

3.3.4 #pragma

这个指令用的比较少,但却比较复杂,主要作用是在源代码里面改变一些编译设置。具体以后专门介绍。 至此,C预处理就介绍完了,其实平时比较常用的还是#define和一些条件编译指令。不得不说,使用#define去定义一些函数宏的确比较高效,但也很容易出问题。从C99开始,C也支持内联函数了,使用方法和C++相同,不妨可以试试。


添加新评论

选择表情 captcha

友情提醒:不填或错填验证码会引起页面刷新,导致已填的评论内容丢失。

|