C Primer Plus之C预处理器和C库
编译程序前,先由预处理器检查程序(因此称为预处理器)。根据程序中使用的预处理器指令,预处理器用符号缩略语所代表的内容替换程序中的缩略语。
预处理器不能理解C,它一般是接受一些文件并将其转换成其他文本。
翻译程序的第一步
对程序作预处理前,编译器会对它进行几次翻译处理。
- 编译器首先把源代码中出现的字符映射到源字符集(?)。该过程处理多字节字符和使C外观更加国际化的三元字符(?)扩展
- 编译器查找反斜线(\)后紧跟换行符的实例并删除这些实例。注意:在这种场合下,“换行符”代表按下回车键在源代码文件中新起一行所产生的字符,而不是符号\n代表的字符。因为预处理表达式的长度为一逻辑行,所以这个功能为预处理做了准备工作。而一个逻辑行可能会比物理行多。预处理指令从#开始,直到其后第一个换行符为止(即指令的长度限于一行代码,即预处理器表达式的长度为一逻辑行)。在预处理开始之前,系统会删除反斜线和换行符之间的组合。因此可以把指令扩展到几个物理行,由这些物理行组成单个逻辑行。举例说明:
printf("That's wond\ // 一个物理行 erful!\n"); // 又一个物理行
↓ 可转换为
printf("That's wonderful!\n"); // 一个逻辑行
为什么说“换行符”代表按下回车键在源代码文件中新起一行所产生的字符,而不是符号\n代表的字符?且看下面分解:
#define OW1 "Consistency is the last refuge of the unimagina\ tive. - Oscar Wilde" #define OW2 "Consistency is the last refuge of the unimagina\ tive. - Oscar Wilde" // 注意最起始有一个空格,这行开头和tive之间的空格也作为字符串的一部分 printf("%s\n", OW1); printf("%s\n", OW2);
↓ 输出分别是
Consistency is the last refuge of the unimaginative. - Oscar Wilde // OW1 Consistency is the last refuge of the unimagina tive. - Oscar Wilde // OW2
- 编译器将文本划分成预处理的语言符号(token)序列和空白字符及注释序列(术语语言符号代表由空格分隔的组)。注意:编译器用一个空格字符代替每一个注释。例如:
int /* 这不是个空格 */ fox;
↓ 将变成
int fox;
C实现可能还会选用单个空格代替每一个空白字符(不包括换行符)序列
- 程序进入预处理阶段。预处理寻找可能存在的预处理指令,这些指令由一行开始处的#符号标识
明显常量(或符号常量)#define
ANSI标准允许#符号前有空格或制表符,而且该标准还允许在#和指令的其余部分(我觉得应该是宏或主体)之间有空格。
指令可出现在源文件的任何地方。指令定义的作用域从定义出现的位置开始直到文件的结尾。
每个#define行(即逻辑行)由三部分组成:
- 指令#define自身。
- 所选择的缩略语,这些缩略语称为宏(macro)。宏有类对象宏,有类函数宏。宏的名字中不允许有空格,而且必须遵循C变量命名规则:只能使用字母、数字和下划线(_),第一个字符不能为数字。
- #define行的其余部分,称为替换列表或主体
预处理器在发现了宏的实例后,总会用实体(主体)代替该宏(有一种例外,即双引号中的宏),如果该字符串中还包括宏,则继续替换这些宏。从宏变成最终的替换文本的过程称为宏展开。注意可以使用标准的C注释方法在#define行中进行注释。宏可以表示任何字符串,甚至可表示整个C表达式。宏定义中可包含其他宏。
注意:C编译器在编译时对所有常量表达式(只包含常量的表达式)求值,所以,一系列运算过程(比如,相乘等)发生在编译阶段,而不是预处理器阶段。预处理器不进行计算,它只是按照指令进行文字替换操作(非常重要)。
记忆值的能力、易更改性、可移植性,这些功能使得符号常量很有使用价值。
宏常量可以用来指定标准数组的大小并作为const值的初始化值。见下:
#define LIMIT 20 const int LIM = 50; static int data1[LIMIT]; // 合法 static int data2[LIM]; // 无效 const int LIM2 = 2 * LIMIT; // 合法 const int LIM3 = 2 * LIM; // 无效
语言符号
从技术方面看,系统把宏的主体当作语言符号(token)类型字符串,而不是字符型字符串。C预处理器中的语言符号是宏定义主体中的单独的“词”。用空白字符把这些词分开。例如:
#define FOUR 2*2 // 只有一个语言符号,即序列2*2 #define SIX 2 * 3 // 有3个语言符号:2、*、3
在处理主体中的多个空格时,字符型字符串和语言符号类型字符串采用不同方法。用字符型字符串的观点看,空格也是主体的一部分;而用语言符号字符串的观点看,空格只是分隔主体中语言符号的符号。
C编译器处理语言符号的方式比预处理器的处理方式更加复杂。编译器能理解C的规则,不需要使用空格来分隔语言符号。例如:
#define FOUR 2*2 // C编译器把2*2当作3个语言符号。原因是C编译器认为每个2都是一个常量,而*是一个运算符
假设您把LIMIT定义为20,后来在该文件中又把LIMIT定义为25。这个过程被称为重定义常量。不同的编译器采用不同的重定义策略。ANSI标准采用的方式是:只允许新定义与旧定义完全相同。相同意味着主体具有相同顺序的语言符号。例如,下面两个定义相同:
#define SIX 2 * 3 #define SIX 2 * 3
在#define中使用参数
通过使用参数,可以创建外形和作用都与函数相似的类函数宏。宏的参数也用圆括号括起来,因此带参数的宏外形与函数非常相似。类函数宏的定义中,用圆括号括起一个或多个参数,随后这些参数出现在替换部分。例如:
#define SQUARE(X) X*X // SQUARE为宏标识符, SQUARE(X)中的X为宏的参数,X*X为替换列表。所以程序中出现SQUARE(X)的地方都用X*X代替。使用这个宏时,即可以使用x,也可以自由地使用其他符号。宏定义中的x由程序调用的宏中的符号代替。
函数调用和宏调用之间的重要差异是:程序运行时,函数调用把参数的值传递给函数。而编译前,宏调用把参数的语言符号传递给程序。这是不同时间发生的不同过程。
注意:以下情况
SQUARE(++x)
↓ 变成
++x*++x
x进行了两次增量操作,其中一次在乘法操作前,一次在乘法操作后。
因为编译器对这些运算的顺序没有做出规定,所以有些编译器产生乘积6*5,有些编译器可能产生乘积5*6,而其他编译器可能在乘法运算前同时对x进行自加操作,从而产生6*6。但是在这些情况中,x的初始值均为4,终止值均为6。
结论:一般来说,在宏中不要使用增量或减量运算符。
#运算符
假设您确实希望在字符串中包含宏参数,ANSI C允许您这样做。在类函数宏的替换部分中,#符号用作一个预处理器运算符,它可以把语言符号转换为字符串。例如,如果x是一个宏参量,那么#x可以把参数名转换为相应的字符串,该过程称为字符串化。
##运算符 ·
和#运算符一样,##运算符可以用于类函数宏的替换部分。其次,##还可用于类对象宏的替换部分。这个运算符把两个语言符号组合成单个语言符号。举例说明:
#define XNAME(n) x ## n
这样,下面的宏调用:
XNAME(4)
↓ 会展开成
x4
可变宏:...和__VA_ARGS__
实现思想就是宏定义中参数列表的最后一个参数为省略号(也就是三个句号)。注意:省略号只能代替最后的宏参数。这样,预定义宏(这些宏总被认为是已定义的,并且不能被取消定义)__VA_ARGS__就可以被用在替换部分中,以表明省略号代表什么。举例说明如下:
#define PR(...) printf(__VA_ARGS__)
↓ 用下面的方式调用该宏
PR("Howdy"); PR("weight = %d, shipping = $%.2f\n", wt, sp);
第一次调用中,__VA_ARGS__展开为1个参数:“Howdy”。
第二次调用中,__VA_ARGS__展开为3个参数:“weight = %d, shipping = $%.2f\n”, wt, sp。
因此,展开后的代码为:
printf("Howdy"); printf("weight = %d, shipping = $%.2f\n", wt, sp);
宏,还是函数
在使用宏时,若不注意的话会产生一些奇怪的现象。因此,宏在某种程度上比常规的函数复杂。有些编译器限制宏只能定义成一行。即使您的编译器没有这个限制,也应遵守这个限制。
宏与函数间的选择实际上是时间与空间的权衡。宏产生内联代码;也就是说,在程序中产生语句。如果使用宏20次。则会把20行代码插入程序中。如果使用函数20次,那么程序中只有一份函数语句的拷贝,因此节省了空间。其次,程序的控制必须转移到函数中并随后返回调用程序,因此这比内联代码花费的时间多。总结起来就是:函数节省了空间,浪费了时间,宏反之。
宏的一个优点是它不检查其中的变量类型(这是因为宏处理字符型字符串,而不是实际值)。因此,对于int或float都可以使用宏SQUARE(x)。程序员一般将宏用于简单函数。下面是需要注意的几点:
- 记住,宏的名字中不能有空格,但是在替代字符串中可以使用空格。ANSI C允许在参数列表中使用空格。
- 用圆括号括住每个参数,并括住宏的整体定义。
- 用大小字母表示宏函数名。该约定不如使用大小字母表示宏常量的约定用得广泛。但是,使用大小字母可以提醒程序员注意宏可能产生的副作用。
- 如果打算使用宏替代函数来加快程序的运行速度,那么首先应确定宏是否会引起重大差异。在程序中只使用一次的宏对程序运行时间可能不会产生明显的改善。在嵌套循环中使用宏更有助于加速程序运行。许多系统提供程序配置器以帮助程序员压缩最耗费运行时间的程序部分。
文件包含:#include
预处理器发现#include指令后,就会寻找后跟的文件名并把这个文件的内容包含到当前文件中。被包含文件中的文本将替换源代码文件中的#include指令,就像您把被包含文件中的全部内容键入到源文件中的这个特定位置一样。#include指令有两种使用形式
#include <stdio.h> // 文件名放在尖括号中 #include "mystuff.h" // 文件名放在双引号中
在UNIX系统中,尖括号告诉预处理器在一个或多个标准系统目录中寻找文件。双引号告诉预处理器先在当前目录(或文件名中指定的其他目录)中寻找文件,然后在标准位置寻找文件。
#include <stdio.h> | 搜索标准系统目录 |
#include "hot.h" | 搜索当前工作目录 |
#include "/usr/biff/p.h" | 搜索/usr/biff目录 |
问:为什么要包含文件呢?
答:因为这些文件包含了编译器所需的信息。很多情况下,头文件中的内容是编译器产生最终代码所需的信息,而不是加到最终代码里的具体语句。
头文件内容的最常见的形式包括:
- 明显常量——例如,典型的stdio.h文件定义EOF,NULL和BUFSIZE(标准I/O缓冲区的大小)
- 宏函数——例如,getchar()通常被定义为getc(stdin),getc()通常被定义为较复杂的宏,而头文件ctype.h通常包含ctype函数的宏定义
- 函数声明——在ANSI C中,声明采用函数原型形式
- 结构模板定义——标准I/O函数使用FILE结构,该结构包含文件及文件相关缓冲区的信息。头文件stdio.h中存放FILE结构的声明
- 类型定义——可以使用指向FILE的指针作为参数调用标准I/O函数。通常,stdio.h用#define或typedef使得FILE代表指向FILE结构的指针。与之类似,size_t和time_t类型也在头文件中定义
其他指令
#undef指令
#undef指令取消定义一个给定的#define。如果想使用一个特定名字,但又不能确定前面是否已经使用了改名字,为安全起见,就可以取消改名字的定义。
预处理器在预处理指令中遇到标识符时,要么把标识符当作已定义的,要么当作未定义的。这里的已定义表示由预处理器定义。如果标识符是该文件前面的#define指令创建的宏名,并且没有用#undef指令关闭该标识符,则标识符是已定义的。如果标识符不是宏,而是(例如)一个具有文件作用域的C变量,那么预处理器把标识符当作未定义的。
已定义宏可以为类对象宏(包括空宏)或类函数宏。
注意,#define宏的作用域从文件中的定义点开始,直到用#undef指令取消宏为止,或直到文件尾为止(由二者中最先满足的那个结束宏的作用域)。还应注意,如果用头文件引入宏,那么,#define在文件中的位置依赖于#include指令的位置。
条件编译
条件编译——使用这些指令告诉编译器:根据编译时的条件接收或忽略信息(或代码)块。
#ifdef、#else和#endif指令
#ifdef指令说明:如果预处理器已经定义了后面的标识符,那么执行所有指令并编译C代码,直到下一个#else或#endif出现为止(无论#else和#endif谁先出现)。如果有#else指令,那么,在未定义标识符时会执行#else(如果需要)和#endif(必须存在)之间的所有代码。也可用这些指令标记C语句块。举例如下:
#ifdef MAVIS #include "horse.h" // 如果已经用#define定义了MAVIS,则执行这里的指令 #define STABLES 5 #else #include "cow.h" // 如果没有用#define定义MAVIS,则执行这里的指令 #define STABLES 15 #endif
#ifndef指令
类似于#ifdef指令,#ifndef指令可以与#else、#endif指令一起使用。#ifndef判断后面的标识符是否为未定义的,#ifndef的反义词是#ifdef。#ifndef通常用来定义此前未定义的常量。
#ifndef指令通常用于防止多次包含同一文件。也就是说,头文件可采用类似下面几行的设置:
/* things.h */ #ifndef THINGS_H_ #define THINGS_H_ /* 头文件的其余部分 */ #endif
问:为什么会多次包含同一文件呢?
最常见的原因是:许多包含文件自身包含了其他文件,因此可能显式地包含其他文件已经包含的文件。
#if和#elif指令
#if指令更像常规的C中的if,#if后跟常量整数表达式。如果表达式为非零值,则表达式为真。在该表达式中可以使用C的关系运算符合逻辑运算符。
可以使用#elif指令扩展if-else序列。
许多新的实现提供又一种方法来判断一个名字是否已经定义。不需要使用#ifdef指令。而是采用下面的形式:
#if defined(VAX)
这里,defined是一个预处理运算符。如果defined的参数已用#define定义过,那么defined返回1;否则返回0。这种新方法的优点在于它可以和#elif一起使用。例如:
#if defined(IBMPC) #include "ibmpc.h" #elif defined(VAX) #include "vax.h" #elif defined(MAC) #include "mac.h" #else #include "general.h" #endif
预定义宏
宏 | 意义 |
__DATE__ | 进行预处理的日期(“Mmm dd yyyy”形式的字符串文字) |
__FILE__ | 代表当前源代码文件名的字符串文字 |
__LINE__ | 代表当前源代码文件中的行号的整数常量 |
__STDC__ | 设置为1时,表示该实现遵循C标准 |
__STDC_HOSTED__ | 为本机环境设置为1,否则设为0 |
__STDC_VERSION__ | 为C99时设置为199901L |
__TIME__ | 源文件编译时间,格式为“hh: mm: ss” |
C99标准提供了一个名为__func__的预定义标识符。__func__展开为一个代表函数名(该函数包含该标识符)的字符串。该标识符具有函数作用域,而宏本质上具有文件作用域。因而__func__是C语言的预定义标识符,而非预定义宏。
#line和#error
#line指令用于重置由__LINE__和__FILE__宏报告的行号和文件名。可以这样使用#line:
#line 1000 // 把当前行号重置为1000 #line 10 "cool.c" // 把行号重置为10,文件名重置为cool.c
#error指令使预处理器发出一条错误信息,该信息包含指令中的文本。可以这样使用#error:
#if __STDC_VERSION__ != 199901L #error Not C99 #endif
#pragma
可用#pragma将编译器指令置于源代码中。例如,在开发C99时,用C9X代表C99。编译器用下面的编译指示来启用对C9X的支持:
#pragma c9x on
一般来说,每台编译器都有自己的编译指示集。这些编译指示集可能用于:
- 控制分配给自动变量的内存大小
- 设置错误检查的严格程度
- 启用非标准语言特征
C99还提供了_Pragma预处理器运算符。_Pragma可将字符串转换为常规的编译指示。例如:
_Pragma("nonstandardtreatmenttypeB on")
↓ 等价于
#pragma nonstandardtreatmenttypeB on
_Pragma运算符完成字符串析构工作;也即是说,将字符串中的转义序列转换成它所代表的字符。例如:
_Pragma("use_bool \"true \"false")
↓ 变成
#pragma use_bool "true "false
内联函数
通常函数调用需要一定的时间开销。这意味着执行调用时花费了时间用于建立调用、传递参数、跳转到函数代码并返回。使用类函数宏的一个原因就是可以减少执行时间。C99还提供了一个方法:内联函数(inline function)。
C99标准这样描述:”把函数变为内联函数将建议编译器尽可能快速地调用该函数。上述建议的效果由实现来定义“。因此,使函数变为内联函数可能会简化函数的调用机制,但也可能不起作用。
创建内联函数的方法是在函数声明中使用函数说明符(inline)。通常,首次使用内联函数前在文件中对该函数进行定义。因此,该定义也作为函数原型。
因为内联函数没有预留给它的单独代码块,所以无法获得内联函数的地址(实际上,可以获得地址,但这样做会使编译器产生非内联函数)。其次,内联函数不会在调试器中显示。
内联函数应该比较短小。但对于很长的函数,调用函数的时间少于执行函数主体的时间;此时,使用内联函数不会节省多少时间。
编译器在优化内联函数时,必须知道函数定义的内容。这意味着内联函数的定义和对该函数的调用必须在同一文件中。正因为这样,内联函数通常具有内部链接。因此,在多文件程序中,每个调用内联函数的文件都要对该函数进行定义。达到这个目标的最简单方法为:在头文件中定义内联函数,并在使用该内联函数的文件中包含该头文件。一般不在头文件中放置可执行代码。但内联函数是个例外。因为内联函数具有内部链接,所以在多个文件中定义同一内联函数不会产生什么问题。
与C++不同的是,C允许混合使用内联函数定义和外部函数定义(具有外部链接的函数定义)。例如:
// file1a.c ... inline double square(double); double square(double x) {return x * x;} int main() { double q = square(1.3); // 使用的是内联函数 } // file2a.c ... extern double square(double); double square(double x) {int y; y = x * x; return y;} void spam(double v) { double kv = square(v); // 使用file1a.c的外部函数定义 ... //file3a.c extern double square(double); void masp(double w) { double kw = square(w); // 使用file1a.c的外部函数定义 ...
C甚至还允许在包含内联函数定义的文件中放置外部函数声明:
// file1b.c -- 小心! ... extern double square(double); // 把square()声明为外部函数 inline double square(double); // 把square()声明为内联函数 double square(double x) {return x * x;} int main() { double q = square(1.3) + square(1.5); // 哪一个square()? ... // file2b.c ... extern double square(double); double square (double x) {int y; y = x * x; return y;} ...
通用工具库
exit()和atexit()函数
注意:从main()返回时自动调用exit()函数,也即main()终止时会隐式地调用exit(),也即在非递归的main()函数中使用exit()函数等价于使用关键字return。在main()以外的函数中使用exit()也会终止程序。
我们可以指定执行exit()时要调用的特定函数。通过对退出时调用的函数进行注册,atexit()函数也提供这项功能;atexit()函数使用函数指针作为参数。
要使用atexit()函数,只需把退出时要调用的函数地址传递给atexit()。因为作为函数参数时,函数名代表地址。atexit()把作为其参数的函数在调用exit()时执行的函数列表中进行注册。ANSI保证在这个列表中至少可放置32个参数。通过使用一个单独的atexit()调用把每个函数添加到列表中。最后,调用exit()函数时,按先进后出(先执行最后添加的函数)的顺序执行这些函数。
由atexit()注册的函数的类型应该为不接受任何参数的void函数。通常它们执行内部处理任务,如更新程序监视文件或重置环境变量。
exit()执行了atexit()指定的函数后,将做一些自身清理工作。它会刷新所有输出流、关闭所有打开的流,并关闭通过调用标准I/O函数tmpfile()创建的临时文件。然后,exit()把控制返回给主机环境(如果可能,还向主机环境报告终止状态)。习惯上,UNIX程序用0表示成功终止,用非零值表示失败。UNIX返回的代码并不适用于所有系统,因此ANSI C定义了可移植的表示失败的宏EXIT_FAILURE。与之类似,ANSI C定义EXIT_SUCCESS表示成功,但是exit()也可接受用0代表成功。
qsort()函数
快速排序算法是最有效的排序算法之一,对大型数组而言更是如此。
中心思想:它把数组不断分成更小的数组,直到变成单元素数组。首先,将数组分成两部分,其中一部分的值都小于其他部分的值。继续这个过程,直至数组完全排好序为止。C实现快速排序算法的函数名为qsort()。qsort()函数对数据对象数组进行排序,其ANSI原型为:
void qsort(void * base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));
- 第一个参数为指向要排序的数组头部的指针。ANSI C允许将任何数据类型的指针转换(类型指派)为void类型指针(即通用指针),因而qsort()的第一个实际参数可以指向任何类型的数组。
- 第二个参数为需要排序的项目数量。
- 第三个参数的作用:因为qsort()将第一个参数转换为void指针,所以会失去每个数组元素的大小信息。为补充该信息,必须把数据对象的大小明确地告诉qsort()。
- 最后,qsort()还需要一个指向函数的指针,被指向的函数用于确定排序顺序。这个比较函数应该接受两个参数,即分别指向进行比较的两个项目的指针。如果第一个项目的值大于第二个项目的值,那么比较函数返回正数;如果两个项目的值相等,那么返回0;如果第一个项目的值小于第二个项目的值,那么返回负数。
qsort()函数把进行比较的两个元素的地址传递给比较函数。
简而言之,为了通用性,qsort()和比较函数使用void指针。因此,必须把数组中每个元素的大小明确地告诉qsort();并且在比较函数的定义中,需要把指针参数转换为对具体应用而言类型正确的指针。
C和C++中的void *
C和C++对待Void类型的指针是不同的。在两种语言中,你都可以把一个指向任意类型的指针赋给类型void *。但是在把一个void *指针赋给一个指针或又一个类型的指针的时候,C++需要一次强制类型转换。而C并没有这个需要。在C中,这种强制类型转换时可选的,在C++中则是必须的。
诊断库
由头文件assert.h支持的诊断库是设计用于辅助调试程序的小型库。它由宏assert()构成,该宏接受整数表达式作为参数,如果表达式值为假(0),宏assert()向标准错误流(stderr)写一条错误信息并调用abort()函数以终止程序(在头文件中stdlib.h中定义了abort()函数的原型)。
assert()宏的作用为:标识出程序中某个条件应为真的关键位置,并在条件为假时,用assert()语句终止该程序。通常,assert()的参数为关系或逻辑表达式。如果assert()终止程序,那么它首先会显式失败的判断、包含该判断的文件名和行号。
使用assert()方式有几个好处,它能自动识别文件,并自动识别发生问题的行号。其次,还有一种无须改变代码就能开启或禁用assert()宏的机制。如果您认为已经排除了程序的漏洞,那么可以把宏定义为:
#define NDEBUG
放在assert.h包含语句所在位置前,并重新编译该程序。编译器将禁用文件中所有的assert()语句。
string.h库中的memcpy()和memmove()
注意:不能把一个数组的值赋予又一个数组,但一个情况例外:可以用strcpy()和strncpy()函数复制字符数组。memcpy()和memmove()函数为复制其他类型的数组提供了类似的便利工具。下面是这两个函数的原型:
void *memcpy(void * restrict s1, const void * restrict s2, size_t n); void *memove(void *s1, const void *s2, size_t n);
这两个函数均从s2指向的位置复制n字节数据到s1指向的位置,且均返回s1的值。两者间的差别由关键字restrict造成,即memcpy()可以假定两个内存区域之间没有重叠。memmove()函数则不作这个假定,因此,复制过程类似于首先将所有字节复制到一个临时缓冲区,然后再复制到最终目的地。
问:如果两个区域存在重叠时使用memcpy()会怎样呢?
答:其行为是不可预知的,即可能正常工作,也可能失败。在不应该使用memcpy()时,编译器不会禁止使用memcpy()。因此使用memcpy(),您必须确保没有重叠区域。
可变参数:stdarg.h
头文件stdarg.h为函数提供了接受可变个数的参数的能力。必须按以下步骤进行:
- 在函数原型中使用省略号。这类函数的原型应具有一个参量列表,参量列表中至少有一个后跟省略号的参量:
void f1(int n, ...); // 合法 int f2(int n, const char * s, ...); // 合法 char f3(char c1, ..., char a2); // 无效,省略号不是最后一个参量 double f3(); // 无效,没有任何参量
最右边的参量(省略号前)起着特殊的作用;ANSI标准使用parmN表示该参量。传递给该参量的实际参数值将是省略号部分代表的参数个数。
- 在函数定义中创建一个va_list类型的变量。在头文件中stdarg.h中声明的va_list类型代表一种数据对象,该数据对象用于存放参量列表中省略号部分代表的参量。函数定义的起始部分应像下面这样:
double sum(int lim, ...) { va_list ap; // 声明用于存放参数的变量
- 用宏将该变量初始化为一个参数列表。函数将使用stdarg.h中定义的宏va_start()把参数列表复制到va_list变量中。宏va_start()有两个参数:va_list类型的变量和参量parmN。对va_start()的调用应如下:
va_start(ap, lim); // 把ap初始化为参数列表
- 用宏访问这个参数列表。这一步涉及宏va_arg()的使用。该宏接受两个参数:一个va_list类型的变量和一个类型名。第一次调用va_arg()时,它返回参数列表的第一项,下次调用时返回第二项,以此类推。类型参数指定返回值的类型。例如:如果参数列表中第一个参数为double类型,第二个为int类型,那么可使用下列语句:
double tic; int toc; ... tic = va_arg(ap, double); // 取得第一个参数 toc = va_arg(ap, int); // 取得第二个参数
注意:实际参数的类型必须与说明的类型相匹配。
- 用宏完成清理工作。这一步应使用宏va_end()。例如,释放动态分配的用于存放参数的内存。该宏接受一个va_list变量作为参数:
va_end(ap);
此后只有使用va_start()重新对ap初始化后,才能使用变量ap。
因为va_arg()不提供后退先前参数的方法,所以保存va_list变量的副本会是有用的。C99为此专门添加了宏va_copy()。该宏的两个参数均为va_list类型变量,它将第二个参数复制到第一个参数中。
举例说明:
#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, 1.1, 2.5, 13.3): %g\n", s); printf("return value for sum(6, 1.1, 2.1, 13.1, 4.1, 5.1, 6.1): %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; }