printf 与可变参数函数

如果在调用函数前不确定参数的个数,那么就常常会用到可变参数的函数设计方法,比如 C 中经常用到的 printf 就是可变参数函数的一个典型例子。

printf 的大致设计原理

在 C/C++ 中,对函数参数的扫描是从后往前的,通过压入栈来实现参数传递。也就是说最后压入的参数(函数的第一个参数)最先被系统找到。

printf 的第一个参数就是那个字符指针,双引号中的部分。然后通过判断字符串里的控制参数的个数与类型控制字符来判断参数的个数以及数据类型。通过这些就可以得到数据需要的栈指针偏移量。

可变参数设计

我们要定义可变参数函数,需要使用到 <stdarg.h> 提供的一套机制,按照相关定义的方式工作。

<stdarg.h> 中提供了一个数据类型 va-list 和三个宏 va-start, va-arg, va-end。下面是在 C 中的源码:

typedef char *  va_list;

#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )

#define va_arg(ap,type) ( *(type *)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)) )

#define va_end(ap) ( ap = (va_list)0 )

va-list 是一个 char 类型的指针,使用可变参数时声明一个 va-list 型的变量,该变量用来指向 va-arg 和 va-end 所需要的信息。

void va-start(va-list ap,lastfix) 是一个宏。它使 va-list 类型变量 ap 指向被传递给函数的可变参数表中的第一个参数,在第一次调用 va-arg 和 va-end 之前,必须首先调用该宏。va-start 的第二个参数 lastfix 是传递给被调用函数的最后一个固定参数的标识符。va-start 使 ap 只指向 lastfix 之外的可变参数表中的第一个参数,很明显它先得到第一个参数内存地址,然后又加上这个参数的内存大小,就是下个参数的内存地址了。

type va-arg(va-list ap,type) 也是一个宏,其使用有双重目的,第一个是返回 ap 所指对象的值,第二个是修改参数指针 ap 使其增加以指向表中下一个参数。va-arg 的第二个参数提供了修改参数指针所必需的信息。在第一次使用 va-arg 时,它返回可变参数表中的第一个参数,后续的调用都返回表中的下一个参数,在使用 va-arg 时,要注意第二个参数所用类型名应与传递到堆栈的参数的字节数对应,以保证能对不同类型的可变参数进行正确地寻址,比如实参依次为 char 型、char 型、int 型和float型时,在 va-arg 中它们的类型则应分别为int、char 、int和double.

void va-end(va-list ap) 也是一个宏,该宏用于被调用函数完成正常返回,功能就是把指针 ap 赋值为 0,使它不指向内存的变量。 va-end 必须在 va-arg 读完所有参数后再调用,否则会产生意想不到的后果。特别地,当可变参数表函数在程序执行过程中不止一次被调用时,在函数体每次处理完可变参数表之后必须调用一次 va-end,以保证正确地恢复栈.

下面举一个可变参数函数的例子:

#include<iostream>
using namespace std;
#include<stdarg.h>

int sum(int n,...)
{
int i , sum = 0;
va_list vap;
va_start(vap , n); //指向可变参数表中的第一个参数
for(i = 0 ; i < n ; ++i)
sum += va_arg(vap , int); //取出可变参数表中的参数,并修改参数指针vap使其增加以指向表中下一个参数
va_end(vap); //把指针vap赋值为0
return sum;
}
int main(void)
{
int m = sum(3 , 45 , 89 , 72);
cout<<m<<endl;
return 0;
}