0%

C语言printf变长参数如何实现

printf函数

函数原型:int printf(const char *format, ...)
调用格式为:printf("<格式化字符串>", <参量表>);
功能:发送格式化输出到标准输出 stdout

变长参数实现思路

C语言支持变长参数函数(Variable Argument Functions),即参数的个数可以是不定个,在函数定义的时候用...表示。采用这种形式定义的变长参数函数,至少需要一个普通的形参,且...需要放在最后一个参数,比如printf函数中的*format后面的...是函数原型的一部分。

变长参数的实现得益于C语言默认的cdecl调用惯例,其参数是从右向左压入栈的,第一个参数位于栈顶。这样printf函数实现的时候,就无需关心调用他的函数会传递几个参数过来,而只要关心自己用到几个,将全部参数压入栈,函数处理时从栈中取即可。

自己实现一个变长参数的函数

C已经有现成可用的一些东西来帮我们实现变长参数,它主要通过stdarg.h头文件定义的一个变量类型(va_list)和三个宏(va_start、va_arg、va_end)来实现。

实现一个可变长参数的sum函数,第一个参数num传递变长参数中有参数的数量,紧接着后面会传递num个整型变量,具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int sum(int num, ...)
{
int i, val = 0;
va_list ap; //定义一个具有va_list型的变量,这个变量是指向参数的指针
va_start(ap, num); //始化变量刚定义的va_list变量,使其指向第一个可变参数的地址,地址自动增加
for(i = 0; i < num; i++)
{
val += va_arg(ap, int); //va_arg返回va_list中的参数,并增加指针偏移
}
va_end(ap); //结束可变参数列表
return val;
}
void main()
{
printf("16+38+53=%d\n", sum(3, 16, 38, 53));
}

变长参数实现原理

上面的sum函数也可以不使用va_list等宏,通过其他方法实现。
当我们调用:int n = sum(3, 16, 38, 53);
参数在栈上会形成如下布局:
img

在函数内部,函数可以使用变量num来访问数字3,但无法使用任何名称访问其他的几个不定参数。但此时由于栈上其他的几个参数实际恰好依序排列在参数num的高地址方向,因此可以很简单地通过num的地址计算出其他参数的地址,sum函数的另一种实现如下:

1
2
3
4
5
6
7
8
int sum(int num, ...)
{
int* p = &num + 1;
int ret = 0;
while(num--)
ret += *p++;
return ret;
}

printf的不定参数比sum要复杂得多,因为printf的参数不仅数量不定,而且类型也不定。所以printf需要在格式字符串中注明参数的类型,例如用%d表明是一个整数。printf里的格式字符串如果将类型描述错误,因为不同参数的大小不同,不仅可能导致这个参数的输出错误,还有可能导致其后的一系列参数错误。[摘自《程序员的自我修养——链接、封装、库》P338]

printf("%lf\t%d\t%c\n", 1, 666, 'a'); 在这行函数里,printf的第一个输出参数是一个int(4 字节),而我们告诉printf它是一个double(8字节),因此printf的输出会错误,由于printf在读取double的时候实际造成了越界,因此后面几个参数的输出也会失败。该程序的实际输出为:0.000000 97(根据实际编译器和环境可能不同)

va_list等宏如何实现

va_list 实际是一个指针,用来指向各个不定参数。由于类型不明,因此这个 va_list 以 void* 或 char* 为最佳选择。
va_start 将 va_list 定义的指针指向函数的最后一个参数后面的位置,这个位置就是第一个不定参数。
va_arg 获取当前不定参数的值,并根据当前不定参数的大小将指针移向下一个参数。
va_end 将指针清 0。
按照以上思路,va_list等宏的一个最简单的实现就可以得到了,如下所示:

1
2
3
4
#define va_list char*
#define va_start(ap, arg) (ap=(va_list)&arg+sizeof(arg))
#define va_arg(ap, t) (*(t*)((ap+=sizeof(t))-sizeof(t)))
#define va_end(ap) (ap=(va_list)0)

注意:实际代码中还套了很多宏,不同编译器,不同架构都有可能使用不同的代码实现,但具体实现思想一致,有些x64条件编译时va_list会是一个结构体,里面会记录可变参数开始地址、结束地址、参数数量等信息。