可变参数详解

printf( )是我们在编程中避不开的函数,之前我们研究了printf( )的实现原理,初步了解了printf( )函数在库中是借用putchar( )来进行实现输出的,但是还有一个问题我们上一篇文章没有解决,那就是printf( )中的可变参数是怎么实现的呢,如何去使用可变参数完成我们自己的输出函数呢?

让我们再来看一眼printf( )的长相,在stdio.h文件中他是这样声明的

int printf(const char *format, ...)

printf( )中分为两种参数,一种是const char *format代表的固定参数,一种是...代表的就是可变参数

/* *****************************************************
* Name: my_vprintf
* fuction: Implement formatting string function
* Input: const char *fmt --  A pointer to a formatted string
         va_list ap -- Parameters of the pointer
* Output: None
* Return: None
* ****************************************************** */
static void my_vprintf(const char *fmt, va_list ap)
{
    char lead = ' ';
    int maxwidth = 0;

    for (; *fmt != '\0'; fmt++)
    {
        if (*fmt != '%')
        { //顺序查找判断,遇到%就推出,否则继续循环输出
            outc(*fmt);
            continue;
        }

        fmt++;
        if (*fmt == '0')
        { //遇到‘0’说明前导码是0
            lead = '0';
            fmt++;
        }

        while (*fmt >= '0' && *fmt <= '9')
        { //紧接着的数字是长度,算出指定长度
            maxwidth *= 10;
            maxwidth += (*fmt - '0');
            fmt++;
        }

        switch (*fmt)
        { //判断格式输出
        case 'd':
            out_num(va_arg(ap, int), 10, lead, maxwidth);
            break;
        case 'o':
            out_num(va_arg(ap, unsigned int), 8, lead, maxwidth);
            break;
        case 'u':
            out_num(va_arg(ap, unsigned int), 10, lead, maxwidth);
            break;
        case 'x':
            out_num(va_arg(ap, unsigned int), 16, lead, maxwidth);
            break;
        case 'c':
            outc(va_arg(ap, int));
            break;
        case 's':
            outs(va_arg(ap, char *));
            break;

        default:
            outc(*fmt);
            break;
        }
    }
}
/* *****************************************************
* Name: printf
* fuction: None
* Input: const char *fmt --  A pointer to a formatted string
         ...  -- Variable number of arguments
* Output: None
* Return: None
* ****************************************************** */

void printf(const char *fmt, ...)
{
    va_list ap; /* 获取输入的参数指针 */
    va_start(ap, fmt); /* 获取 */
    my_vprintf(fmt, ap);
    va_end(ap);
}

可变参数与 stdarg.h 库

首先我们来看看stdarg.h 库中有那些内容

库变量

下面是头文件 stdarg.h 中定义的变量类型:

序号变量 & 描述
1va_list 这是一个适用于 va_start()、va_arg()va_end() 这三个宏存储信息的类型。

库宏

下面是头文件 stdarg.h 中定义的宏:

序号宏 & 描述
1void va_start(va_list ap, last_arg) 这个宏初始化 ap 变量,它与 va_argva_end 宏是一起使用的。last_arg 是最后一个传递给函数的已知的固定参数,即省略号之前的参数。
2**type va_arg(va_list ap, type)**这个宏检索函数参数列表中类型为 type 的下一个参数。
3**void va_end(va_list ap)**这个宏允许使用了 va_start 宏的带有可变参数的函数返回。如果在从函数返回之前没有调用 va_end,则结果为未定义。

注意:以上的所有操作,只能从头到尾顺序访问后面的可变参数,可以暂停,但不能反向读取

库实现

/* VC++ 6.0 */
#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, t) (*(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))          //下一个参数的值
#define va_end(ap) (ap = (va_list)0)                                           // 将指针置为无效

如果将其转换成函数则是

void va_start(va_list ap, xxx v) /* 其中的 xxx 为任意类型变量 */
{
    ap = (va_list)&v + sizeof(v);
}
xxx va_arg(va_list ap, xxx t) /* 其中的 xxx 为任意类型变量 */
{
    // (*(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))  
    ap += sizeof(t);
    return *((t *)(ap - sizeof(t)))
   	
}
void va_end(va_list ap) 
{
    ap = (va_list)0; //强制转换
}

可变参数的存储与实现

以下文字引用自博客 可变参数函数详解

C调用约定下可使用va_list系列变参宏实现变参函数,此处va意为variable-argument(可变参数)。

典型用法如下:

#include <stdarg.h>

int VarArgFunc(int dwFixedArg, ...){ //以固定参数的地址为起点依次确定各变参的内存起始地址

    va_list pArgs = NULL;  //定义va_list类型的指针pArgs,用于存储参数地址
    va_start(pArgs, dwFixedArg); //初始化pArgs指针,使其指向第一个可变参数。该宏第二个参数是变参列表的前一个参数,即最后一个固定参数
    int dwVarArg = va_arg(pArgs, int); //该宏返回变参列表中的当前变参值并使pArgs指向列表中的下个变参。该宏第二个参数是要返回的当前变参类型
    //若函数有多个可变参数,则依次调用va_arg宏获取各个变参
    va_end(pArgs);  //将指针pArgs置为无效,结束变参的获取
    /* Code Block using variable arguments */

}
//可在头文件中声明函数为extern int VarArgFunc(int dwFixedArg, ...);,调用时用VarArgFunc(FixedArg, VarArg);

变参宏根据堆栈生长方向和参数入栈特点,从最靠近第一个可变参数的固定参数开始,依次获取每个可变参数的地址。

变参宏的定义和实现因操作系统、硬件平台及编译器而异(但原理相似)。

下面这四小段引用自 C语言中函数参数入栈的顺序

C程序栈底为高地址,栈顶为低地址,函数参数入栈顺序的确是从右至左的。可到底为什么呢?查了一直些文献得知,参数入栈顺序是和具体编译器实现相关的。比如,Pascal语言中参数就是从左到右入栈的,有些语言中还可以通过修饰符进行指定,如Visual C++.即然两种方式都可以,为什么C语言要选择从右至左呢?

进一步发现,Pascal语言不支持可变长参数,而C语言支持这种特色,正是这个原因使得C语言函数参数入栈顺序为从右至左。具体原因为:C方式参数入栈顺序(从右至左)的好处就是可以动态变化参数个数。通过栈堆分析可知,自左向右的入栈方式,最前面的参数被压在栈底。除非知道参数个数,否则是无法通过栈指针的相对位移求得最左边的参数。这样就变成了左边参数的个数不确定,正好和动态参数个数的方向相反。

因此,C语言函数参数采用自右向左的入栈顺序,主要原因是为了支持可变长参数形式。换句话说,如果不支持这个特色,C语言完全和Pascal一样,采用自左向右的参数入栈方式。

System V Unix在varargs.h头文件中定义va_start宏为va_start(va_list arg_ptr),而ANSI C则在stdarg.h头文件中定义va_start宏为va_start(va_list arg_ptr, prev_param)

两种宏并不兼容,为便于程序移植通常采用ANSI C定义。

gcc编译器使用内置宏间接实现变参宏,如#define va_start(v,l) __builtin_va_start(v,l)。因为gcc编译器需要考虑跨平台处理,而其实现因平台而异。例如x86-64或PowerPC处理器下,参数不全都通过堆栈传递,变参宏的实现相比x86处理器更为复杂。

x86平台VC6.0编译器中,stdarg.h头文件内变参宏定义如下:

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 )

各宏的含义如下:

  1. _INTSIZEOF宏考虑到某些系统需要内存地址对齐。从宏名看应按照sizeof(int)即堆栈粒度对齐,即参数在内存中的地址均为sizeof(int)=4的倍数。例如,若在1≤sizeof(n)≤4,则_INTSIZEOF(n)=4;若5≤sizeof(n)≤8,则_INTSIZEOF(n)=8。为便于理解,简化该宏为

#define _INTSIZEOF(n) ((sizeof(n) + x) & ~(x))x = sizeof(int) - 1 = 3 = 0b’0000 0000 0000 0011~x = 0b’1111 1111 1111 1100

一个数与(~x)相与的结果是sizeof(int)的倍数,即_INTSIZEOF(n)将n圆整为sizeof(int)的倍数。

  1. va_start宏根据(va_list)&v得到第一个可变参数前的一个固定参数在堆栈中的内存地址,加上_INTSIZEOF(v)即v所占内存大小后,使ap指向固定参数后下个参数(第一个可变参数地址)。

    固定参数的地址用于va_start宏,因此不能声明为寄存器变量(地址无效)或作为数组类型(长度难定)。

  2. va_arg宏取得type类型的可变参数值。首先ap+=_INTSIZEOF(type),即ap跳过当前可变参数而指向下个变参的地址;然后ap-_INTSIZEOF(type)得到当前变参的内存地址,类型转换后返回当前变参值。

    va_arg宏的等效实现,如下将指针移动至下个变参,并返回左移的值[-1](数组下标表示偏移量),即当前变参值#define va_arg(ap,type) ((type *)((ap) += _INTSIZEOF(type)))

  3. va_end宏使ap不再指向有效的内存地址。该宏的某些实现定义为((void*)0),编译时不会为其产生代码,调用与否并无区别。但某些实现中va_end宏用于函数返回前完成一些必要的清理工作:如va_start宏可能以某种方式修改堆栈,导致返回操作无法完成,va_end宏可将有关修改复原;又如va_start宏可能对参数列表动态分配内存以便于遍历va_list,va_end宏可释放此前动态分配的内存。因此,从使用va_start宏的函数中退出之前,必须调用一次va_end宏。函数内可多次遍历可变参数,但每次必须以va_start宏开始,因为遍历后ap指针不再指向首个变参。

下图给出基于变参宏的可变参数在堆栈中的分布:

img

变参宏无法智能识别可变参数的数目和类型因此实现变参函数时需自行判断可变参数的数目和类型

前者可显式提供变参数目或设定遍历结束条件(如-1、'\0’或回车符等)。

后者可显式提供变参类型枚举值,或在固定参数中包含足够的类型信息(如printf函数通过分析format字符串即可确定各变参类型),甚至主调函数和被调函数可约定变参的类型组织等。

都看到这里了,如果有帮助,点个赞👍👍👍再走呗!
点个赞,代码没有Bug呦~

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐