VectorLu

C 语言预处理器

C 处理器可以是强大的工具,也可以成为不易发现的错误之源。

预处理器

  • 用 gcc -E program.c 就能看到预处理器的输出
  • 预处理器仅知道很少的 C 语言规则
  • 所以很容易出现错误
  • 对于复杂的程序,
  • 检查预处理器的输出是找到这类错误的很好的办法

预处理器指令

大多数预处理指令属于下面 3 种类型之一:

  • 宏定义#define 定义一个宏,#undef 删除一个宏定义。
  • 文件包含#include
  • 条件编译#if, #ifdef, #ifndef, #elif#endif 指令可以让预处理器以测试的条件来确定是否将一段文本段包含到程序中。

剩下 #error, #line, #pragma 较少用到。

注意

  1. 指令都以 # 开头
  2. 指令总是在第一个换行符处结束,除非明确地指明要延续——在行末使用 \。例如,下面的指令定义了一个宏来表示硬盘的容量(按字节算):

    1
    2
    3
    4
    #define DISK_CAPACITY(SIDES * \
    TRACKS_PER_SIDE * \
    SECTORS_PER_TRACK * \
    BYTES_PER_SECTOR)
  3. 指令可以出程序中的任何地方,甚至函数定义的中间。但是我们通常将 #define#include 指令放在文件的开始。

  4. 注释可以与指令放在同一行。

宏定义

不需要使用等号

1
2
3
4
// 样式:
// #define 标识符 替换列表
#define STR_LEN 80

对 C 语言做小的修改

虽然这通常不是个好主意。比如:

1
2
3
4
#define BEGIN {
#define END }
#define LOOP for(; ; )

对类型重命名

1
#define BOOL int

带参数的宏

1
2
3
4
5
6
7
8
// 格式:
// #define 标识符(x1, x2, ..., xn) 替换列表
// x1-xn 是宏的参数
// 在宏的名字和左括号间必须没有空格
#define MAX(x, y) ((x)>(y)?(x):(y))
#define IS_EVEN(n) ((n)%2 == 0)
#define IS_ODD(n) ((n)%2 != 0)

如果后面的程序中有如下语句:

1
2
i = MAX(j+k, m-n);
if (IS_EVEN(i)){i++;}

预处理器会将这些行替换为:

1
2
i = ((j+k)>(m-n)?(j+k):(m-n));
if (((i)%2==0)) {i++;}

使用带参数的宏替代真正的函数有两个优点:

  • 程序可能稍微快些。程序执行时调用函数通常会有些额外开销——存储上下文信息、复制参数的值等,而调用宏则没有这些运行开销。(C99 中的内联函数为我们提供了不使用宏而避免这一开销的办法。)
  • 宏更“通用”。与函数的参数不同,宏的参数没有类型。只要预处理后的程序依然是合法的,宏可以接受任何类型的参数。如上例,可以用 MAX 宏来选出较大的一个。数的类型可以是 int, long, float, double 等。
  • 编译后的代码通常会变大。宏的使用越频繁,效果越明显。
  • 宏参数没有类型检查。预处理器不会检查宏参数的类型,也不回进行类型转换。
  • 无法用指针指向一个宏
  • 宏可能会不止一次地计算它的参数。为了自我保护,最好避免使用带有副作用的参数。函数对它的参数只会计算一次,而宏可能会计算两次甚至更多次。如果参数有副作用:

    1
    2
    n = MAX(i++, j);
    // n = ((i++)>(j)?(i++):(j));

带参数的宏不仅适用于模拟函数调用,还经常用作需要重复书写的代码段模式。例如:

1
#define PRINT_INT(n) printf("%d\n", n)

# 运算符

将宏的一个参数转换为字符串字面量。

1
2
3
4
5
#define PRINT_INT(n) printf(#n " = %d\n", n)
PRINT_INT(i/j);
// printf("i/j = %d\n", i/j);
// i/j = 5

## 运算符

使两个记号(如标识符)“粘合”在一起

1
2
3
4
5
#define MK_ID(n) i##n
int MK_ID(1), MK_ID(2), MK_ID(3);
// 预处理后
// int i1, i2, i3;

一个更实用的例子:

1
2
3
4
5
#define GENERIC_MAX(type) \
type type##_max(type x, type y) \
{ \
return x > y ? x : y; \
}

如果需要一个针对 float 值的 max()。下面是使用 GENERIC_MAX 宏来定义这一函数的方法:

1
2
3
4
GENERIC_MAX(float)
// 预处理将其展开为:
// float float_max(float x, float y) { return x > y ? x : y;}

宏的通用属性

替换列表可以包含对其他宏的调用

1
2
#define PI 3.14159
#define TWO_PI (2*PI)

预处理器会不断重新检查替换列表,直到所有的宏名字都替换掉为止。

只替换完整的记号

预处理器会忽略前在标识符、字符常量、字符串字面量值之中的宏名。

宏不可以被定义两遍

除非新旧定义一样

#undef 取消定义

#undef N 会删除宏 N 当前的定义。(如果 N 没有被定义成一个宏,#undef 指令没有任何作用。)

宏定义中的括号

  1. 如果有运算符,始终将替换列表放在括号中:#define TWO_PI (2*3.14159)
  2. 如果宏有参数,每个参数每次在替换列表中出现时都要放在圆括号中:#define SCALE(x) ((x)*10)

如果没有括号,编译器可能会不按我们期望的方式应用运算符的优先级和结合性规则。

预定义宏

__DATE__ 宏和 __TIME__ 宏指明程序编译的时间。来帮助区分同一程序的不同版本。

1
2
printf("Wacky Windows (c) 2010 Wacky Software, Inc.\n");
printf(Compiled on %s at %s\n", __DATE__, __TIME__);

可以使用 __LINE__ 宏和 __FILE__ 宏来找到错误。考虑被零除的定位问题。当 C 程序因为被零除而导致终止时,通常没有信息指明哪条除法导致错误。下面的宏可以帮助我们查明错误的根源:

1
2
3
4
#define CHECK_ZERO(divisor) \
if (divisor == 0) \
printf("*** Attempt to divide by zero on line %d " \
"of file %s ***\n", __LINE__, __FILE__)

__func__ 标识符

每一个函数都可以访问 __func 标识符,它的行为很像一个存储当前正在执行的函数的名字的字符串变量。其作用相当于在函数体的一开始包含如下声明:

1
static const char __func__[] = "function-name";

其中 function-name 是函数名。这个标识符的存在使得我们可以写出如下的调试宏:

1
2
#define FUNCTION_CALLED() printf("%s called\n", __func__);
#define FUNCTION_RETURNS() printf("%s returns\n", __func__);

对这些宏的调用可以放在函数体中,以跟踪函数的调用:

1
2
3
4
5
6
void f(void)
{
FUNCTION_CALLED();
...
FUNCTION_RETURNS();
}

__func__ 另一个用法:作为参数传递给函数,让函数知道调用它的函数的名字。

条件编译

#if and #endif

1
2
3
4
#define DEBUG 1
#if DEBUG
...
#endif

defined

如果标识符是一个定义过的宏则返回 1,否则返回 0

1
2
3
#if defined DEBUG
...
#endif

和下面这段等价:

1
2
3
#ifdef 标识符
当标识符被定义为宏时需要包含的代码
#endif

#elif and #else

使用条件编译

条件编译用于调试是非常方便的,但是其应用不限于此。

在多台机器或多种操作系统之间可移植的程序

下面的例子中会根据 WIN32, MAC_OS 或 LINUX 是否被定义为宏,而将三组代码之一包含到程序中:

1
2
3
4
5
6
7
#if defined(WIN32)
...
#elif defined(MAC_OS)
...
#elif defined(LINUX)
...
#endif

可以用不同编译器编译的程序

为宏提供默认定义

1
2
3
#ifndef BUFFER_SIZE
#define BUFFER_SIZE 256
#endif

条件屏蔽

1
2
3
#if 0
包含注释的代码行
#endif

其他指令

#error 指令

通常与条件编译指令一起监测正常编译过程中不应该出现的情况,往往预示着程序中出现了严重的错误。

1
2
3
#if INT_MAX < 100000
#error int type is too small
#endif

如果试图在一台以 16 位存储整数的机器上编译这个程序,将产生一条出错消息:

Error directive: int type is too small

#error 指令通常会出现在 #if-#elif-#else 中的最后一部分:

1
2
3
4
5
6
7
8
9
#if defined(WIN32)
...
#elif defined(MAC_OS)
...
#elif defined(LINUX)
...
#else
#error No operating system specified
#endif

FAQ

究竟哪些常量需要定义成宏?

除了 0 和 1 以外的每一个数值常量都应该定义成宏。
使用宏来替换字符或字符串常量并不总能够提高程序的可读性,建议是:当常量被不止一次地使用,或以后可能需要修改常量时使用。

在执行预处理指令前,先处理注释。如果使用了条件屏蔽,而 #if#endif 之间有未终止的注释,会引起错误。

您的支持将鼓励我继续创作!

热评文章