C编程Tips
索引:
2.1 执行字符集(execution character set)
2.11 浮点常量(floating-point constants)
2.12 字符常量(character constants)
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)。
除了以下情形,这些空白字符都会被忽略:
- 分割相邻的词法单位;
- 属于字符常量;
- 属于字符串常量;
- 属于#include中的文件名。
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)。
使用数字转义一定要小心,一方面它依赖于特定的字符集,从而导致可移植性问题。此外,不仔细的写法可能会出现不期望的结果。如:
- "\0111"包含两个字符('\011'和'1')而不是一个,因为最多三个八进制数构成一个字符;
- "\090"包含三个字符('\0','9'和'0'),因为9不是合法的八进制数;
- "\xabc"只是一个字符,要表示两个字符,得写成:"\xab" "c"。
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)
实际扩展过程如下:
- plus(plus(a, b), c)
- add(c, plus(a, b))
- ((c) + (plus(a, b)))
- ((c) + (add(b, a)))
- ((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指令:
- #include <...>
- #include "..."
- #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
(待续...)