先来回顾一下C语言固定参数个数的函数形式:
int func(int a, char b);
可见,该函数有2个参数,分别是int型变量a和char型变量b。但是为什么标准输入输出库里面的printf函数可以输入多个参数?我们找来printf函数的原型:
int printf (const char*, ...);
观察printf函数的原型,除了一个指向字符型常量地址(刚好是字符串常量的类型)的指针外,还有一个省略号,问题就出在这里。
与可变参数表相关的头文件是标准C库头文件”stdarg.h”,表示standard arguments(标准参数),找到里面几个用到的宏:
宏名称 | 描述 | 兼容 |
---|---|---|
va_list | 创建一个va_list类型变量 | C89 |
va_start(v,l) | 使va_list指向起始的参数 | C89 |
va_arg(v,l) | 检阅参数 | C89 |
va_end(v,l) | 释放va_list | C89 |
va_copy(v,s) | 拷贝va_list的内容 | C99 |
设一个可变参数表函数void func(int a, ...)
。在使用可变参数表前,必须用va_list定义一个变量(假设变量名为ap),然后调用va_start(v,l)
令ap指向该参数表的起始地址,va_start的第一个参数是va_list类型的变量ap,第二个参数是省略号前最后一个变量的变量名,初始化完成后就可以调用va_arg(v,l)
依次检索参数了。va_arg的第一个参数是va_list类型的变量ap,第二个参数是将要检索的变量的类型名(如int,其本质是在内存中截取int长即2字节的数据赋给返回值后ap指向2字节以后的地址)。在检索完所有的参数后必须调用va_end(v)
,使ap指向NULL(空指针),避免越界访问的问题。C99提供额外的宏”va_copy”,它能够复制va_list。而va_copy(v ,s)
函数作用为拷贝s到v。
写了一个例程来说明可变参数表函数的实现以及调用:
#include <stdio.h>
#include <stdarg.h>
/*用于测试可变参量表的打印函数,仅调用了putchar(int)函数*/
void print_test(char *string, ...);
/*将整数转换成字符串,value为输入的整形变量,s保存转换结果,radix = 10表示十进制,其他输出NULL*/
static char* inter2string(int value, char *s, int radix);
int main()
{
int a = -67;
char st[6];
inter2string(a, st, 10);
printf("%s\n", st);
print_test("Tell me %s teleph%cne number:-n153 1418 %d-r188-n", "your", 'o', 1234);
return 0;
}
/*--------------------------------------
examples:
print_test("%d.Sunny-r-nHello", inter);
---------------------------------------*/
void print_test(char *string, ...)
{
int a; //用来接收整形变量
char iTos[6]; //用来存放字符串型整数
char *c = string; //避免直接使用string,导致string指向的地址发生变化
const char *s; //用来指向指针常量,即字符串
va_list ap; //定义一个va_list类型变量ap
va_start(ap, string); //初始化va,使va指向string的下一个地址
while(*c != '\0')
{
if(*c == '-') //'-',自定义的转义字符起始符
{
switch(*++c)
{
case 'n':
putchar('\n');
break;
case 'r':
putchar('\r');
}
}
else if(*c == '%') //格式控制符
{
switch(*++c)
{
case 'd':
a = va_arg(ap, int); //检索变量
inter2string(a, iTos, 10);
for(s = iTos; *s != '\0'; s++)
putchar(*s);
break;
case 'c':
a = va_arg(ap, char);
putchar(a);
break;
case 's':
s = va_arg(ap, const char*);
while(*s != '\0')
putchar(*s++);
}
}
else
{
putchar(*c);
}
c++; //指向下一个地址
}
va_end(ap); //不要忘记关闭ap
}
运行结果如下:
需要注意的是:
(1) 省略号前至少要有一个有类型变量。参数存放在内存的堆栈段中,发生调用时,函数的多个参数从右至左依次入栈,是连续存放的。因此从理论上说,我们只要探测到任意一个变量的地址,并且知道其他变量的类型,通过指针移位运算,则总可以顺藤摸瓜找到其他的输入变量。如果参数表中没有具体的变量,CPU也就无法找到参数表所在的位置,更不要谈可变参数表了,所以省略号前的最后一个变量的地址再加上它所占用字节的偏移量其实相当于可变参数表的“入口地址”,用于确定参数表在内存中的存放位址。
(2) 只能有一个…,而且必须位于参数末尾。
(3) …的每个变量类型是未知的,而且变量的个数也是未知的,但是我们必须正确获取确切的变量类型及数量。不幸的是,我们没办法直接从…知道,所以参数个数可变的机制可以说是一个严重的漏洞。一个公开的做法是参考标准化输入输出函数的参数const char* string中\n \r等转义字符的前缀’\’和格式控制符%,如%d,%s,通过检查’\’后面或’%’后面的字符判断输入参数的格式。
(4) 变参表的大小并不能在编译时获取,这样就存在一个访问越界的可能性,导致后果严重的 RUNTIME ERROR。
附录:
关于函数static char* inter2string(int value, char *s, int radix)
的实现方法有很多,下面是我的思路:
static char* inter2string(int value, char *s, int radix)
{
char* tmp = s;
int count = 0, digit[5] = {0};
if(radix == 10)
{
if(value < 0)
{
value = -value;
*tmp++ = '-';
}
do {
digit[count] = value % 10 + '0';
value = value / 10;
count++;
} while(value != 0);
for(; count>0; count--)
{
*tmp = (char)digit[count - 1];
tmp++;
}
*tmp = '\0'; //补充字符串结束标志
return s;
}
else
{
*s = '\0';
return NULL;
}
}