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)NAMEstdarg, va_start, va_arg, va_end, va_copy - variable argument listsSYNOPSIS#include <stdarg.h>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);elsenum = 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);elseunum = 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 %<unknown>. */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<char&>(reinterpret_cast<const volatile char&>(v)))
#else#define _ADDRESSOF(v) (&(v))
#endif

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

/*******************************************************************
*
*   @author: erice_s
*   @date:   20191001
*   @email: binbin_erices@163.com
*
*******************************************************************/
#include <stdio.h>
#include <stdarg.h>
#include <math.h>// 计算标准差
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的幂。

VA_LIST可变参数列表的使用方法与原理相关推荐

  1. java 可变参数列表_java中可变参数列表的实现方法

    我们在对可变参数有一定的认识后,可以引申一下它的使用范围.在数组中也会需要参数的传入,那么结合参数的数量不固定,我们在参数类型上也得到了增加,这就是本篇所要讲的可变参数列表.下面我们就java可变参数 ...

  2. php函数可变参数列表,PHP函数可变参数列表的具体实现方法介绍

    也许对于PHP初级程序员来说,对于PHP函数并不能完全熟练的掌握.我们今天为大家介绍的PHP函数可变参数列表的实现方法主要是利用func_get_args(). func_num_args().fun ...

  3. 【C语言】可变参数列表

    文章目录 前言 一.可变参数列表是什么? 二.怎么用可变参数列表 三.对于宏的深度剖析 隐式类型转换 对两个函数的重新认知 总结 前言 可变参数列表,使用起来像是数组,学习过函数栈帧的话可以发现实际上 ...

  4. java基础(九) 可变参数列表介绍

    一.可变参数简介 在不确定参数的个数时,可以使用可变的参数列表. 1. 语法: 参数类型-(三个点) 例如: void printArray(Object...) 注意: 每个方法最多只有一个可变参数 ...

  5. c语言理解参数,c语言中对可变参数列表的简单理解

    函数原型中一般情况下参数的数目是固定的,但是如果想在不同的时候接收不定数目的参数时该怎么办呢?c语言提供了可变参数列表来实现. 可变参数列表是通过宏来实现的,这些宏定义在stdarg.h的头文件中.头 ...

  6. C语言中可变参数列表

    该博文为原创文章,未经博主同意不得转载,如同意转载请注明博文出处 本文章博客地址:https://cplusplus.blog.csdn.net/article/details/105113526 可 ...

  7. 《C++ Primer 第五版》(第6.1~6.3节) 函数形参和实参传递,可变参数列表和函数返回值

    1.函数形参和实参传递问题 函数参数传递有两种:值传递(变量,指针),引用传递(使用别名). 在形参和实参的传递过程中,牵涉到大的类类型对象.容器类型对象或者不支持拷贝操作的对象时,不适合采用值传递, ...

  8. c语言可变参数 printf,c语言 使用可变参数列表实现printf(my_printf)

    //使用可变参数列表实现print("s\t c\n","bit-tech",'w'); #include #include void int_to_char( ...

  9. c语言中的函数可变参数列表相关的三个宏

    在stdarg.h头文件中声明了一个类型va_list和3个与函数可变参数列表有关的宏:va_start.va_arg.va_end. #include<stdarg.h> //包含宏相关 ...

  10. java 可变参数列表 数组_java可变参数列表如何填充数组?

    在对于数组的填充上,我们可以运行参数的传递,把数组作为可变参数的列表进行调整.对于一些参数个数和类型未知的时候,这种方法就能帮助我们解决填充数组的难题,因为这种方法并不需要提前知道.下面我们就可变参数 ...

最新文章

  1. sap Status状态栏设计
  2. 推荐10款非常有用的 Ajax 插件
  3. springboot 支付宝电脑支付
  4. python列表元素循环左移_JavaScript系列——数组元素左右移动N位算法实现
  5. 电大计算机网络本作业1,2017年最新电大计算机网络作业1-3答案.doc
  6. 卷积,DFT,FFT,图像FFT,FIR 和 IIR 的物理意义。
  7. 一文带你斩杀Python之Numpy☀️Pandas全部操作【全网最详细】❗❗❗
  8. iOS蓝牙开发(一)蓝牙相关基础知识
  9. 佳能Canon imageCLASS MF236n 一体机驱动
  10. postgresql12的同步流复制搭建及主库hang问题处理和分析
  11. python chunk模块
  12. wpf Route Event Code Snippet
  13. 计算关联系数matlab,matlab相关系数计算公式
  14. PLETL的主谓宾 定状补模式 命令 已经设计更新并执行成功如图 中节点 的m_ID 输出已经出现 ,已经开源
  15. 第八届中国大学计算机设计大赛,2015年(第八届)中国大学生计算机设计大赛.PDF...
  16. 双臂模式DPVS+RPM安装教程
  17. google地图测距原码
  18. excel数据透视表中插入一列新数据
  19. 《自学是门手艺活》读后感
  20. 23个机器学习最佳入门项目!(附数据+源代码)

热门文章

  1. 模拟量万能换算公式4-20ma
  2. signature=9aadee6a3f882c84134bf5f6f04d2c93,Fw: Updated Scor Input Requirements
  3. php 跨站脚本攻击漏洞,PHP跨站脚本攻击(XSS)漏洞修复思路(二)
  4. 荣耀V20是起点,一波炫酷荣耀潮配又来啦!
  5. java中编写一个三角形类,java 三角形类Triangle java 三角形类 Triangle的用法详解
  6. git Untracked files
  7. 如何用分布式Pollard-Rho法对椭圆曲线离散对数问题(ECDLP)进行攻击(下)
  8. 如何设置 ASP.NET Core 程序监听的 IP 和端口
  9. TIM2_CH1_ETR可以当做TIM2_CH1来用
  10. #MoreThanCode:社会正义技术