这一章在干嘛?

预处理阶段的全套指令:#define 的替换规则与宏的副作用、宏 vs 函数的取舍、条件编译、文件包含与头文件守卫。宏是 C 最锋利也最容易割手的部分。

14.1 define:替换规则与副作用

14.2 宏与函数:怎么选

14.3 条件编译与文件包含

14.1 define:替换规则与副作用

#define MAX_COLS   20          /* 符号常量 */
#define SQUARE(x)  ((x) * (x)) /* 宏:名字(参数) 替换体 —— 中间不能有空格 */

int a = MAX_COLS;              /* 替换为 20 */
int b = SQUARE(3);             /* 替换为 ((3) * (3)) = 9 */

替换是纯文本操作:宏参数在替换体里每次出现都被原文替换。不加括号的后果:

#define BAD(x)  x * x
int c = BAD(1 + 2);      /* 变成 1 + 2 * 1 + 2 = 5,而不是 9! */
int d = SQUARE(1 + 2);   /* ((1+2)*(1+2)) = 9 —— 参数加括号 */

int n = 5;
int e = SQUARE(n++);     /* ((n++)*(n++)):同一序列点两次修改,未定义! */

宏的两大军规:

① 替换体里每个参数、整体结果都套括号;② 参数在替换体中只出现一次最安全——出现多次且调用方传 ++/—,就是副作用未定义行为。

14.2 宏与函数:怎么选

维度define函数
开销零调用开销,代码膨胀(每处展开)调用开销,代码一份
类型检查无——文本替换,任何类型都能套严格原型检查
副作用参数重复求值(++ 危险)参数只求值一次
类型灵活性天然泛型(对任何类型都「对」)类型固定(或用 void* 模拟)
适用短小、频繁、想要任意类型的计算逻辑复杂、参数有副作用、需要递归

两个实用补充:#undef NAME 取消定义(重定义前必须先 undef);#define 名称 只判断「是否定义过」用于条件编译。命名约定:宏全大写,让调用方一眼识别「这是替换、小心副作用」。

14.3 条件编译与文件包含

#if defined(DEBUG) && DEBUG >= 2    /* 表达式形式 */
    ...调试代码...
#elif defined(TRACE)
    ...精简日志...
#endif

#ifndef DEBUG                       /* 是否被定义的简写 */
#define DEBUG 1
#endif

/* 头文件守卫:防止重复包含导致重定义 */
#ifndef POINTS_ON_C_LIST_H
#define POINTS_ON_C_LIST_H
/* ...头文件内容:原型、类型定义、宏... */
#endif
写法含义用途
include <stdio.h>到系统目录找标准库/第三方
include “list.h”先当前目录再系统目录自己的头文件

头文件里只放声明(原型、类型、宏、extern 变量),不放定义(函数体、变量实体)——定义放 .c 文件,否则被两个单元包含就重定义。#include 嵌套靠守卫兜底。

本章通关标准:

看到 SQUARE(n++) 能指出未定义行为;写头文件条件反射加守卫;分清 <> 和 "" 的查找路径。