C 预处理指令

0. Overview

C的预处理指令格式为#name,均以#开头,#和指令名之间不可有空白字符,#前可以有空字符,但为增强可读性,一般应从第一列开始。

#name不能由宏展开得来,name也不能由宏展开得来,如:

// Wrong 1
#define INC #include
INC <stdio.h>
// Wrong 2
#define INC include
#INC <stdio.h>

预处理指令只能占一行,但是在写代码时可以用'\'分隔多行,但处理时仍会将这多行合为一行。有些指令带参数,参数需与指令由空白字符分隔。

预处理指令主要提供下列功能:

  • 引入头文件;
  • 宏展开;
  • 条件编译;
  • line control:#line(感觉一般人用不着);
  • 诊断(diagnostics):可在编译器检查程序,发出errors或warnings。

1. 头文件

#include来包含头文件,该指令的参数形式有两种:

  1. #include <file>
    用于系统头文件。Preprocessor将在a standard list of system directories下搜寻文件file,可以用编译器的-I选项来将目录添加到这个list;
  2. #include "file"
    用于程序自身的头文件。Preprocessor的搜寻顺序如下:
    a. 先在包含该文件的当前目录搜寻文件file
    b. 然后在quote directories中搜寻,可以用编译器的-iquote选项来将目录添加到quote directories中,
    c. 最后再在用于搜寻<file>的目录下搜寻(即 1. 中的顺序)(所以用#include "stdio.h"只要你不覆盖这个头文件的话也不会出现问题,总能找着)。

#include的参数无论是用""还是<>括起来,都如同一个字符串,里面的注释不会被识别,宏也不会展开。但是不同于字符串的是,backslash不再有转义作用,而是一个单纯的字符'\'。

在这一行,文件名参数后面除了注释外不能有任何其他内容。

只包含一次 Once-Only Headers

如果一个头文件被include两次,编译器就会处理两次,因此可能会出错,如重定义等等,标准做法是用所谓的wrapper #ifndef将头文件的内容包起来,如:

/* File foo.  */
#ifndef FILE_FOO_SEEN
#define FILE_FOO_SEEN

the entire file

#endif /* !FILE_FOO_SEEN */

代码片段中的宏FILE_FOO_SEEN叫做controlling macro或者guard macro,在用户程序头文件中,该宏的名字不能_开头,在系统头文件中,该宏的名字需要__(双下划线)开头以免与用户程序头文件冲突。在任意类型的头文件中,该宏的名字应该包含头文件文件名再加上额外的文字以避免与其他头文件冲突。

2. 宏 Macros

宏是赋予名字的一段代码,每次使用时都将名字替换成宏内容。宏分为两种,它们在使用时有很大的不同:

  1. Object-like macros:使用时像用data objects一样,
  2. Function-like macros:使用时像函数调用一样。

2.1 对象形式的宏 Object-like macros

Object-like macro就是一个简单的标志符,表示一个代码片段,在使用时由这个代码片段来替换,用法:

#define NAME macro_body

宏body又叫expansion或replacement list,是一个token序列。

按照惯例,宏的名字一般用大写字母。

#define macro_body也只占一行,并且macro_body后面不能有其他内容(除空白字符或注释外),在写代码时也可以用'\'分隔多行,但预处理时仍会将它们合为一行。

C preprocessor顺序地扫描源程序,因此宏定义只从定义处开始生效。

宏展开是递归进行的,preprocessor将一个宏展开后会接着处理展开后的结果,如果这里面有其他的宏,会继续展开下去。但是如果结果里面再次出现刚刚展开的这个宏的话将不会展开第二次,以免出现无限递归的情况:

#define TABLESIZE BUFSIZE
#define BUFSIZE 1024
TABLESIZE
// -> 先展开为 BUFSIZE
// -> 再展开为 1024

注意虽然宏会展开多次,但是每次的展开过程只是单纯地用body替换name,如上面的例子中在展开TABLESIZE时只是单纯地用BUFSIZE来替换它,接下来preprocessor才检查替换结果是不是另一个宏。

2.2 函数形式的宏 Function-like macros

如其名,这种宏使用起来像函数调用一样。用法:

#define name() body

注意,小括号()必须和宏的名字连在一起,否则会被当成object-like宏来展开,同时,在使用时也必须用name()的形式(此时name()间可以有空格,2.3中同),只用name的话不会被展开。

2.3 宏参数

Function-like宏像函数一样可以接受参数,用法:

#define name(params_list)

其中params_list是参数列表,参数必须是有效的C标志符,由,分隔(参数列表中可以出现空格,但是空格没有实际作用)。

在“调用”函数形式的宏时,将实参列表写在宏name后面的小括号里,由,分隔,函数形式宏的“调用”不限制在一行内,可以写成多行,但是参数数量必须和定义时的数量相匹配。可以实参可以是空,但是数量也必须匹配(直白讲即逗号数量必须一致),如:

#define min(X, Y)  ((X) < (Y) ? (X) : (Y))
  x = min(a, b);          →  x = ((a) < (b) ? (a) : (b));
  y = min(1, 2);          →  y = ((1) < (2) ? (1) : (2));
  z = min(a + 28, *p);    →  z = ((a + 28) < (*p) ? (a + 28) : (*p));
min(, b)        → ((   ) < (b) ? (   ) : (b))
min(a, )        → ((a  ) < ( ) ? (a  ) : ( ))
min(,)          → ((   ) < ( ) ? (   ) : ( ))
min((,),)       → (((,)) < ( ) ? ((,)) : ( ))

min()      error→ macro "min" requires 2 arguments, but only 1 given
min(,,)    error→ macro "min" passed 3 arguments, but takes just 2

在展开时会去除各个实参的leading、trailing whitespace,实参的token序列中的whitespace会减成一个空格。在每个实参中,小括号必须平衡,小括号中的逗号不会结束这个参数(即小括号中的逗号不是实参分隔符),但中括号和大括号不要求平衡,而且它们中的逗号会作为实参分隔符截断这个参数。

宏定义中若参数出现在字符串中,在展开时不会展开成相应实参,如:

#define foo(x) x, "x"
foo(bar)        → bar, "x"

2.4 字符串化 Stringizing

有时可能需要讲宏参数转换成字符串常量,但是在 2.3 的最后提到字符串中的参数不会被实参替换,为了解决这个问题,可以用预处理操作符#来进行转换。当参数有一个前导#时,preprocessor会将其替换为实参,再转换成字符串常量,但是这个过程发生后,被转换成的字符串中如果还有宏则不会继续展开,如果还想继续展开,则需要写成多级宏的形式,如:

#define xstr(s) str(s)
#define str(s) #s
#define foo 4
str (foo)
     → "foo"
xstr (foo)
     → xstr (4)
     → str (4)
     → "4"

2.5 拼接

预处理操作符##用于在宏body中将两个tokens拼在一起,如A ## B将展开为AB,要求展开后必须是一个有效的C标志符,如一个标志符和数字拼接,两个数字间的拼接,一些复合操作符如+=的拼接等等,有些拼接是无效的,如x+

拼接常见的应用场景为宏参数间的拼接,如:

#define COMMAND(NAME)  { #NAME, NAME ## _command }

struct command commands[] =
{
  COMMAND (quit),
  COMMAND (help),
  …
};

2.6 取消宏定义

#undef name用于取消宏定义,name可以是object-like宏的名字,或者是function-like宏的名字(不用加小括号以及参数列表)。

3. 条件编译

3.1 条件编译常用场景

  • 根据机器架构或操作系统的不同使用不同的代码;
  • 将原文件编译成两个不同的程序,其中一个版本可能会用于输出一些data进行debugging等等;
  • 使用#if 0来将排除一段代码,但将其保留在源文件中用作注释。

3.2 条件编译语法

ifdefifndef

#ifdef MACRO

controlled text

#endif /* MACRO */

if

#if expression

controlled text

#endif /* expression */

expression是一个integer类型的C表达式,可以包含:

  • 整形常量;
  • 字符常量;
  • 数学运算表达式和逻辑运算表达式(遵循短路求值);
  • 宏,在计算宏所代表的表达式前将先展开所有的宏;
  • defined预处理指令;
  • 所有不是宏的标志符都视为数字0,函数形式的宏但没有调用实参列表也视为0。

defined

用在#if#elif表达式中,用于测试一个名字是否被定义成了一个宏,defined namedefiend ( name )作用相同:如果name定义为了一个宏,则表达式值为1,否则为0,因此#if defined MACRO等价于#ifdef MACRO

在测试多个宏是否存在时defined比较有用,如:

#if defined (__vax__) || defined (__ns16000__)

else

可以用在#if#ifdef#ifndef中。

elif

elif不需要一个#endif和其匹配。

4. 诊断信息

  • #error导致preprocessor产生一个fatal error,#error所在行的剩余tokens组成错误信息
  • #warning导致preprocessor产生一个warning并继续预处理,#warning所在行的剩余tokens组成错误信息

两者都不对其参数进行宏展开

参考

posted @ 2019-06-23 00:46  westwindrest  阅读(1759)  评论(0编辑  收藏  举报