c++不定参数函数
不定参数当年做为C/C++语言一个特长被很多人推崇,但是实际上这种技术并没有应用很多。除了格式化输出之外,我实在没看到多少应用。主要原因是这种技术比较麻烦,副作用也比较多,而一般情况下重载函数也足以替换它。尽管如此,既然大家对它比较感兴趣,我就简单总结一下它的使用和需要注意的常见问题。
原理
刚学C语言的时候,一般人都会首先接触printf函数。通过这个函数,你可以打印不定个数的变量到屏幕,如:
printf("%d", 3);
printf("%d,%d",3,4);
上述代码看似简单,实际上却需要我们解决许多问题。在我们设计printf的时候,我们是不知道到底会传入几个参数的。在这种未知的情况下,我们需要解决下面几个问题:
- 怎么告诉printf我们会传入几个参数
- printf怎么去访问这些参数
- 函数调用完成后,系统怎么把参数从传递用的堆栈中释放
为了解决这些问题,我们首先要解释cdecl调用约定(参见论调用约定),所有使用不定参数的函数必须是使用cdecl(全局函数)或者this call(类成员函数)调用约定。该约定对于参数传递规定如下:
- 参数从右向左入栈(也就是如果你调用f(a,b,c),则c先入栈,然后是b,最后是a入栈)
- 调用者负责清理堆栈
其中第二点直接解决了前面三个问题中的第三个问题。我们来详细说说其他两个问题。
确定参数的个数
在一个函数中,一般有如下prolog代码:
00401020 push ebp
00401021 mov ebp,esp
00401023 sub esp,48h
执行上述代码之后,func(a,b,c)函数所处的堆栈上下文就变成如下布局:
其中,ebp指向保存旧的ebp的堆栈内存的下一个字的地址,ebp+8指向eip地址,ebp+12则指向函数调用的第一个参数,而ebp和esp之间是用于临时变量(也就是堆栈变量)的空间。
注意,由于上述prolog代码的存在,我们很容易通过ebp得到第一个参数的地址,对于不定参数列表之前的类型固定的参数,我们也可以根据类型信息得到其实际的位置(例如,第一个参数的位置偏移第一个参数的大小,就是第二个参数的地址)。
注意不定参数函数有个限制,就是不定参数的列表必须在整个函数的参数列表的最后。我们不可以定义如下的函数:
void func(int a, ..., int c)
所有类型固定的参数都必须出现在参数列表的开始。这样根据前面的论述,我们就可以得到所有类型固定的参数。
在设计具有不定参数列表的函数的时候,我们有两种方法来确定到底多少参数会被传递进来。
方法1是在类型固定的参数中指明后面有多少个参数以及他们的类型。printf就是采用的这种方法,它的format参数指明后面每个参数的类型。
方法2是指定一个结束参数。这种情况一般是不定参数拥有同样的类型,我们可以指定一个特定的值来表示参数列表结束。下面这个sum函数就是一个例子:














使用这个函数的代码为:







访问各个参数
其实前文已经告诉我们怎么去访问不定参数。va_start和va_arg函数可以被结合起来用于依次访问每个函数,他们实际上都是宏函数。
在vc6,va_start函数定义为:


其中_INTSIZEOF(n)计算比n大的sizeof(int)的最小倍数,如果n=101,则_INTSIZEOF(n)为104。
va_start执行完毕后,ap指向变量v后第一个4字节对齐的地址。例如,v的地址为0x123456, v的大小为13,则v后面的下一个与字边界对齐的地址为0x123456+0x0D=0x123463再调整为与4字节对齐的下一个地址,也就是0x123464.
va_arg函数定义为:

分析与va_start一样,它的结果是使ap指向当前变量的下一个变量。
这样,我们只要在开始时使用va_start把不定参数列表赋值给ap,然后依次用va_arg获得不同参数即可。
潜在问题
使用不定参数列表,有两个问题特别需要注意。
问题1的理解相对简单:我们在重载一个函数的时候,不能依赖不定参数列表部分对函数进行区分。
假定我们定义两个重载函数如下:
int func(int a, int b, ...)
int func(int a, int b, float c);
则上述函数会导致编译器不知道怎么去解释func(1,2, 3.3),因为当第三个参数为浮点数时,两个实现都可以满足匹配要求。一般情况,个人建议对于不定参数函数不要去做重载。
另外一个问题是关于类型问题。绝大多数情况下,C和C++的变量都是强类型的,而不定参数列表属于一个特例。
当我们调用va_arg的时候,我们指明下一个参数的类型,而在执行的时候,va_arg正是根据这个信息在堆栈上来找到对应的参数的。如果我们需要的类型和真实传递进来的参数完全一致时自然没有问题,但是假如类型不一样,则会有大麻烦。
假如上面的的sumi函数,我们用下面方法调用:

注意第二个参数我们传入了一个double类型的2.2,我们希望sumi在做加法时可以做隐式类型转换,转换为int进行计算。但是实际情况时,当我们分析到这个参数时,调用的是:

根据前文va_arg的定义,这个宏被翻译成:

如果后面的+=计算出正确的地址,最后就变成
如果希望能得到正确的整数值,必须要求addr所在的地址是一个真实的int类型。但是当我们传入double时,实际上其内存布局和int完全不同,因此我们得不到需要的整数。感兴趣的朋友可以用下面简单的代码做测试:



因此,当我们调用有不定参数列表的函数时,不要期望系统做隐式类型转换,系统不会做这种检查或者转换,你给的参数类型必须严格和你希望的值一样。
c++不定参数函数的更多相关文章
- 不定参数函数原理以及实现一个属于自己的printf函数
一.不定参数函数原理 二.实现一个属于自己的printf函数 参考博文:王爽汇编语言综合研究-函数如何接收不定数量的参数
- Python不定参数函数
1. 元组形式 def test1(*args): print('################test1################') print(type(args)) print(arg ...
- GO语言练习:不定参数函数
1.代码 2.运行 1.代码 package main import "fmt" func MyPrintf(args ...interface{}){ for _, arg := ...
- Python 不定参数函数
1. 元组形式 def test1(*args): print('################test1################') print(type(args)) print(arg ...
- oc自定义不定参数函数
-(void)getValueFormConfig:(NSString *)key,... or -(void)getValueFormConfig:(NSString *)key,...NS_REQ ...
- UE3多参数函数实现
基础宏定义 #define VARARG_EXTRA(A) A, #define VARARG_NONE #define VARARG_PURE =0 static inline DWORD Chec ...
- 有关 java 不定参数
不定参数实际为数组参数的一种写法而已,本质上与数组参数完全相同 //1.数组参数函数 public static int sum(int[] values) { } //2.不定参数函数 不定参数只能 ...
- C++传递不定参函数
定义不定参数函数,要用到下面这些宏: va_start(ap, farg): 初始化一个va_list变量ap,farg是第一个形参 va_arg(ap, type): 获取(下)一个type类型的参 ...
- C语言函数不定参数实现方式
函数如何实现不定参数: 由于在C语言中没有函数重载,解决不定数目函数参数问题变得比较麻烦,即使采用C++,如果参数个数不能确定,也很难采用函数重载.对这种情况,提出了指针参数来解决问题. (1)va_ ...
随机推荐
- SQL Server没有足够的内存继续执行程序
有一个表的数据特别大,我点击生成脚本的时候,喜欢新建窗口,但是不行,数据量太大了,所以选择保存文件,保存到本地了.然后我点击执行,又报没有内存去执行...还是因为数据量太大了 解决办法,使用sqlcm ...
- 阿里云MongoDB存储数据
近期上了个活动,考虑后期的运维及人力成本,还是选择了阿里云的MongoDB,不过阿里云这玩意本地测试官方没有给本地测试链接地址,只能做映射上去了测了... 选了个2核4G的,更多详细信息,可以去阿里上 ...
- Web API中的传参方式
在Restful风格的WebApi的里面,API服务的增删改查,分别对应着Http Method的Get / Post / Delete /Put,下面简单总结了Get / Post /Delete ...
- angular,vue,react的基本语法—样式处理
基本语法 样式处理: vue: 动态属性: v-bind:class 简写 :class react: 变量:class={selecter} angular: 指令:[ngClass]=" ...
- Redis之路
前言:数据库是一切数据的源头,因此我们没有逃避的理由 (一) 什么是redis? redis是nosql(not noly sql)产品中最为出色的一种非关系型的数据库,主要包括以下几种存储结构:St ...
- jsp页面的共用
我们经常希望一个网页,根据不同得请求显示不同得数据. 方法就是在session中添加一个变量,根据不同得值区分不同得请求类型. 后台:request.getSession().setAttribute ...
- final 关键字:用来修饰类,方法,成员变量,局部变量
final 关键字:用来修饰类,方法,成员变量,局部变量 表示最终的不可变的 1.final修饰一个类 表示当前的类不能有子类,也就是不能将一个类作为父类 格式: public final class ...
- Silverlight设计器——Path
如下图,在设计一个InfoWindow的时候,顶栏的关闭按钮没有出现.观察了半天,也没有弄明白.无意中,拖动一个几乎透明的信息框,突然就出现了关闭的按钮.原来,那个信息框只是一个Path,它遮住了关闭 ...
- SQL Server 调用 C# 方法实现正则表达式验证
Ø 前言 1. 在 SQL Server 中默认是不支持正则表达式验证的,如果需要某个字符串匹配一个正则表达式的验证规则,就需要额外的编写 C# 方法,并发布到 SQL Server 数据库中. ...
- 解决yum安装mysql时Requires: libc.so.6(GLIBC_2.17)(64bit)
1.yum install mysql-community-server 1 2 3 4 5 6 7 Error: Package: mysql-community-libs-5.7.17-1.el7 ...