VA_LIST可变参数列表的使用方法与原理

VA_LIST可变参数列表的使用方法与原理

va_list是在C语言中解决变参问题的定义的一个类型,常在 va_start(), va_arg(), and va_end() 宏中使用。变参问题是指参数的个数不定,可以是传入一个参数也可以是多个。可变参数中的每个参数的类型可以不同,也可以相同;可变参数的每个参数并没有实际的名称与之相对应,用起来是很灵活。

0x01 可变参数列表定义与使用

在Linux终端下输入 man 3 stdarg

查看va_list和va_start, va_arg, va_end相关宏的声明

STDARG(3)                                                                        Linux Programmer's Manual                                                                       STDARG(3)

NAME
       stdarg, va_start, va_arg, va_end, va_copy - variable argument lists

SYNOPSIS
       #include 

       void va_start(va_list ap, last);
       type va_arg(va_list ap, type);
       void va_end(va_list ap);
       void va_copy(va_list dest, va_list src);
va_arg 访问下一个可变参数函数参数(函数宏)
va_end 结束可变参数函数参数的遍历(函数宏)
va_list 保存va_start,va_arg,va_end和va_copy(typedef)所需的信息
va_start 允许访问可变参数函数参数(函数宏)

下面展示一段可变参数列表在Redis中的使用:

函数功能是将可变参数fmt拼接到简单动态字符串sds后面

/* This function is similar to sdscatprintf, but much faster as it does
 * not rely on sprintf() family functions implemented by the libc that
 * are often very slow. Moreover directly handling the sds string as
 * new data is concatenated provides a performance improvement.
 *
 * However this function only handles an incompatible subset of printf-alike
 * format specifiers:
 *
 * %s - C String
 * %S - SDS string
 * %i - signed int
 * %I - 64 bit signed integer (long long, int64_t)
 * %u - unsigned int
 * %U - 64 bit unsigned integer (unsigned long long, uint64_t)
 * %% - Verbatim "%" character.
 */
typedef char *sds;//指向存储数据的起始地址

sds sdscatfmt(sds s, char const *fmt, ...) {
    size_t initlen = sdslen(s);
    const char *f = fmt;
    long i;
    va_list ap;

    va_start(ap,fmt);
    f = fmt;    /* Next format specifier byte to process. */
    i = initlen; /* Position of the next byte to write to dest str. */
    while(*f) {
        char next, *str;
        size_t l;
        long long num;
        unsigned long long unum;

        /* Make sure there is always space for at least 1 char. */
        if (sdsavail(s)==0) {
            s = sdsMakeRoomFor(s,1);
        }

        switch(*f) {
        case '%':
            next = *(f+1);
            f++;
            switch(next) {
            case 's':
            case 'S':
                str = va_arg(ap,char*);
                l = (next == 's') ? strlen(str) : sdslen(str);
                if (sdsavail(s) < l) {
                    s = sdsMakeRoomFor(s,l);
                }
                memcpy(s+i,str,l);
                sdsinclen(s,l);
                i += l;
                break;
            case 'i':
            case 'I':
                if (next == 'i')
                    num = va_arg(ap,int);
                else
                    num = va_arg(ap,long long);
                {
                    char buf[SDS_LLSTR_SIZE];
                    l = sdsll2str(buf,num);
                    if (sdsavail(s) < l) {
                        s = sdsMakeRoomFor(s,l);
                    }
                    memcpy(s+i,buf,l);
                    sdsinclen(s,l);
                    i += l;
                }
                break;
            case 'u':
            case 'U':
                if (next == 'u')
                    unum = va_arg(ap,unsigned int);
                else
                    unum = va_arg(ap,unsigned long long);
                {
                    char buf[SDS_LLSTR_SIZE];
                    l = sdsull2str(buf,unum);
                    if (sdsavail(s) < l) {
                        s = sdsMakeRoomFor(s,l);
                    }
                    memcpy(s+i,buf,l);
                    sdsinclen(s,l);
                    i += l;
                }
                break;
            default: /* Handle %% and generally %. */
                s[i++] = next;
                sdsinclen(s,1);
                break;
            }
            break;
        default:
            s[i++] = *f;
            sdsinclen(s,1);
            break;
        }
        f++;
    }
    va_end(ap);

    /* Add null-term */
    s[i] = '\0';
    return s;
}

可变参数列表的用法:

(1)首先在函数里定义一具va_list型的变量,这个变量是指向参数的指针;
(2)然后用va_start宏初始化变量刚定义的va_list变量;
(3)然后用va_arg返回可变的参数,va_arg的第二个参数是你要返回的参数的类型(如果函数有多个可变参数的,依次调用va_arg获取各个参数);
(4)最后用va_end宏结束可变参数的获取。

上面是可变参数列表的具体用法,下面讲解一下可变参数列表中出现几个宏的定义:下面的实现摘取至VS2015的X86平台定义。

#define va_start __crt_va_start
#define va_arg   __crt_va_arg
#define va_end   __crt_va_end
#define va_copy(destination, source) ((destination) = (source))

#ifdef _M_IX86
	//为了满足需要内存对齐的系统
	#define _INTSIZEOF(n)          ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
	//ap指向第一个变参的位置,即将第一个变参的地址赋予ap
	#define __crt_va_start_a(ap, v) ((void)(ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v)))
	//获取变参的具体内容,t为变参的类型,如有多个参数,则通过移动ap的指针来获得变参的地址,从而获得内容
	#define __crt_va_arg(ap, t)     (*(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))
	//清空va_list,即结束变参的获取
	#define __crt_va_end(ap)        ((void)(ap = (va_list)0)) 
#endif

#ifdef __cplusplus
    #define _ADDRESSOF(v) (&const_cast(reinterpret_cast(v)))
#else
    #define _ADDRESSOF(v) (&(v))
#endif

C语言的函数形参是从右向左压入堆栈的,以保证栈顶是第一个参数,而且x86平台内存分配顺序是从高地址到低地址。

/*******************************************************************
*
*	@author: erice_s
*	@date:   20191001
*	@email: [email protected]
*   
*******************************************************************/
#include 
#include 
#include 
 
// 计算标准差
double sample_stddev(int count, ...) 
{
    /* Compute the mean with args1. */
    double sum = 0;
    va_list args1;
    va_start(args1, count);
    va_list args2;
    va_copy(args2, args1);   /* copy va_list object */
    for (int i = 0; i < count; ++i) {
        double num = va_arg(args1, double);
        sum += num;
    }
    va_end(args1);
    double mean = sum / count;
 
    /* Compute standard deviation with args2 and mean. */
    double sum_sq_diff = 0;
    for (int i = 0; i < count; ++i) {
        double num = va_arg(args2, double);
        sum_sq_diff += (num-mean) * (num-mean);
    }
    va_end(args2);
    return sqrt(sum_sq_diff / count);
}
 
int main(void) 
{
    printf("%f\n", sample_stddev(4, 25.0, 27.3, 26.9, 25.7));
}

内存分配顺序是从高地址到低地址。因此似函数sample_stddev(int count, T var1, T var2 …) 内存分配大致上是这样的:(可变参数在中间)

栈区:

栈顶 低地址
第一个参数count &args1
第二个参数var1 va_start(args1, count)后args1指向地址
最后一个参数var
函数的返回地址
栈底 高地址

使用可变参数列表应该注意的问题:
(1)因为va_start, va_arg, va_end等定义成宏,所以它显得很愚蠢,可变参数的类型和个数完全在该函数中由程序代码控制,它并不能智能地识别不同参数的个数和类型.。也就是说,你想实现智能识别可变参数的话是要通过在自己的程序里作判断来实现的.
(2)另外有一个问题:因为编译器对可变参数的函数的原型检查不够严格,对编程查错不利,不利于我们写出高质量的代码。
(3)由于参数的地址用于va_start宏,所以参数不能声明为寄存器变量,或作为函数或数组类型。


0x02 内存对齐代码的分析

//为了满足需要内存对齐的系统
#define _INTSIZEOF(n)          ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))	

_INTSIZEOF(n)整个做的事情就是将n的长度化为int长度的整数倍。

例如n为13,二进制为1101b, int长度为4,二进制为100b, 那么n化为int长度的整数倍就应该为16。

~(sizeof(int) – 1) )就应该为(4-1)=(00000011b)=11111100b,这样任何数& ~(sizeof(int) – 1) )后最后两位肯定为0,就肯定是4的整数倍。

(sizeof(n) + sizeof(int) – 1)就是将大于4m但小于等于4(m+1)的数提高到大于等于4(m+1)但小于4(m+2),这样再& ~(sizeof(int) – 1) )后就正好将原长度补齐到4的倍数

当M = power(2,Y) 时 (N >>Y) << Y = (N &(~(M-1))也是一个恒等式

注意:

(1)这里最关键的一点就是M必须是2的幂(有人常常理解成2的倍数也可以,那是不对的),否则上面的结论是不成立的.
(2) ~(M-1)更专业的叫法就是掩码(mask)。因为数字和这个掩码进行与运算后,数字的最右边Y位的数字被置0("掩抹"掉了).即掩码最右边的0有多少位,数字最右边就有多少位被清0。

小结:

  • 字节对齐的数学本质就是数论中的取模运算。在计算机上的含义就是求出一个对象占用的机器字数目。
  • 在数学上看内存计算的过程就是先右移再左移相同的位数,以得到箱子的最大容量。
  • 在c中/运算可以用位运算和掩码来实现以加快速度(省掉了求位数的过程),前提是机器字长度必须为2的幂。

你可能感兴趣的:(C/C++,可变参数列表,va_list,va_start,va_arg)