C/C++支持可变参数个数的函数定义,这一点与C/C++语言函数参数调用时入栈顺序有关,
首先引用其他网友的一段文字,来描述函数调用,及参数入栈:

------------ 引用开始 ------------
C支持可变参数的函数,这里的意思是C支持函数带有可变数量的参数,最常见的例子就
是我们十分熟悉的printf()系列函数。我们还知道在函数调用时参数是自右向左压栈的
。如果可变参数函数的一般形式是:
    f(p1, p2, p3, …)
那么参数进栈(以及出栈)的顺序是:
    …
    push p3
    push p2
    push p1
    call f
    pop p1
    pop p2
    pop p3
    …
我可以得到这样一个结论:如果支持可变参数的函数,那么参数进栈的顺序几乎必然是
自右向左的。并且,参数出栈也不能由函数自己完成,而应该由调用者完成。

这个结论的后半部分是不难理解的,因为函数自身不知道调用者传入了多少参数,但是
调用者知道,所以调用者应该负责将所有参数出栈。

在可变参数函数的一般形式中,左边是已经确定的参数,右边省略号代表未知参数部分
。对于已经确定的参数,它在栈上的位置也必须是确定的。否则意味着已经确定的参数
是不能定位和找到的,这样是无法保证函数正确执行的。衡量参数在栈上的位置,就是
离开确切的函数调用点(call f)有多远。已经确定的参数,它在栈上的位置,不应该
依赖参数的具体数量,因为参数的数量是未知的!

所以,选择只能是,已经确定的参数,离开函数调用点有确定的距离(较近)。满足这
个条件,只有参数入栈遵从自右向左规则。也就是说,左边确定的参数后入栈,离函数
调用点有确定的距离(最左边的参数最后入栈,离函数调用点最近)。

这样,当函数开始执行后,它能找到所有已经确定的参数。根据函数自己的逻辑,它负
责寻找和解释后面可变的参数(在离开调用点较远的地方),通常这依赖于已经确定的
参数的值(典型的如prinf()函数的格式解释,遗憾的是这样的方式具有脆弱性)。

据说在pascal中参数是自左向右压栈的,与C的相反。对于pascal这种只支持固定参数函
数的语言,它没有可变参数带来的问题。因此,它选择哪种参数进栈方式都是可以的。
甚至,其参数出栈是由函数自己完成的,而不是调用者,因为函数的参数的类型和数量
是完全已知的。这种方式比采用C的方式的效率更好,因为占用更少的代码量(在C中,
函数每次调用的地方,都生成了参数出栈代码)。

C++为了兼容C,所以仍然支持函数带有可变的参数。但是在C++中更好的选择常常是函数
重载。
------------ 引用结束 ------------

根据上文描述,我们查看printf()及sprintf()等函数的定义,可以验证这一点:
_CRTIMP int __cdecl printf(const char *, ...);
_CRTIMP int __cdecl sprintf(char *, const char *, ...);

这两个函数定义时,都使用了__cdecl关键字,__cdecl关键字约定函数调用的规则是:
调用者负责清除调用堆栈,参数通过堆栈传递,入栈顺序是从右到左。

下一步,我们来看看printf()这种函数是如何使用变个数参数的,下面是摘录MSDN上的例子,
只引用了ANSI系统兼容部分的代码,UNIX系统的代码请直接参考MSDN。

------------ 例子代码 ------------
#include <stdio.h>
#include <stdarg.h>
int average( int first, ... );

void main( void )
{
   printf( "Average is: %d/n", average( 2, 3, 4, -1 ) );
}

int average( int first, ... )
{
   int count = 0, sum = 0, i = first;
   va_list marker;

va_start( marker, first );     /* Initialize variable arguments. */
   while( i != -1 )
   {
      sum += i;
      count++;
      i = va_arg( marker, int);
   }
   va_end( marker );              /* Reset variable arguments.      */
   return( sum ? (sum / count) : 0 );
}
------------ 代码结束 ------------

上例代码功能是计算平均数,函数允许用户输入多个整型参数,要求作后一个参数必须
是-1,表示参数输入完毕,然后返回平均数计算结果。

逻辑很简单,首先定义
   va_list marker;
表示参数列表,然后调用va_start()初始化参数列表。注意va_start()调用时不仅使用了marker
这个参数列表变量,还使用了first这个参数,说明参数列表的初始化与函数给定的第一个
确定参数是有关系的,这一点很关键,后续分析会看到原因。

调用va_start()初始化后,即可调用va_arg()函数访问每一个参数列表中的参数了。注意va_arg()
的第二个参数指定了返回值的类型(int)。

当程序确定所有参数访问结束后,调用va_end()函数结束参数列表访问。

这样看起来,访问变个数参数是很容易的,也就是使用va_list,va_start(),va_arg(),va_end()
这样一个类型与三个函数。但是对于函数变个数参数的机制,感觉仍是一头雾水。看来需要
继续深入探究,才能的到确切的答案了。

找到va_list,va_start(),va_arg(),va_end()的定义,在.../VC98/include/stdarg.h文件中。
.h中代码如下(只摘录了ANSI兼容部分的代码,UNIX等其他系统实现略有不同,感兴趣的朋友可以
自己研究):

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,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap)      ( ap = (va_list)0 )

从代码可以看出,va_list只是一个类型转义,其实就是定义成char*类型的指针了,这样就是为了
以字节为单位访问内存。
其他三个函数其实只是三个宏定义,且慢,我们先看夹在中间的这个宏定义_INTSIZEOF:

#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )

这个宏的功能是对给定变量或者类型n,计算其按整型字节长度进行字节对齐后的长度(size)。在32位系统中
int占4个字节,16位系统中占2字节。
表达式
 (sizeof(n) + sizeof(int) - 1)
的作用是,如果sizeof(n)小于sizeof(int),则计算后
的结果数值,会比sizeof(n)的值在二进制上向左进一位。
如:sizeof(short) + sizeof(n) - 1 = 5
5的二进制是0x00000101,sizeof(short)的二进制是0x00000010,所以5的二进制值比2的二进制值
向左高一位。
表达式
 ~(sizeof(int) - 1)
的作用是生成一个蒙版(mask),以便舍去前面那个计算值的"零头"部分。
如上例,~(sizeof(int) - 1) = 0xFFFFFFFC
同5的二进制0x00000101做"与"运算得到的是0x00000100,也就是4,而直接计算sizeof(short)应该得到2。
这样通过_INTSIZEOF(short)这样的表达式,就可以得到按照整型字节长度对齐的其他类型字节长度。
之所以采用int类型的字节长度进行对齐,是因为C/C++中的指针变量其实就是整型数值,长度与int相同,
而指针的偏移量是后面的三个宏进行运算时所需要的。

关于编程中字节对齐的内容请有兴趣的朋友到网上参考其他文章,这里不再赘述。

继续,下面这个三个宏定义:

第一:
#define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF(v) )

编程中这样使用
   va_list marker;
   va_start( marker, first );
可以看出va_start宏的作用是使给定的参数列表指针(marker),根据第一个确定参数(first)所属类型的
指针长度向后偏移相应位置,计算这个偏移的时候就用到了前面的_INTSIZEOF(n)宏。

第二:
#define va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )

此处乍一看有点费解,(ap += _INTSIZEOF(t)) - _INTSIZEOF(t)表达式的一加一减,对返回值是不起作用
的啊,也就是返回值都是ap的值,什么原因呢?
原来这个计算返回值是一方面,另一方面,请记住,va_start(),va_arg(),va_end这三个宏的调用是有关联
性的,ap这个变量是调用va_start()时给定的参数列表指针,所以

(ap += _INTSIZEOF(t)) - _INTSIZEOF(t)

表达式不仅仅是为了返回当前指向的参数的地址,还是为了让ap指向下一个参数(注意ap跳向下一参数是,
是按照类型t的_INTSIZEOF长度进行计算的)。

第三:
#define va_end(ap)      ( ap = (va_list)0 )

这个很好理解了,不过是将ap指针置为空,算作参数读取结束。

至此,C/C++变个数函数参数的机制已经很清晰了。最后还要说一点要注意的问题:
在用va_arg()顺序跳转指针读取参数的过程中,并没有方法去判断所得到的下一个指针是否是有效地址,也
没有地方能够明确得知到底要读取多少个参数,这就是这种变个数参数的危险所在。前面的求平均数的例子
中,要求输入者必须在参数列表最后提供一个特殊值(-1)来表示参数列表结束,所以可以假设,万一调用
者没有遵循这种规则,将导致指针访问越界。

那么,可能有朋友会问,printf()函数就没有提供这样的特殊值进行标识啊。

别急,printf()使用的是另一种参数个数识别方式,可能比较隐蔽。注意他的第一个确定参数,也就是被我
们用作格式控制的format字符串,他的里面有"%d","%s"这样的参数描述符,printf()函数在解析format字符
串时,可以根据参数描述符的个数,确定需要读取后面几个参数。我们不妨做下面这样的试验:

printf("%d,%d,%d,%d/n",1,2,3,4,5);
 
实际提供的参数多于前面给定的参数描述符,这样执行的结果是

1,2,3,4

也就是printf()根据format字符串认为后面只有4个参数,其他的就不管了。那么再做一个试验:

printf("%d,%d,%d,%d/n",1,2,3);

实际提供的参数少于给定的参数描述符,这样执行的结果是(如果没有异常的话)

1,2,3,2367460

这个地方,每个人的执行结果可能都不相同,原因是读取最后一个参数的指针已经指向了非法的地址。这也是
使用printf()这类函数需要特别注意的地方。

总结:
变个数的函数参数在使用时需要注意的地方比较多。我个人建议尽量回避使用这种模式。比如前面的计算平均
数,宁可使用数组或其他列表作为参数将一系列数值传递给函数,也不用写这样的变态函数。一方面是容易出
现指针访问越界,另一方面,在实际的函数调用时,要把所有计算值依次作为参数写在代码里,很龌龊。
虽然这么说,但有些地方这个功能还是很有用处的,比如字符串的格式化合成,像printf()函数;在实际应用
中,我还经常使用一个自己写的WriteLog()函数,用于记录文件日志,定义与printf()相同,使用起来非常灵
活便利,如:

WriteLog("用户%s, 登录次数%d","guanzhong",10);
 
写在文件里的内容就是

用户guanzhong, 登录次数10
 
编程语言的使用,在遵循基本规则的前提下,是仁者见仁,智者见智。总之,透彻了解之后,选择一个符合自
己的好的习惯即可。

探究C/C++可变参数相关推荐

  1. x64 可变参数原理完全解析

    问题引子 众所周知,在 X64 下,函数参数都是尽量通过寄存器来传递的(浮点数使用专用的浮点数寄存器 xmm, 其他参数使用通用寄存器),只有 abi 里规定的这些寄存器用完之后,才使用栈来传递参数. ...

  2. c语言va_start函数,va_start和va_end,以及c语言中的可变参数原理

    FROM:http://www.cnblogs.com/hanyonglu/archive/2011/05/07/2039916.html 本文主要介绍va_start和va_end的使用及原理. 在 ...

  3. 浅谈C#可变参数params

    前言 前几天在群里看到群友写了一个基础框架,其中涉及到关于同一个词语可以添加多个近义词的一个场景.当时群友的设计是类似字典的设计,直接添加k-v的操作,本人看到后思考了一下觉得使用c#中的params ...

  4. Python 函数的可变参数(*paramter与**paramter)的使用

    Python 函数的可变参数主要有 *paramter与**paramter 可变参数主要有 *paramter的作用 接受任意多个实际参数并放到一个元组中 def people(*people):f ...

  5. c语言 可变参数的宏,可变参数的宏__ VA_ARGS__的用法

    回顾 在[ANSIC几种特殊的标准定义]中我们讲述了比较常用的几项: __FILE__:正在编译文件的路径及文件名 __LINE__:正在编译文件的行号 __DATE__:编译时刻的日期字符串 如&q ...

  6. java——慎用可变参数列表

    说起可变参数,我们先看下面代码段,对它有个直观的认识,下方的红字明确地解释了可变参数的意思: 1 public class VarargsDemo{ 2 3 static int sum(int... ...

  7. python中lambda 表达式(无参数、一个参数、默认参数、可变参数(*args、**kwargs)、带判断的lambda、列表使用lambda)

    如果⼀个函数有⼀个返回值,并且只有⼀句代码,可以使⽤ lambda简化. lambda语法: lambda 参数列表 : 表达式 注意: lambda表达式的参数可有可⽆,函数的参数在lambda表达 ...

  8. python注解实现原理_Python3注解+可变参数实现

    一.说明 1.1 关于注解 关于注解这个东西,最早是在大学学java的时候经常会看到某些方法上边@override之类的东西,一方面不知道其作用但另一方面似乎去掉也没什么影响,所以一直都不怎么在意. ...

  9. next用法C语言,C语言可变参数的使用

    先来个简单的例子:#include #include void test0(int num,...) { va_list ap; va_start(ap, num); while(num--) { p ...

最新文章

  1. python内置方法就是内置函数_python内置函数
  2. R语言构建仿真列联表并进行卡方检验(chisq.test):检验两个分类变量是否独立、输出期望的列联表
  3. hls二次加密 m3u8_HLS实战之Wireshark抓包分析
  4. 原创 | 人工智能的人文主义,如何让AI更有爱
  5. 谷歌“夜莺计划”曝光:秘密采集数百万医疗隐私数据!医生患者毫不知情
  6. hdu3665 水最短路
  7. php 调用vnc协议,Centos7下部署VNC(示例代码)
  8. 避免jquery的click多次绑定方法
  9. Delphi常用关键字用法详解
  10. 对于半结构化数据的讲解,这可能是最通俗易懂的一篇文章了
  11. mac下kafka环境搭建 测试
  12. Django中的class Meta知识点
  13. LeetCode 1773. 统计匹配检索规则的物品数量
  14. 河内之塔算法_如何解决河内问题之塔-图解算法指南
  15. 处理table 超出部分滚动问题
  16. android学习笔记---34_Activity的启动模式
  17. Python 爬虫入门(二)—— IP代理使用
  18. php hr系统,专业hr管理系统
  19. 小票打印机安装配置全过程 58mm热敏票据打印机驱动安装
  20. 弘玑Cyclone发布全线产品 | 多个产品与功能系行业首创

热门文章

  1. ant-design-pro Login 组件 实现 rules 验证
  2. saltstack中salt-key收集的主机名与实际主机名不一致
  3. iOS旋钮动画-CircleKnob
  4. activemq - 浅析消息确认模式
  5. 使用配置hadoop中常用的Linux(ubuntu)命令
  6. Oracle 触发器(上)
  7. Windows API一日一练(56)SetEndOfFile和GetFileSizeEx函数
  8. 电脑装windows和ubuntu,如何卸载ubuntu系统
  9. CentOS7重新生成 /boot/grub2/grub.cfg
  10. JDK容器学习之ArrayList:底层存储和动态扩容