Note
翻译程序的第一步
进入预处理之前,编译器要先做几步准备:把源代码字符映射到源字符集(处理多字节字符等国际化扩展);删掉行尾的反斜杠+换行组合,把多个物理行拼成一个逻辑行(logical line)——预处理指令必须是一整个逻辑行,但逻辑行可以跨多个物理行;把文本切分成预处理记号(token)与空白、注释序列,每个注释被替换成一个空格。之后预处理器才登场,查找以 # 开头的指令。
明示常量:#define
#define 与所有预处理指令一样以行首 # 开始(ANSI 允许 # 前有空格或制表符),从出现处生效到文件末尾。
程序清单 16.1 preproc.c 程序
/* preproc.c -- 简单的预处理器示例 */
#include <stdio.h>
#define TWO 2 /* 定义行里也可以用注释 */
#define OW "Consistency is the last refuge of the unimagina\
tive. - Oscar Wilde" /* 反斜杠把定义延续到下一行 */
#define FOUR TWO*TWO
#define PX printf("X is %d.\n", x)
#define FMT "X is %d.\n"
int main(void)
{
int x = TWO;
PX;
x = FOUR;
printf(FMT, x);
printf("%s\n", OW);
printf("TWO: OW\n");
return 0;
}运行输出:X is 2.、X is 4.、王尔德那句名言,以及字面量 TWO: OW。
每个 define 逻辑行分三部分:指令本身、你选的缩写(称为宏,macro;表示值的叫类对象宏,object-like macro,命名规则同变量、中间不能有空格)、以及行内其余部分——替换列表(replacement list)或体(body)。

预处理器找到宏时,几乎总会用体替换它(唯一例外马上讲到),这个过程叫宏展开(macro expansion)。注意几个细节:
int x = TWO;展开成int x = 2;;PX;展开成整个 printf 语句——宏能代表任意 C 表达式串,不只是常量。x = FOUR;先展开成x = TWO*TWO;再展开成x = 2*2;就停了。预处理器不做计算,乘法是编译器在编译期求值的。宏定义里还可以嵌套别的宏。- 字符串字面量里的宏不被替换:
printf("TWO: OW\n");原样打印。想打印展开值,宏要写在引号外:printf("%d: %s\n", TWO, OW); - 字符常量用
#define HAL 'z',字符串用#define HAP "Z"——引号差别很关键。 - 宏常量能用在
static int data1[LIMIT];里指定数组大小、作为 const 值的初始化值,这些 const 变量做不到。 - 术语补充:宏的体被视为记号串(string of tokens)而非字符串。
#define SIX 2 * 3的体是 3 个记号(2、*、3),空格只是分隔符;#define SIX 2*3的体只有 1 个记号。这也是 ANSI 判断”重复定义是否一致”的依据——想重定义宏,先用 undef 取消(后述)。
在 define 中使用参数
带参数的类函数宏(function-like macro)看上去和函数调用几乎一样:参数在括号里,出现在替换部分中。

程序清单 16.2 mac_arg.c 程序
/* mac_arg.c -- 带参数的宏 */
#include <stdio.h>
#define SQUARE(X) X*X
#define PR(X) printf("The result is %d.\n", X)
int main(void)
{
int x = 4;
int z;
printf("x = %d\n", x);
z = SQUARE(x);
printf("Evaluating SQUARE(x): ");
PR(z);
z = SQUARE(2);
printf("Evaluating SQUARE(2): ");
PR(z);
printf("Evaluating SQUARE(x+2): ");
PR(SQUARE(x+2));
printf("Evaluating 100/SQUARE(2): ");
PR(100/SQUARE(2));
printf("x is %d.\n", x);
printf("Evaluating SQUARE(++x): ");
PR(SQUARE(++x));
printf("After incrementing, x is %d.\n", x);
return 0;
}运行输出(倒数第二行依编译器而异):SQUARE(x) 得 16、SQUARE(2) 得 4,接着三行”意外”值得细看:
SQUARE(x+2)得 14 而非 36。预处理器只做字符串替换:X*X变成x+2*x+2,即 4+2×4+2 = 14。函数调用传的是参数的值,宏调用传的是参数记号——过程不同、时机不同。修复:给参数加括号#define SQUARE(X) ((X)*(X)),让SQUARE(x+2)展开成((x+2)*(x+2))。整体括号也必须加,否则100/SQUARE(2)展开成100/2*2(先 100/2 再乘 2 = 100),加了才变成100/((2)*(2))= 25。SQUARE(++x)展开成++x*++x,x 被自增两次!有的编译器算 56=30,有的算 66=36——结果未定义,而且无论哪种情况 x 都从 4 变 6。函数调用就没这问题:f(++x)先把 x 自增为 5,再把值 5 传进去。
Warning
不要把带副作用的表达式(++x、x++、函数调用等)当作宏参数——即使括号加得再全,参数也会被求值多次。守则:宏体里每个参数用括号包住,整个替换体也用括号包住;调用宏时避免自增、自减或赋值表达式。
用宏参数创建字符串:# 运算符
写在引号字符串里的宏参数只是普通文本,不会被替换。ANSI C 为此提供了 # 运算符(字符串化,stringizing):在类函数宏的替换部分,#x 把参数名转成字符串 “x”。
程序清单 16.3 subst.c 程序
/* subst.c -- 在字符串中替换 */
#include <stdio.h>
#define PSQR(x) printf("The square of " #x " is %d.\n", ((x)*(x)))
int main(void)
{
int y = 5;
PSQR(y);
PSQR(2 + 4);
return 0;
}运行输出:The square of y is 25. 和 The square of 2 + 4 is 36.——PSQR(2 + 4) 时 x 变成 “2 + 4”,连空格都保留。随后 ANSI C 的字符串拼接把相邻字符串连成一个:"The square of " "y" " is %d.\n" 合成 "The square of y is %d.\n"。
预处理器粘合剂:## 运算符
## 运算符把两个记号粘合成一个新记号,类函数宏和类对象宏都能用。#define XNAME(n) x ## n 让 XNAME(4) 展开成 x4——不是字符串,而是货真价实的标识符。
程序清单 16.4 glue.c 程序
// glue.c -- 使用 ## 运算符
#include <stdio.h>
#define XNAME(n) x ## n
#define PRINT_XN(n) printf("x" #n " = %d\n", x ## n);
int main(void)
{
int XNAME(1) = 14; // 展开成 int x1 = 14;
int XNAME(2) = 20; // 展开成 int x2 = 20;
PRINT_XN(1); // 展开成 printf("x1 = %d\n", x1);
PRINT_XN(2); // 展开成 printf("x2 = %d\n", x2);
return 0;
}运行输出 x1 = 14 和 x2 = 20。PRINT_XN() 同时用到两个运算符:# 把参数拼进字符串,## 把参数粘进标识符。
可变宏:… 和 VA_ARGS
printf() 能接受个数不定的参数,C99 把这能力也给了宏——可变宏(variadic macro)。宏参数列表最后一个参数写成省略号(…),替换部分用预定义宏 VA_ARGS 表示省略号代表的内容:
#define PR(...) printf(__VA_ARGS__)PR("Howdy"); 时 VA_ARGS 展开为 "Howdy";PR("weight = %d\n", wt); 时展开为 "weight = %d\n", wt,整体变成一次 printf 调用。
程序清单 16.5 variadic.c 程序
// variadic.c -- 可变宏
#include <stdio.h>
#include <math.h>
#define PR(X, ...) printf("Message " #X ": " __VA_ARGS__)
int main(void)
{
double x = 48;
double y;
y = sqrt(x);
PR(1, "x = %g\n", x);
PR(2, "x = %.2f, y = %.4f\n", x, y);
return 0;
}运行输出 Message 1: x = 48 和 Message 2: x = 48.00, y = 6.9282。第一次调用中 X 为 1,#X 变 “1”,字符串拼接后得到 "Message 1: x = %g\n"。省略号必须放最后,#define WRONG(X, ..., Y) 是无效的。
宏还是函数?
很多任务宏或函数都能做,如何取舍?宏的本质是时间换空间:宏产生内联代码,用 20 次就插入 20 份代码;函数只存一份但每次调用有跳转开销。宏还有个优势是不关心类型——它处理字符串而非值,SQUARE(x) 对 int 和 float 都适用。程序员一般只用宏实现简单操作:#define MAX(X,Y) ((X) > (Y) ? (X) : (Y))、#define ABS(X) ((X) < 0 ? -(X) : (X)) 这类。习惯上宏名全大写,提醒自己警惕副作用;若用宏是为了提速,先用性能分析工具(profiler)确认瓶颈真在那儿。C99 还提供了第三条路——内联函数(inline function),见后文。
文件包含:#include
预处理遇到 #include 时,会把指定文件的全部内容替换进当前文件,就像你亲手把整个文件敲在那个位置。两种写法:
#include <stdio.h> /* 文件名在尖括号里:查标准系统目录 */
#include "mystuff.h" /* 文件名在双引号里:先查当前目录,再查标准位置 */为什么要包含文件?因为里面有编译器需要的信息:stdio.h 包含 EOF、NULL 的定义、getchar()/putchar() 的宏定义以及 I/O 函数原型。头文件(header file,习惯 .h 后缀)里放编译器生成代码所需的信息,不是可执行代码。
头文件示例
假设写好一个”姓名”结构及配套函数,可以把声明集中到头文件 names_st.h(清单 16.6):#define 指令、结构声明、typedef、函数原型;函数定义放进源文件 names_st.c(清单 16.7,它也要 include 头文件);使用方 useheader.c(清单 16.8)同样 include,编译时两个 .c 一起编译链接。要点:声明进头文件,定义进源文件;多个源文件共用同一结构时都要包含对应头文件。
一个实用技巧:在源文件中定义 int status = 0;(文件作用域、外部链接),在配套头文件中放引用式声明 extern int status;,包含该头文件的文件就都能使用这个状态变量。
undef 与条件编译
写跨环境的程序时要按条件选择代码。相关指令:#undef 取消定义;#ifdef、#ifndef、#if、#elif、#else、#endif 实现条件编译;#line 重置行号与文件名;#error 发出错误消息;#pragma 向编译器下指令。
#undef:#define LIMIT 400 之后,#undef LIMIT 取消定义,之后可以重新定义。就算 LIMIT 从未定义过,#undef 它也合法——不确定名字是否用过时,先 undef 最保险。
“已定义”的预处理器视角:预处理器眼中的”已定义”(defined)只与宏有关——由本文件先前的 define 创建且未被 undef 取消的标识符才算。文件作用域的 C 变量在预处理器看来什么都不是。宏可以是类对象宏(含空宏,如 #define GOOD)或类函数宏。少数预定义宏(如 DATE、FILE)永远算已定义,不允许 undef。
ifdef、#else 和 endif
ifdef 类似 C 的 if/else:标识符已定义就处理到 else 或 endif 为止的部分,否则处理 else 到 endif 的部分(#else 可省,#endif 必须有)。条件结构可嵌套,也能圈住普通 C 语句:
#ifdef MAVIS
#include "horse.h" /* MAVIS 已定义时执行 */
#define STABLES 5
#else
#include "cow.h" /* MAVIS 未定义时执行 */
#define STABLES 15
#endif清单 16.9(ifdef.c)把这套机制用在循环体内:定义了 JUST_CHECKING 就输出每轮中间合计,删掉定义(或注释掉、或 undef)重新编译,就只剩最后一行——这正是调试输出的经典管理方式。
Success
ifndef 指令
ifndef 是 ifdef 的反面——问”标识符未定义吗”。两个常见用法:
/* arrays.h */
#ifndef SIZE
#define SIZE 100
#endif其一,“没定义才定义”,允许调用方覆盖默认值:include 之前先 #define SIZE 10,头文件里的 100 就被跳过——测试时用小数组很方便。
其二,更重要的用法:防止头文件被重复包含。头文件之间常常互相包含,而结构模板声明这类内容在一个文件里只能出现一次,重复包含会编译报错。解决办法是守卫宏(清单 16.10 给 names_st.h 加保护,清单 16.11 doubincl.c 故意重复包含同一头文件验证——有保护时程序正常,去掉保护就编译失败):
/* things.h */
#ifndef THINGS_H_
#define THINGS_H_
/* 文件其余内容 */
#endif第一次包含时 THINGS_H_ 未定义,于是定义它并处理整个文件;再次包含时它已定义,其余内容被跳过。标准头文件都这么干(stdio.h 里就有 #ifndef _STDIO_H 这样的保护)。注意:下划线开头的前缀是标准保留的,自己的守卫宏用 NAMES_H_ 这种形式即可。
if 和 elif 指令
if 更像普通 C if:后面跟整型常量表达式,非零即真,可用关系和逻辑运算符。defined 运算符让 #if defined (VAX) 等价于 #ifdef VAX,且能配合 elif 做多平台分支:
#if SYS == 1
#include "ibmpc.h"
#elif SYS == 2
#include "vax.h"
#else
#include "general.h"
#endif只需在文件开头改几个 define,整套程序就能适配不同系统——这是条件编译最典型的用途。
预定义宏
C 标准规定了若干预定义宏:
| 宏 | 含义 |
|---|---|
| DATE | 预处理日期,形如 “Mmm dd yyyy” 的字符串 |
| FILE | 当前源代码文件名的字符串 |
| LINE | 当前文件中的行号(整数) |
| STDC | 遵循 C 标准时置为 1 |
| STDC_HOSTED | 宿主环境为 1,否则为 0 |
| STDC_VERSION | C99 环境为 199901L |
| TIME | 翻译时间,形如 “hh:mm:ss” |
C99 另有预定义标识符 func,展开为所在函数的名字。注意它是 C 语言的预定义标识符而非宏——宏是文件作用域的,func 是函数作用域的。清单 16.12(predef.c)演示了这些宏的用法。#line 可重置 LINE 和 FILE 报告的值;#error 让预处理器发出错误消息并中止编译,常与 if 搭配做版本检查(#if __STDC_VERSION__ != 199901L 后接 #error Not C99);#pragma 在源码中直接向编译器下指令,各编译器有自己的 pragma 集合。
内联函数
函数调用有开销:传参、跳转、返回都花时间。类函数宏就是为省这笔开销,C99 提供了更正规的替代——用 inline 限定符建议编译器”让调用尽可能快”,编译器可能用函数体直接替换调用(也可能什么都不做,效果”实现定义”):
inline void eatline() // inline 定义同时充当原型
{
while (getchar() != '\n')
continue;
}要点:内联函数应短小;它没有独立代码块,取函数地址会导致编译器生成非内联版本;调试器里可能看不到它。内联函数通常有内部链接,定义须与调用同文件——多文件程序最简单的做法是把 inline 定义放进头文件让各文件包含(这是”头文件不放可执行代码”规则的例外)。C 还允许 inline 定义与外部定义混合,但取地址后编译器会改用外部定义,标准警告不要写依赖具体选哪一版的代码。
C 库
C 库最初没有官方版本,ANSI 委员会在 Unix 事实标准基础上制定了标准库。使用库函数有三个途径:自动访问(很多系统上直接编译即可,只需 include 相应头文件获得声明);文件包含(函数若是宏,如 getchar(),包含定义它的头文件即可);库包含(有些函数库需显式指定链接选项,如 Unix 数学库 -lm)。注意:头文件提供函数原型,库选项告诉系统函数代码在哪里,两者是两回事。
读库函数文档的关键是看懂函数头。以 fread() 的 ANSI C90 描述为例:
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);两个新面孔:size_t 是 sizeof 返回的无符号整数类型(typedef 或 define 定义在 stddef.h 等头文件中);void 指针(pointer-to-void)充当”通用指针”,任何数据类型的指针都能赋给它,fread() 因此能接收指向任意类型数组的指针。C99 又在描述中加入了 restrict 关键字。
数学库:程序清单 16.13 rect_pol.c
math.h 声明数学库函数,角度一律用弧度(radian,1 弧度 = 180/π ≈ 57.296°):acos、asin、atan、atan2、cos、sin、tan、exp、log、log10、pow、sqrt、ceil、floor、fabs 等,参数和返回值都是 double。
一个典型应用:直角坐标 (x, y) 转极坐标。模长 = √(x²+y²),角度 = arctan(y/x)。但 atan() 有盲区:(-5)/(-5) 和 5/5 的商都是 1,它分不清方向相差 180° 的两条线。atan2() 接收 y、x 两个参数,能按符号判断正确象限。

/* rect_pol.c -- 直角坐标转极坐标 */
#include <stdio.h>
#include <math.h>
#define RAD_TO_DEG (180/(4 * atan(1)))
typedef struct polar_v { double magnitude; double angle; } POLAR_V;
typedef struct rect_v { double x; double y; } RECT_V;
POLAR_V rect_to_polar(RECT_V);
int main(void)
{
RECT_V input;
POLAR_V result;
puts("Enter x, y coordinates; enter q to quit:");
while (scanf("%lf %lf", &input.x, &input.y) == 2)
{
result = rect_to_polar(input);
printf("magnitude = %0.2f, angle = %0.2f\n",
result.magnitude, result.angle);
}
puts("Bye.");
return 0;
}
POLAR_V rect_to_polar(RECT_V rv)
{
POLAR_V pv;
pv.magnitude = sqrt(rv.x * rv.x + rv.y * rv.y);
if (pv.magnitude == 0)
pv.angle = 0.0;
else
pv.angle = RAD_TO_DEG * atan2(rv.y, rv.x);
return pv;
}小技巧:4 * atan(1) 就是 π(tan 45° = 1),宏 RAD_TO_DEG 用它把弧度换算成角度。若编译时报 Undefined: _sqrt 之类的链接错误,说明链接器没找到数学库——Unix/Linux 上加 -lm 选项:cc rect_pol.c -lm。
通用工具库:exit()、atexit() 与 qsort()
stdlib.h 是个”百宝箱”:随机数(rand()、srand())、排序搜索、类型转换、内存管理(malloc()、free())。main() 返回时其实隐式调用了 exit(),ANSI 的新花样是 atexit():注册若干函数,在 exit() 执行时自动调用(清单 16.14 byebye.c 演示:atexit() 接收函数指针作参数,函数名在这里就是地址;ANSI 保证至少能注册 32 个;exit() 调用它们时后注册的先执行)。exit() 还会刷新所有输出流、关闭打开的流和 tmpfile() 创建的临时文件,然后报告终止状态——用 EXIT_SUCCESS(或 0)表成功、EXIT_FAILURE 表失败,比裸写返回值更可移植。在非递归的 main() 里 exit() 等价于 return;但 exit() 用在其他函数里也能直接终止整个程序,这是 return 做不到的。
快速排序(quick sort,C. A. R. Hoare 1962 年提出)是对大数组最有效的排序算法之一,C 库版本叫 qsort():
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));四个参数:待排序数组起始地址(void 指针,什么数组都行)、元素个数、单个元素的大小(qsort() 只拿到 void 指针,不知道元素多大)、比较函数指针。比较函数接收两个指向待比较元素的 const void 指针:第一个元素应排后面返回正数,相同返回 0,应排前面返回负数。
程序清单 16.15 qsorter.c 程序
清单 16.15 创建一个随机 double 数组并排序,其中 fillarray() 用 rand() 填充随机值、showarray() 每行打印 6 个,核心是下面这部分:
double vals[NUM];
fillarray(vals, NUM);
showarray(vals, NUM);
qsort(vals, NUM, sizeof(double), mycomp); /* 四个参数各就各位 */
showarray(vals, NUM);
/* 按值递增排序的比较函数 */
int mycomp(const void * p1, const void * p2)
{
/* qsort() 传来的是 void 指针,必须还原成 double 指针才能解引用 */
const double * a1 = (const double *) p1;
const double * a2 = (const double *) p2;
if (*a1 < *a2)
return -1;
else if (*a1 == *a2)
return 0;
else
return 1;
}运行后随机数组就变成了升序数组。比较函数是固定套路:把 void 指针转换成正确类型((const double *) p1)再解引用比较。这个类型转换在 C 里可选,在 C++ 里强制——写上它,代码两种语言通吃。
Success
qsort() + 自定义比较函数是”通用排序”的教科书范例:同一个 qsort(),换个比较函数就能按任何标准排任何类型。比如给 struct names 数组按姓氏排序,比较函数里先用 strcmp() 比姓氏、相同再比名字,
qsort(staff, 100, sizeof(struct names), comp);一行搞定。
assert 库:程序清单 16.16 assert.c
assert.h 提供的 assert() 宏是调试好帮手:接收一个整型表达式,若为假(求值为 0),就向标准错误流 stderr 写一条错误信息并调用 abort() 终止程序;信息自动包含失败的表达式、文件名和行号。
/* assert.c -- 使用 assert() */
#include <stdio.h>
#include <math.h>
#include <assert.h>
int main()
{
double x, y, z;
puts("Enter a pair of numbers (0 0 to quit): ");
while (scanf("%lf%lf", &x, &y) == 2
&& (x != 0 || y != 0))
{
z = x * x - y * y; /* 本应是 + */
assert(z >= 0);
printf("answer is %f\n", sqrt(z));
puts("Next pair of numbers: ");
}
puts("Done");
return 0;
}程序故意把加法写成减法,输入 3 和 5 时 z 为负,assert 拦截并打印类似 Assertion failed: z >= 0, file C:\assert.c, line 14 的信息。注意消息的意思是”断言 z >= 0 失败了”,不是说 z 大于等于 0。用 if 加 abort() 也能达到类似效果,但 assert() 自动报告文件和行号,还有免改代码的开关:在包含 assert.h 之前定义 #define NDEBUG 再重新编译,文件中所有 assert() 都失效;删掉定义重新编译又全部恢复。
memcpy() 和 memmove()
数组不能整体赋值。字符数组有 strcpy()/strncpy(),其他数组靠 string.h 的这两个函数:
void *memcpy(void * restrict s1, const void * restrict s2, size_t n);
void *memmove(void *s1, const void *s2, size_t n);两者都从 s2 指向的位置拷贝 n 个字节到 s1,并返回 s1。区别在于 restrict 透露的约定:memcpy() 假设两块内存不重叠;memmove() 允许重叠(先拷到临时缓冲再拷到目的地)。清单 16.17(mems.c)还演示了一点:把 double 数组拷进 int 数组,得到的是同一批位被重新解读的结果——这两个函数不认识数据类型,按位模式原样搬运,没有类型转换。
Warning
内存区间重叠时调用 memcpy() 是未定义行为——可能碰巧正确,也可能悄悄出错,编译器不会替你把关。拿不准就用 memmove()。另外第三个参数是字节数不是元素个数:拷 10 个 double 要写 10*sizeof(double)。
变参函数:stdarg.h
可变宏之外,stdarg.h 让函数也能接受个数不定的参数,步骤固定:原型中参数列表至少有一个形参、后面跟省略号(必须在最后);函数内声明一个 va_list 类型变量存放参数;用 va_start() 初始化它(第二个参数是省略号前最后一个形参,即 parmN);用 va_arg() 逐个读取参数(给出要读取的类型);用 va_end() 收尾清理。C99 又加了 va_copy(),可把 va_list 变量复制一份备用(va_arg() 只能前进不能回头)。类型必须与实参真正匹配:va_arg() 不做自动转换,实参传 10 而按 double 读就会出问题。
程序清单 16.18 varargs.c 程序
清单 16.18 的 sum() 用第一个参数指明省略号里有几个数,把它们全部累加:
// varargs.c -- 变参函数
#include <stdio.h>
#include <stdarg.h>
double sum(int, ...);
int main(void)
{
double s, t;
s = sum(3, 1.1, 2.5, 13.3);
t = sum(6, 1.1, 2.1, 13.1, 4.1, 5.1, 6.1);
printf("return value for sum(3, ...): %g\n", s);
printf("return value for sum(6, ...): %g\n", t);
return 0;
}
double sum(int lim, ...)
{
va_list ap; // 声明存放参数的对象
double tot = 0;
int i;
va_start(ap, lim); // 把 ap 初始化为参数列表
for (i = 0; i < lim; i++)
tot += va_arg(ap, double); // 逐个读取参数
va_end(ap); // 清理
return tot;
}运行输出 16.9 和 31.6。变参函数通常都得靠 lim 这样的”带路人”参数。总体上变参函数比可变宏繁琐,但适用面更广。
关键概念与小结
C 标准描述的是一个包含三件套的包:C 语言、C 预处理器和标准 C 库。预处理器让你左右编译过程——指定替换、挑选参与编译的代码、影响编译器行为;C 库为输入输出、文件处理、内存管理、排序查找、数学计算、字符串处理等常见问题提供现成方案。
- define 创建宏:类对象宏、类函数宏、# 字符串化、## 记号粘合、VA_ARGS 可变参数;#include 包含文件;#ifdef/#ifndef/#if/#elif/#else/#endif 条件编译;#undef 取消定义。
- 内联函数是类函数宏的正规替代,适合短小且频繁调用的函数;inline 定义通常放头文件。
- C 库按头文件分组:math.h 数学函数、stdlib.h 通用工具(exit/atexit/qsort)、assert.h 调试断言、string.h 的 memcpy/memmove、stdarg.h 变参函数。记住两个通用约定:size_t 表示大小,void * 表示”任意类型的地址”。
自测题(点击展开答案)
1. define POD FEET + FEET 之后写 plort = FEET * POD; 展开成什么?有问题吗?
展开成
plort = FEET * FEET + FEET;,即 44+4 = 20,而你可能想要 4(4+4) = 32。问题出在宏体没加括号。更可靠的定义是#define POD (FEET + FEET)——参数和整体都要括起来。2. SQUARE(X) 定义为 X*X,为什么 SQUARE(x+2) 和 SQUARE(++x) 都会出问题?
前者展开成 x+2x+2,乘法优先级高,结果是 14 而非 36;后者展开成 ++x++x,参数被求值两次,x 自增两次且结果依编译器而异(未定义行为)。括号版
((X)*(X))能救前者,救不了后者——带副作用的表达式根本不该作为宏参数。ifdef 问”已定义吗”,常用于调试开关和多平台分支;#ifndef 问”未定义吗”,最典型用途是头文件包含保护(守卫宏防止重复包含),也用于”没定义才定义默认值”,让调用方能覆盖默认配置。
4. memcpy() 和 memmove() 的区别是什么?
两者都按字节拷贝 n 字节并返回目的指针。memcpy() 带 restrict 限定,假设源和目的内存不重叠,速度可能更快;memmove() 允许区间重叠(借助临时缓冲)。重叠时用 memcpy() 是未定义行为,不确定时选 memmove()。
5. 用 qsort() 把 1000 个 int 降序排列,调用和比较函数怎么写?
调用:
qsort(scores, 1000, sizeof(int), comp);。比较函数:int comp(const void * p1, const void * p2) { const int * a1 = (const int *) p1; const int * a2 = (const int *) p2; if (*a1 < *a2) return 1; else if (*a1 == *a2) return 0; else return -1; }——降序只需把返回值方向反过来(第一个元素”更小”时返回正数,让它排后面)。