C预处理
定义
C的翻译过程包含预处理、编译和链接等阶段。预处理器处理包含、宏替换与条件编译。预处理指令以 # 开头、独占一行、不以分号结尾,常见的有三类:
#include:把头文件的内容原地插入;#define:定义宏,编译前做文本替换;#if/#ifdef/#ifndef/#elif/#else/#endif:条件编译,按条件保留或丢弃一段文本。
宏替换按预处理记号进行,不执行C的类型检查。
性质
宏是文本替换
函数式宏的每个参数、以及整个展开式都要加括号,否则运算符优先级会改变结果:
#define SQUARE_BAD(x) x*x /* SQUARE_BAD(1 + 2) 展开为1 + 2*1 + 2,得5 */#define SQUARE_OK(x) ((x)*(x)) /* SQUARE_OK(1 + 2) 展开为 ((1 + 2)*(1 + 2)),得9 */宏参数可能在展开结果中出现多次,从而多次求值;SQUARE(i++) 中对同一对象的多次修改没有规定求值次序,会产生未定义行为。宏也不进行函数参数式的类型检查。只做一次求值、又想要类型检查时,用static inline函数代替宏。
#define定义的宏没有作用域,从定义处到文件末尾都有效,可以用 #undef取消。宏名习惯全大写,以便和变量区分。
头文件与包含
#include <stdio.h>:使用尖括号形式包含头文件。#include "myheader.h":使用引号形式包含头文件。具体查找位置由实现和构建配置决定。- 头文件里放声明(函数原型、类型定义、宏),需要外部链接的对象定义通常放在一个
.c文件中;头文件中的类型定义和某些具有内部链接的定义则另有规则。
包含是文本插入,同一个头文件被包含两次就会展开两次,因此需要包含守卫:
#ifndef POINT_H#define POINT_H
struct point { int x; int y;};
int point_length_sq(const struct point *p);
#endif第一次包含时POINT_H未定义,内容被保留并定义该宏;再次包含时 #ifndef条件不成立,整段被跳过。#pragma once是非标准但被广泛支持的等价写法。
条件编译
条件编译按宏的值决定保留哪一段文本,常用于调试开关和跨平台分支:
#define DEBUG 1
#if DEBUG printf("debug: n = %d\n", n);#endif
#ifdef _WIN32 /* Windows专用代码 */#endif#if后跟常量表达式,值为0时丢弃该段;#ifdef NAME判断宏是否已定义,#ifndef判断是否未定义;#elif、#else、#endif必须配对,嵌套时逐层闭合。
#if 0 ... #endif是暂时排除一段代码的常用写法,比整段注释安全,因为段内的注释符不会打断它。
例子
宏缺少括号的后果
#include <stdio.h>
#define SQUARE_BAD(x) x*x#define SQUARE_OK(x) ((x)*(x))
int main(void) { printf("%d\n", SQUARE_BAD(1 + 2)); /* 1 + 2*1 + 2 = 5 */ printf("%d\n", SQUARE_OK(1 + 2)); /* (1+2)*(1+2) = 9 */ return 0;}预处理器只做替换,替换后乘法先于加法,SQUARE_BAD得到5而不是9。
用包含守卫避免重复定义
#include "point.h"#include "point.h" /* 第二次包含会被守卫跳过 */
int main(void) { struct point p = {0, 0}; return point_length_sq(&p);}如果point.h没有包含守卫,struct point会被定义两次,编译报重复定义。
交互探索
改变实参,比较未加括号的宏展开与预期的平方。
宏参数缺少括号会怎样?
改变宏实参,比较替换后表达式的分组。
正在准备演示…
练习
在
#define SQUARE(x) x*x下,SQUARE(1 + 2)展开后的值是 。参考解答
宏只做文本替换,得到
1 + 2*1 + 2,乘法先算,结果是 。关于预处理与头文件,下列说法正确的是:
参考解答
预处理器不理解类型,C错;其余三项都是预处理阶段的实际行为。