Note

ANSI C 标准描述的不只是 C 语言本身,还包括两员”编外大将”:预处理器(preprocessor)和 C 库(C library)。预处理器在编译之前检查你的程序,按指令替换符号缩写、包含其他文件、挑选编译器能看到的代码——它并不懂 C,只做文本到文本的转换,却威力巨大。C 库则提供数学计算、排序、内存拷贝等现成工具。本章把散落各章的 defineinclude 知识汇拢,再添上不少新货。

翻译程序的第一步

进入预处理之前,编译器要先做几步准备:把源代码字符映射到源字符集(处理多字节字符等国际化扩展);删掉行尾的反斜杠+换行组合,把多个物理行拼成一个逻辑行(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)。

/图:图16.1 类对象宏定义的三个部分

预处理器找到宏时,几乎总会用体替换它(唯一例外马上讲到),这个过程叫宏展开(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 类函数宏定义的组成部分

程序清单 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 ## nXNAME(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 = 14x2 = 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 = 48Message 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)或类函数宏。少数预定义宏(如 DATEFILE)永远算已定义,不允许 undef

ifdef、#else 和 endif

ifdef 类似 C 的 if/else:标识符已定义就处理到 elseendif 为止的部分,否则处理 elseendif 的部分(#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

ifdef 调试开关是”零成本调试”利器:调试代码平时完全不编译进程序,不占任何运行时间;哪天又需要了,把 define 加回来重新编译即可,不用重敲所有 printf。

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_ 这种形式即可。

ifelif 指令

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_VERSIONC99 环境为 199901L
TIME翻译时间,形如 “hh:mm:ss”

C99 另有预定义标识符 func,展开为所在函数的名字。注意它是 C 语言的预定义标识符而非宏——宏是文件作用域的,func 是函数作用域的。清单 16.12(predef.c)演示了这些宏的用法。#line 可重置 LINEFILE 报告的值;#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 两个参数,能按符号判断正确象限。

/图:图16.3 直角坐标与极坐标

/* 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 * 表示”任意类型的地址”。