Duangw

C编程Tips

索引:

1 背景知识

    1.1 UNIX和C的起源

    1.2 C的特点

2 词法(Lexical Elements)

    2.1 执行字符集(execution character set)

    2.2 特殊符号

    2.3 空白字符(whitespace)

    2.4 行结束符(end-of-line)

    2.5 字符编码(character encoding)

    2.6 注释(comments)

    2.7 词法分析的贪心原则

    2.8 标识符(identifiers)的限制

    2.9 关键字(keywordrs)

    2.10 整型常量(integer constants)

    2.11 浮点常量(floating-point constants)

    2.12 字符常量(character constants)

    2.13 字符串常量(string constants)

    2.14 转义字符(escape characters)

3 预处理(Preprocessor)

    3.1 定义

    3.2 参数宏

    3.3 宏扩展

    3.4 预定义宏

    3.5 #undef

    3.6 #参数字符串化

    3.7 ##符号合并

    3.8 #include文件包含

    3.9 条件编译

    3.10 #line指定行号

    3.11 #error出错

 

 

1 背景知识

1.1 UNIX和C的起源

1969年,Multics项目失败后,贝尔实验室的Ken Thompson使用汇编语言在PDP-7系统上编写了一个简易的操作系统,1970年,Brain Kernighan给它取名为UNIX。

使用汇编语言很繁琐,所以,Ken Thompson尝试开发新的语言,他在BCPL语言的基础上进行了简化,创建了无类型的B语言,但B语言从来没有真正成功应用过。

1970年随着开发平台转移到PDP-11,无类型语言就不合时宜了,效率也是个问题。于是,Ken Thompson不得不在PDP-11上重新用汇编实现了UNIX。

与此同时,Dennis Ritchie在ALGOL60、CPL、BCPL、B等语言的基础上创建了强类型的编译型语言:C语言。

1973年,C语言实现的UNIX面世。

之后,UNIX得到了广泛的应用,C语言也随之茁壮成长。C的高效率与移植性,反过来也帮助UNIX获得了巨大的成功。

 

1.2 C的特点

C wears well as one's experience with it grows.(Brain W. Kernighan, Dennis M. Ritchie, The C Programming Language)

C语言编程是一门技艺,需要多年历练才能达到较为完善的境界。一个头脑敏捷的人很快就能学会C语言中基础的东西。但要品味出C语言的细微之处,并通过大量编写各种不同程序成为C语言专家,则耗时甚巨。

要提高C语言的水平,需要站在巨人的肩膀上不断学习,善于总结,虚心聆听专家的教诲。同时,自省也是一条重要的途径,在认识错误的过程中获得进步。

 

 

2 词法(Lexical Elements)

2.1 执行字符集(execution character set)

编写C代码所用的字符集(source character set)和生成的执行程序所用的字符集(execution character set:执行字符集)不一定是相同的,比如交叉编译(cross-compile)。此时,编译器在计算包含字符的常量表达式时,必须使用目标机器上的字符编码,而不是当前机器的字符编码。

虽然出现的场合很少,但这点需要记住。后面还会用到执行字符集这个概念。

 

2.2 特殊符号

2.2.1 三元组

有些国家和地区的字符集不足以囊括ASCII字符,故标准C引入三元组(trigraph)来表示如下的字符:

三元组 代表字符
??( [
??) ]
??< {
??> }
??/ \
??! |
??' ^
??- ~
??= #

三元组字符的转化在词法分析(lexical analysis)和识别字符(串)常量中的转义字符(escape characters)之前进行。

注意:只有上述9个三元组被识别,其他类似的字符组合不会被转化。

如果一个字符序列刚好与三元组相同,要避免识别为三元组,要使用\转义其中的至少一个字符。如"What?\?!",等于What??!。

要表示一个\,由于\本身是转义字符,需要两个\来表示,即"??/??/"。

2.2.2 替代符号

类似的,有些字符集中的操作符(operator)和标点(punctuator)也有替代形式:

符号 代表符号
<% {
%> }
<: [
:> ]
%: #
%:%: ##

与三元组不同在于,这些符号在字符(串)常量中不会被识别。

例如下面的有点怪异的helloworld程序:

#include <stdio.h>
int main(void)
<%
        char str<:20:> = "Hello, World!";
        printf("%s\n", str);
%>

等价于:

#include <stdio.h>
int main(void)
{
        char str[20] = "Hello, World!";
        printf("%s\n", str);
}

实际应用中很少会碰到此类特殊符号,除非万不得已,一般不应该刻意使用它们。

 

2.3 空白字符(whitespace)

在C源程序里,空白字符包括:空格(space)、行结束符(end-of-line)、竖向制表符(vertical tab)、换页符(form feed)、横向制表符(horizontal tab)和C注释(comment)。

除了以下情形,这些空白字符都会被忽略:

 

2.4 行结束符(end-of-line)

行结束符(end-of-line)用来标记C源程序中一行源代码的结束,跟在该结束符后的第一个字符被认作下一行的第一个字符。

可以通过反斜杆(\)将多个源代码行合并成一个逻辑行,合并时反斜杆和之后的行结束符被删除。合并工作发生在预处理(preprocessing)和词法分析(lexical analysis)之前,三元组处理(trigraph processing)和多字节字符序列到源程序字符集转化之后。

例如:

	if (a == b) x = 1; el\
se x = 2;

等价于:

	if (a == b) x = 1; else x = 2;

关于一行的长度,各种实现存在不同的限制。C89要求一个逻辑行至少保证有509个字符,C99则达到4095个字符。

 

2.5 字符编码(character encoding)

不同计算机上的字符编码可能是不同的。一个常见的C程序错误,就是对使用的字符编码做出假设。如通过计算:

'Z' - 'A' + 1

获得大写字母的数目,这在ASCII中是对的,但在EBCDIC中则是错的!

 

2.6 注释(comments)

C有两种注释方法,第一种是传统的方式,即从/*开始,到*/的第一次出现结束。

C99开始引入第二种方式,即从//开始,到本行末尾(不包括行结束符)。这种新的注释方式可能会导致旧代码出现兼容问题,不是很建议使用。

在字符串和字符常量以及其他注释里的注释不被识别。如下面的代码中不含注释:

	printf("%d //squared// is %d\n", i, i * i);

注释在预处理(preprocessing)之前就删除了,所以在注释里的预处理命令不会被识别。

注释中间的行结束符也不会中断预处理命令,如前面所言,注释被当作空白(whitespace)处理。标准C规定,编译时整个注释被替换为一个空格(space)。下面两条指令是等价的:

#define ten (2 * 5)

#define ten /* ten
                      one greater than nine
                   */ (2 * 5)

有些非标准的实现允许使用嵌套的注释,即/*...*/之间还可以有/*和*/,只要配对就行,这种不标准的做法不应该使用。程序员出现误用的情形,多半是为了注释掉一块代码,而没有注意到代码中间存在注释。合理的做法是使用预处理命令而不是注释。如:

#if 0
int page_queue_congested(struct page *page)
{
        ... ...
        /* It pins the swap_info_struct */
        ... ...
#endif

 

2.7 词法分析的贪心原则

编译器在词法分析时,总是从坐到右,尽可能的获取词法单位,即使最终可能导致错误的程序。如(--有两个减号):

	b--x;

词法分析的结果为"b","--","x",这是一个无效的语句。虽然分解为"b","-","-","x"能得到合法的结果,但这不是编译器的行为。

这种处理策略称为“贪心法”。

需要注意的是,除了字符常量和字符串常量,符号的中间不能有空白字符。如==是一个符号,而= =则是两个符号。

好的编程风格之一,就是合理的使用空白分隔符号,不仅是为了代码的可读性,还可以避免潜在的错误。如:

y = x/*p   /* p指向除数 */;

本意是x除以p指向的值,结果赋值给y。然而,/*被编译器认为是注释的开始,直到*/出现为止,于是结果等于:

y = x;

合理的使用空白或括号,就可以避免这种问题:

y = x / *p;
或
y = x/(*p);

 

2.8 标识符(identifiers)的限制

标识符的命名不能与关键字相同,不能与标准库中的名字相同,此外,标准C将所有以一个下划线(underscore)开头,后面跟一个大写字母或者另一个下划线,之后为其他字符的标识符保留,以备将来扩展之用。程序员在命名自己的标识符时,要注意这些限制。

C89要求一个标识符的最小有效长度为31个字符,C99提高到63个。

外部标识符,即定义为extern的标识符,则限制要严得多。C89只保证6个字符,而且不区分大小写;C99提高到31个字符,区分大小写。

可以使用预处理的方法来解决外部限制带来编写代码时的不便,如:

#define error_handler eh73
extern void error_handler();
...
error_handler("nil pointer error);

外部标识符eh73的长度仅为4,但在编写代码时,我们可以使用可读性更好的名字:error_handler。

 

2.9 关键字(keywordrs)

标准C的关键字列表如下:

auto _Bool break case char
_Complex const continue default restrict
do double else enum extern
float for goto if _Imaginary
inline int long register return
short signed sizeof static struct
switch typedef union unsigned void
volatile while

其中_Bool、_Complex、_Imaginary、inline和restrict是C99新增加的。C99还引入了一个预定义的标识符__func__,表示当前的函数名,多用于调试。

除了以上标准的关键字,好多C实现都扩展了asm、fortran,编程时注意不要定义类似的标识符。

 

2.10 整型常量(integer constants)

整型常量的表现形式有十进制(decimal)、八进制(octal)和十六进制(hexadecimal)。十六进制以0x或0X开头,八进制以0开头,其余为十进制形式。

整型常量可以接后缀,字母l或L表示long型,字母u或U表示无符号(unsigned)整数,C99增加了long long类型,相应的,ll或LL表示long long型。u/U和l/L、u/U和ll/LL可以同时出现,顺序无所谓。

注:由于小写字母l和数字1很容易混淆,所以建议用大写的L。

整型常量的值总是为正数(没有溢出时),即使常量前面有负号(-),也作为一元操作符对待,而不作为常量的一部分。

整型常量的具体类型依赖于诸多因素,而且在Tradtional C、C89、C99中的处理方式都不同。具体规则如下表所示:

常量 Traditional C C89 C99
dd...d int int int
long long long
unsigned long long long
0dd...d
0Xdd...d
unsigned int int
long unsigned unsigned
long long
unsigned long unsigned long
long long
unsigned long long
dd...dU
0dd...dU
0Xdd...dU
not applicable unsigned unsigned int
unsigned long unsigned long
unsigned long long
dd...dL long long long
unsigned long long long
0dd...dL
0Xdd...dL
long long long
unsigned long unsigned long
long long
unsigned long long
dd...dUL
0dd...dUL
0Xdd...dUL
not applicable unsigned long unsigned long
unsigned long long
dd...dLL not applicable not applicable long long
0dd...dLL
0Xdd...dLL
not applicable not applicable long long
unsigned long long
dd...dULL
0dd...dULL
0Xdd...dULL
not applicable not applicable unsigned long long

同组中以从上到下的顺序选择第一个不会溢出的类型。

如果常量的值超过了组内最大类型可表示的值,则结果是未定义(undefined)的。C99允许实现在保持符号性的前提下,可以选择一个扩展的类型(如果存在的话)。

需要注意0开头的是八进制而不是十进制数的情况。有时为了对齐格式,可能无意间将十进制数写成了八进制数,如:

struct {
	int part_number;
	char *description;
} parttab[] = {
	046, "left-handed widget",
	047, "right-handed widget",
	... ...
	125, "framins"
};

 

2.11 浮点常量(floating-point constants)

浮点常量除了用小数点表示,也可以有指数部分,以e或E开头。

标准C允许添加后缀来指定浮点类型,f或F表示float类型,l或L表示long double,不指定后缀则为double型。

浮点常量的值总是为正数(没有溢出时),即使常量前面有负号(-),也作为一元操作符对待,而不作为常量的一部分。

如果浮点常量的值太大或太小,以致于无法表示,则结果是未定义(undefined)的。

C99允许用十六进制的形式表示浮点常量,此时用p/P代替e/E(e/E是十六进制的字符之一),同时指数部分的底数为2,不再是10。

 

2.12 字符常量(character constants)

字符常量由单引号(')包含的一个或多个字符构成。标准C允许使用前缀L来表示宽字符常量。

常量中可以使用反斜杆(\)实现转义。如果常量中要包含单引号(')和反斜杆(\),则需要转义。

没有前缀L的字符常量的类型是int,具体的值是执行字符集(execution character set)中相应字符的数字编码,由char类型整提升为int类型后的结果。例如,如果char为8bit的有符号类型,则常量'\377'经过符号扩展后得到的int值为-1。

带前缀L的字符常量类型为wchar_t。

当出现下面几种情况时,字符常量的值依赖于实现(implementation-defined):

使用包含多个字符的字符常量是不可移植的(依赖于实现),有的实现不允许这么做,有些实现则将其转化为对应字节数的整型常量。但即使这样,由于各个系统的字节序(byte ordering)的不同,获得的整型常量的值也是不同的。

例如,常量'ABCD'(以ASCII字符集为例)可能为0x41424344(left-to-right packing),也可能为0x44434241(right-to-left packing)。

所以,不建议在程序中使用这种字符常量。

 

2.13 字符串常量(string constants)

字符串常量由双引号(")包含的0个或多个字符构成。标准C允许使用前缀L来表示宽字符串常量。

字符串常量中可以使用反斜杆(\)实现转义。如果字符串常量中要包含双引号(")和反斜杆(\),则需要转义。

存储含有N个字符的字符串,所需的空间是N+1个字符,末尾用来存放null字符('\0')。所以,sizeof("abcdef")的结果为7,而不是6。

存储字符串常量的空间可能是只读的(read-only),不应该在程序中尝试修改字符串常量的内容。所以,有些函数如mktemp,不能传递给它一个字符串常量。

标准C允许实现给两个有相同内容的字符串常量分配同一个存储空间,所以不能假设所有的字符串常量都会有不同的地址。下面的程序用于检测实现是否共享相同内容的字符串常量:

        char *str1 = "abcd";
        char *str2 = "abcd";

        if (str1 == str2)
                printf("Strings are shared.\n");

当字符串太长,无法写在同一行代码时,除了使用\来续行外,标准C也支持将相邻的字符串常量合并为一个字符串常量的做法。如:

        char *str = "This long sring is permissible "
                        "in Standard C.";

C99支持普通字符串常量和宽字符串常量的连接,结果为宽字符串常量。C89则不支持这种做法。

 

2.14 转义字符(escape characters)

转义字符有两种形式:符号形式和数字形式。

符号形式的列表如下:

\a alert(e.g., bell) 响铃
\b backspace 回退
\f form feed 换页
\n newline 换行
\r carriage return 回车
\t horizontal tab 横向制表符
\v vertical tab 纵向制表符
\\ backslash 反斜杆
\' single quote 单引号
\" double quote 双引号
\? question mark 问号

注:?主要用于三元组。双引号在字符常量中不需要使用反斜杆,单引号在字符串常量中不需要反斜杆。

数字形式的有八进制和十六进制。反斜杆后面直接跟的数字是八进制数,最多可以有三个数字。

十六进制是标准C引入的,在反斜杆后跟x,然后是十六进制数字,数量不限。

虽然如此,但标准C规定,数字形式的普通字符不能超过unsigned char的范围,数字形式的宽字符不能超过wchar_t的范围。

如在ASCII中,字符'a'的八进制形式为'\141',十六进制为'\x61'。

如果数字的值在执行字符集(execution character set)中没有对应的字符,则结果依赖于实现(implementation-defined)。

除了以上三种转义形式,在反斜杆后出现其他形式的转义,其结果是未定义的(undefined)。

使用数字转义一定要小心,一方面它依赖于特定的字符集,从而导致可移植性问题。此外,不仔细的写法可能会出现不期望的结果。如:

 

 

3 预处理(Preprocessor)

3.1 定义

预处理指令以#开头,标准C允许#的前后可以有空白字符,但旧的编译器可能不允许这么做。如果一行中只有#一个非空白字符,标准C当作空白行处理。

在标准C中,预处理行在宏扩展(macro expansion)之前识别。如果宏的扩展结果是预处理指令,则这条指令将不会被预处理器发现。如:

#define GETIO #include <stdio.h>

GETIO

int main(void)
{
        printf("hello, world!\n");
}

这可不等于经典的“hello, world”程序。经过预处理后,"#include <stdio.h>"仍旧在C源程序中,传递给编译器,从而编译时出错!

在前面词法部分提到过,预处理指令可以用\续行。注释中间的行结束符也不会中断预处理指令。

宏在注释、字符常量、字符串常量和#include中的文件名里不会被识别。

所有预处理命令列表如下:

#define 定义宏
#undef 取消宏定义
#include 包含头文件
#if 条件编译
#ifdef
#ifndef
#elif
#else
#endif
#line 显式设定宏__LINE__和__FILE__
defined 宏(参数)定义返回1,否则返回0
#operator 参数字符串化
##operator 符号合并
#pragma 设定依赖于实现的编译指示
#error 产生一个编译错误

 

3.2 参数宏

对于带参数的宏,定义时左括号(parenthesis)必须紧接着宏名,之间不能有空格,否则,从左括号开始的部分都被识别为宏的定义。当运行时,宏名和左括号之间可以有空白。

宏调用时,实参可以包含多层括号(parentheses),括号内可以有逗号(commas)。大括号(braces)和中括号(brackets)也可以出现在实参里,但他们里面不能包含逗号。如定义:

#define insert(stmt) stmt

如下调用是合法的:

insert( { a = 1; b = 1; } )

但这个就不行:

insert( { a = 1, b = 1; } )

必须写成这样才行(有点诡异):

insert( {  (a = 1, b = 1);  } )

编写带参数的宏要特别注意。记住宏扩展仅仅是进行文本式的替换(textual substitution),它不是真正的函数。如:

#define SQUARE(x) x * x

本意是求平方,但调用:

SQUARE(z + 1)

的扩展结果为:

z + 1 * z + 1

一个安全的做法是在宏体内使用括号括住每个参数。如果整个宏的形式是一个表达式,那么宏也用括号括住。如上例:

#define SQUARE(x) ((x) * (x))

如果没有用括号括住整个宏,那么下面的调用就可能会出问题:

(short)SQUARE(z+1)

此外,要注意宏参数可能存在的副作用,宏不是函数。如调用:

a = 3;
b = SQUARE(a++);

导致a++了两次,结果为5,而b的结果则依赖于实现。

对于多语句的情况,一般用do-while结构比较合适。如:

#define swap(x, y) {unsigned long temp = x; x = y; y = temp;}

实际调用时,很容易在末尾加个分号(semicolon),导致出错:

if ( x > y) swap(x, y);
else x = y;

改用do-while实现就好了:

#define swap(x, y) \
	do { unsigned long temp = x; x = y; y = temp; } while (0)

C99允许在参数中使用...实现变参宏。所有额外的参数,包含其中的逗号,用标识符__VA_ARGS__代替。示例:

#define my_printf(...)	fprintf(stderr, __VA_ARGS__)

调用:

my_printf("x = %d\n", x);

的扩展结果为:

fprintf(stderr, "x = %d\n", x);

另一个例子:

#define make_em_a_string(...)   #__VA_ARGS__

调用:

printf("%s\n", make_em_a_string(a, b, c, d));

的扩展结果为:

printf("%s\n", "a, b, c, d");

 

3.3 宏扩展

宏的扩展是一个多次进行的过程。当一个宏扩展之后,会返回到扩展结果的开头,继续再做一次扩展,以便识别出别的宏并扩展它,直到没有宏为止。

扩展在宏调用的时候进行,在#define定义的时候不进行,如前所述。

如定义:

#define plus(x, y) add(y, x)
#define add(x, y) ((x) + (y))

调用:

plus(plus(a , b), c)

实际扩展过程如下:

  1. plus(plus(a, b), c)
  2. add(c, plus(a, b))
  3. ((c) + (plus(a, b)))
  4. ((c) + (add(b, a)))
  5. ((c) + (((b) + (a))))

标准C不对递归形式(直接或间接)的宏进行重复扩展,否则扩展将无限进行下去,直到系统出错(有些旧的编译器会这样)。

递归形式的宏一般用于覆盖旧的定义。如:

#define char unsigned char

 

3.4 预定义宏

标准C预定义了一些宏,宏名以双下划线开始,以双下划线结束。这些宏不能用#undef取消定义。

列表如下:

__LINE__ 当前行在源文件中的行号(十进制表示)。
__FILE__ 当前的源文件名。
__DATE__ 编译时的日历日期(calendar date),由asctime()产生。
__TIME__ 编译时的时间,由asctime()产生。
__STDC__ 当且仅当编译器符合ISO标准时(标准C),值为1。
__STDC_VERSION__ C95的值为199409L,C99的值为199901L,其他情况的值为未定义。
__STDC_HOSTED__ (C99新增)如果编译器是hosted实现,值为1;如果是freestanding实现,则为0。
__STDC_IEC_559__ (C99新增)如果浮点实现遵从IEC 60559,值为1;其他情况的值为未定义。
__STDC_IEC_559_COMPLEX__ (C99新增)如果复数实现遵从IEC 60559,值为1;其他情况的值为未定义。
__STDC_ISO_10646__ (C99新增)wchar_t遵从ISO 10646标准的年月,用long整型常量yyyymmL表示。其他情况的值为未定义。

__LINE__和__FILE__在调试程序时很有用。如:

if (a != b)
    printf("Ierror: line %d, file %s\n", __LINE__, __FILE__);

__DATE__和__TIME__用于记录编译的时间,在整个编译期间,这两个值保持不变。

__STC__和__STDC_VERSION__的配合使用,可以让我们编写兼容各种C标准的代码。如下是一个典型的模板:

#ifdef __STDC__
        /* Some version of Standard C */
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L
        /* C99 */
#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199409L
        /* C89 and Amendent 1 */
#else
        /* C89 but not Amendent 1 */
#endif
#else
        /* Not Standard C */
#endif

 

3.5 #undef

使用#undef来取消一个宏的定义。

取消一个本来就没有定义的宏,不算错误。

取消之后,可以使用#define给宏一个新的定义。

#undef中不进行宏扩展。

标准C允许使用#define重复定义同一个宏,但要求宏的定义是一样的,包括使用的符号和空白的位置,当然具体的空白字符可以不同。如:

#define NULL 0
#define FUNC(x) x+4
#define NULL /* null pointer */ 0
#define FUNC(x) x + 4
#define FUNC(y) y + 4

NULL的重定义是合法的,而FUNC的两个重定义都不合法。

在实践中,程序员不应该依赖这种重复定义,一个定义应该只出现在一个地方。

 

3.6 #参数字符串化

标准C使用#符号将宏参数字符串化(stringization)。

#后面必须是一个宏参数名,当宏扩展时,#和后面的宏参数名替换成实际参数内容构成的字符串常量。

转化时,实参中的引号和转义前会添加\以保留它的原义。如:

#define TEST(a, b) printf( #a " < " #b " = %d\n", (a) < (b) )

调用TEST(0, 0xFFFF)和TEST('\n', 10)扩展为:

printf( "0" " < " "0xFFFF" " = %d\n", (0) < (0xFFFF) );
printf( "'\\n'" " < " "10" " = %d\n", ('\n') < (10) );

执行结果:

0 < 0xFFFF = 1
'\n' < 10 = 0

有些非标准的编译器的处理方式与标准C有很大不同,有的会替换字符常量和字符串常量里的宏参数,有的不会添加\转义,等等。所以,只在标准C中使用这个特性。

 

3.7 ##符号合并

标准C使用##将两边的符号合并成单个符号。所以,##不能出现在开头或结尾,必须是被符号包围。如果合并生成的符号非法,则结果是未定义的。如:

#define TEMP(a) temp ## a

调用TEMP(1) = TEMP(2 + k) + 3的扩展结果为:

temp1 = temp2 + k + 3

 

3.8 #include文件包含

标准C支持三种形式的#include指令:

  1. #include <...>
  2. #include "..."
  3. #inlcude preprocessor-tokens

<>和""之间的内容通常是一个依赖于实现的文件名,文件名中可以有三元组(trigraph)和续行符。使用<>还是""依赖于具体实现对文件存放位置的定义。通常<>表示文件存放于实现的一些标准位置,而""表示在查找标准位置之前,先在一些本地位置查找,如当前目录。

第三种形式的preprocessor-tokens执行正常的宏扩展,扩展后的结果必须符合前两种形式。如:

#if some_thing==this_thing
#define includefile "thisname.h"
#else
#define includefile <thisname.h>
#endif
...
#include includefile

一些非标准的实现对此的行为有很大不同,为了兼容,更好的方法是:

#if some_thing==this_thing
#include "thisname.h"
#else
#include <thisname.h>
#endif

include的文件里面还可以包含#include指令,能嵌套的最大层数依赖于实现,标准C要求至少能嵌套8层(C99提高到15层)。

嵌套存在一个路径问题。如/near/first.c中有:

#include "/far/second.h"

而/far/second.h包含:

#include "third.h"

这里的third.h应该在/near目录还是/far目录?不同实现可能有不同的答案。

一个通常的做法是由程序员指定若干头文件搜索路径(如cc的-Idir选项);同时在源代码里只包含文件名,而不包含路径。

 

3.9 条件编译

通过#if、#elif、#ifdef、#ifndef、#else、#endif等预编译指令选择进行下一步编译的代码。基本形式如下:

#if constant-expression-1
	group-of-lines-1
#elif constant-expression-2
	group-of-lines-2
...
#elif constant-expression-n
	group-of-lines-n
#else
	last-group-of-lines
#endif

其中,#elif和#else是可选的。constant-expression-?经过宏替换后,结果必须是一个算术常量,非0表示真,0表示假。

这些预编译指令可以嵌套,但必须配对。

任意情况下,最多有一组代码会进行下一步的编译,其余都被丢弃。丢弃部分不会被预处理器处理,所以其中的宏替换代码、预处理指令等都不会执行。

如果constant-expression-?中含有未定义的宏,则将其当作整型常量0进行计算。

#ifdef(#ifndef)用于测试一个预处理宏是否被定义(没有定义),如果定义(没有定义)则为真,否则为假。

注意:#ifdef(#ifndef)关心的是宏定义与否,而与宏的具体值无关。一个常见的错误就是把"#if name"与"#ifdef name"混淆。

#ifdef的另一个表达方式是使用defined操作符,形式如下:

defined name
或
defined(name)

#ifdef name与#if defined(name)等同。

#if defined(name)的形式在表达式比较复杂时尤为有用。如:

#if defined(VAX) && !defined(UNIX) && defined(DEBUG)

 

3.10 #line指定行号

程序员可以通过#line预处理命令告诉编译器下一行源代码的行号和文件名(可选)。

两种形式:

#line n
或
#line n "filename"

指令影响预定义宏__LINE__和__FILE__(可选)。

一些自动生成源程序的工具使用该指令来生成调试信息,以便能定位到最初由程序员编写的源程序,而不是之后生成的代码。

 

3.11 #error出错

#error指令产生一个编译时错误信息。示例:

#if (SIZE % 256) != 0
#error "SIZE must be a multiple of 256!"
#endif

 

(待续...)