预处理

#include

文件包含指令,即 #include 指令,使我们比较容易处理一组 #define 指令以及说明等。在源程序文件中,任何形如:

1
#include "文件名"

1
#include <文件名>

的行都被替换成由文件名所指定的文件的内容。如果文件名用引号括起来,那么就在源程序所在位置查找该文件;如果在这个位置没有找到该文件,或者如果文件名用尖括号<>括起来,那么就按实现定义的规则来查找该文件。被包含的文件本身也可包含 #include 指令。

在源文件的开始处一般都要有一些 #include 指令,或包含 #define 语句与 extern 说明,或访问诸如 <stdio.h> 等头文件中库函数的函数原型说明。(严格地说,这些没有必要做成文件。访问头文件的细节依赖于实现。)

对于比较大的程序, #include 指令是把各个说明捆在一起的优选方法。它使所有源文件都被提供以相同的定义与变量说明,从而可避免发生一些特别讨厌的错误。自然地,如果一个被包含的文件的内容做了修改,那么所有依赖于这个被包含文件的源文件都必须重新编译。

#define

变量式宏定义

1
#define 名字 替换文本

它是一种最简单的宏替换——出现各个的名字都将被替换文本替换。 #define 指令中的名字与变量名具有相同的形式,替换文本可以是任意字符串。正常情况下,替换文本是 #define 指令所在行的剩余部分,但也可以把一个比较长的宏定义分成若干行,这时只需在尚待延续的行后加上一个反斜杠 \ 即可。 #define 指令所定义的名字的作用域从其定义点开始到被编译的源文件的结束。在宏定义中也可以使用前面的宏定义。替换只对单词进行,对括在引号中的字符串不起作用。例如,如果 YES 是一个被定义的名字,那么在 printf ( “YES” ) 或 YESMAN 中不能进行替换。

函数式宏定义

例如:

1
2
#define MAX(a, b) ((a)>(b)?(a):(b))
k = MAX(i&0x0f, j&0x0f)

但这种做法往往会有潜在的问题,通常不推荐这么做。

我们想看第二行的表达式展开成什么样,可以用gcc的-E选项或cpp命令:

1
2
3
4
5
6
7
$ cpp main.c
# 1 "main.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "main.c"
k = ((i&0x0f)>(j&0x0f)?(i&0x0f):(j&0x0f))

#undef

可以用 #undef 指令取消对宏名字的定义,这样做通常是为了保证一个调用所调用的是一个实际函数而不是宏:

1
2
3
#undef getchar
int getchar( void ) { ... }

宏替换的规则

下面给出的用法模式可以避免使用宏带来的问题; 如果你要使用宏, 尽可能遵守:

  • 不要在 .h 文件中定义宏;
  • 在马上要使用时才进行 #define, 使用后要立即 #undef;
  • 不要只是对已经存在的宏使用#undef,选择一个不会冲突的名称;
  • 不要试图使用展开后会导致 C++ 构造不稳定的宏, 不然也至少要附上文档说明其行为.

#、##运算符和可变参数

#运算符

在函数式宏定义中,#运算符用于创建字符串,#运算符后面应该跟一个形参(中间可以有空格或Tab),例如:

1
2
#define STR(s) # s
STR(hello world)

用cpp命令预处理之后是"hello␣world",自动用"号把实参括起来成为一个字符串,并且实参中的连续多个空白字符被替换成一个空格。

使用##连接参数

预处理运算符 ##为宏扩展提供了一种连接实际参数的手段。如果替换文本中的参数用 ## 相连,那么参数就被实际参数替换, ## 与前后的空白符被删除,并对替换后的结果重新扫描。例如,下面定义的宏 paste 用于连接两个参数:

1
#define paste( front, back ) front ## back

从而宏调用 paste(name, 1) 的结果是建立单词 name1

使用宏时要非常谨慎,尽量以枚举和常量代替之。

#if

在预处理语句中还有一种条件语句,用于在预处理中进行条件控制。这提供了一种在编译过程中可以根据所求条件的值有选择地包含不同代码的手段。

#if语句中包含一个常量整数表达式(其中不得包含 sizeof、类型强制转换运算符或枚举常量),若该表达式的求值结果不等于 0时,则执行其后的各行,直到遇到 #endif#elif#else 语句为止(预处理语句 #elif 类似于 if 语句的 else if 结构)。在 #if 语句中可以使用一个特殊的表达式 defined (名字):当名字已经定义时,其值为 1;否则,其值为 0。

例如,为了保证 hdr.h 文件的内容只被包含一次,可以像下面这样用条件语句把该文件的内容包围起来:

1
2
#if !defined(HDR) #define HDR /* hdr.h文件的内容*/
#endif

#if#endif 包含的第一行定义了名字 HDR,其后的各行将会发现该名字已有定义并跳到 #endif。还可以用类似的样式来避免多次重复包含同一文件。如果连续使用这种,那么每一个头文件中都可以包含它所依赖的其他头文件,而不需要它的用户去处理这种依赖关系。

下面的预处理语句序列用于测试名字 SYSTEM 以确定要包含进哪一个版本的头文件:

1
2
3
4
5
6
7
8
9
10
11
#if SYSTEM == SYSV
#define HDR "sysv.h"
#elif SYSTEM == BSD
#define HDR "bsd.h"
#elif SYSTEM == MSDOS
#define HDR "msdos.h"
#else
#define HDR "default.h"
#endif
# include HDR

当需要测试一个名字是否已经定义时,可以使用两个特殊的预处理语句: #ifdef#ifndef。可以使用 #ifdef 将上面第一个关于 #if 的例子改写如下:

1
2
3
4
#ifdef HDR
#define HDR
/* hdr.h文件的内容 */
#endif

最后通过下面的例子说一下#if后面的表达式:

1
2
#define VERSION 2
#if defined x || y || VERSION < 3

  1. 首先处理defined运算符,defined运算符一般用作表达式中的一部分,如果单独使用,#if defined x相当于#ifdef x,而#if !defined x相当于#ifndef x。在这个例子中,如果x这个宏有定义,则把defined x替换为1,否则替换为0,因此变成#if 0 || y || VERSION < 3
  2. 然后把有定义的宏展开,变成#if 0 || y || 2 < 3
  3. 把没有定义的宏替换成0,变成#if 0 || 0 || 2 < 3,注意,即使前面定义了一个变量名是y,在这一步也还是替换成0,因为#if的表达式必须在编译时求值,其中包含的名字只能是宏定义。
  4. 把得到的表达式0 || 0 || 2 < 3像C表达式一样求值,求值的结果是#if 1,因此条件成立。

头文件

头文件的作用

  1. 通过头文件来调用库功能。在很多场合,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口怎么实现的。编译器会从库中提取相应的代码。
  2. 头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,这一简单的规则能大大减轻程序员调试、改错的负担。

规则

  • 为了防止头文件被重复引用,应当用 ifndef/define/endif 结构产生预处理块。
  • #include <filename.h> 格式来引用标准库的头文件(编译器将从标准库目录开始搜索)。
  • #include "filename.h" 格式来引用非标准库的头文件(编译器将从用户的工作目录开始搜索)。
  • 头文件中只存放“声明”而不存放“定义”。
  • 不提倡使用全局变量,尽量不要在头文件中出现象 extern int value 这类声明。

示例

头文件示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/*
* kernel/sched.c
*
* Kernel scheduler and related syscalls
*
* Copyright (C) 1991-2002 Linus Torvalds
*
* 1996-12-23 Modified by Dave Grothe to fix bugs in semaphores and
* make semaphores SMP safe
* 1998-11-19 Implemented schedule_timeout() and related stuff
* by Andrea Arcangeli
* 2002-01-04 New ultra-scalable O(1) scheduler by Ingo Molnar:
* hybrid priority-list and round-robin design with
* an array-switch method of distributing timeslices
* and per-CPU runqueues. Cleanups and useful suggestions
* by Davide Libenzi, preemptible kernel bits by Robert Love.
* 2003-09-03 Interactivity tuning by Con Kolivas.
* 2004-04-02 Scheduler domains code by Nick Piggin
*/
#ifndef GRAPHICS_H /* 防止 graphics.h 被重复引用 */
#define GRAPHICS_H
#include <math.h>
/* 引用标准库的头文件 */
...
#include “myheader.h”
/* 引用非标准库的头文件 */
...
void Function1(...); /* 全局函数声明 */
...
#endif

定义文件示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/*
* kernel/sched.c
*
* Kernel scheduler and related syscalls
*
* Copyright (C) 1991-2002 Linus Torvalds
*
* 1996-12-23 Modified by Dave Grothe to fix bugs in semaphores and
* make semaphores SMP safe
* 1998-11-19 Implemented schedule_timeout() and related stuff
* by Andrea Arcangeli
* 2002-01-04 New ultra-scalable O(1) scheduler by Ingo Molnar:
* hybrid priority-list and round-robin design with
* an array-switch method of distributing timeslices
* and per-CPU runqueues. Cleanups and useful suggestions
* by Davide Libenzi, preemptible kernel bits by Robert Love.
* 2003-09-03 Interactivity tuning by Con Kolivas.
* 2004-04-02 Scheduler domains code by Nick Piggin
*/
#include “graphics.h” /* 引用头文件 */
...
/* 全局函数的实现体 */
void Function1(...)
{
...
}
/* 类成员函数的实现体 */
void Box::Draw(...)
{
...
}

Comments