浅析C/C++中的可变参数与默认参数

千万要注意,C不支持默认参数

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) = 0x00000011(谢谢glietboys的提醒,此处应该是0xFFFFFF00)
同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

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

(0)

相关推荐

  • C++可变参数的函数与模板实例分析

    本文实例展示了C++可变参数的函数与模板的实现方法,有助于大家更好的理解可变参数的函数与模板的应用,具体内容如下: 首先,所谓可变参数指的是函数的参数个数可变,参数类型不定的函数.为了编写能处理不同数量实参的函数,C++提供了两种主要的方法:如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型:如果实参的类型不同,我们可以编写可变参数模板.另外,C++还有一种特殊的省略符形参,可以用它传递可变数量的实参,不过这种一般只用于与C函数交互的接口程序. 一.可变参数函数

  • C++中可以接受任意多个参数的函数定义方法(详解)

    能够接受任意多个参数的函数,可以利用重载来实现.这种函数的执行过程类似于递归调用,所以必须要有递归终止条件. #include <iostream> #include <bitset> void print() {} // 递归终止条件.这是必需的. template<typename Type, typename... Types> void print(const Type& arg, const Types&... args) { std::cou

  • C++可变参数的实现方法

    可变参数的实现要解决三个问题: 1.如何调用带有可变参数的函数2.如何编译有可变参数的程序3.在带有可变参数的函数体中如何持有可变参数第一个问题, 调用时在可以传入可变参数的地方传入可变参数即可,当然,还有一些需要注意的地方,后面会提到. 第二个问题,编译器需要在编译时采用一种宽松的检查方案,,这会带来一些问题, 比如对编程查错不利. 第三个是我在这里要关心的问题,先以C语言为例分析其实现原理. printf和scanf是C语言标准库中最常见的可变参数函数, printf的签名是 复制代码 代码

  • C/C++中可变参数的用法详细解析

    可变参数即表示参数个数可以变化,可多可少,也表示参数的类型也可以变化,可以是int,double还可以是char*,类,结构体等等.可变参数是实现printf(),sprintf()等函数的关键之处,也可以用可变参数来对任意数量的数据进行求和,求平均值带来方便(不然就用数组或每种写个重载).在C#中有专门的关键字parame,但在C,C++并没有类似的语法,不过幸好提供这方面的处理函数,本文将重点介绍如何使用这些函数. 第一步 可变参数表示用三个点-来表示,查看printf()函数和scanf(

  • ES6字符串模板,剩余参数,默认参数功能与用法示例

    本文实例讲述了ES6字符串模板,剩余参数,默认参数功能与用法.分享给大家供大家参考,具体如下: 这里介绍一下字符串模板以及剩余参数,默认参数的概念以及使用. 先来说说字符串模板. 字符串模板 ES6中允许使用反引号 ` 来创建字符串,此种方法创建的字符串里面可以包含由美元符号加花括号包裹的变量${varible}.如果你使用过像C#等后端强类型语言的话,对此功能应该不会陌生. //产生一个随机数 var num=Math.random(); //将这个数字输出到console console.l

  • C++核心编程之占位参数和默认参数

    目录 1,默认参数 2,函数占位参数 1,默认参数 在c++中,函数的形参列表中的形参是可以有默认值的. 语法: 返回值类型 函数名 (参数 = 默认值) 注意事项:1从左到右设置默认参数.2不能重定义默认参数 代码示例: int test(int a, int b=10, int c=10) {   return a + b + c; } ​b=10和c=10就是函数的默认参数,当不给b和c赋值时,就用默认的参数 使用 ​使用默认参数时候有两点要注意​: (1)设置默认参数的时候,必须从左到右

  • 浅析C/C++中的可变参数与默认参数

    千万要注意,C不支持默认参数 C/C++支持可变参数个数的函数定义,这一点与C/C++语言函数参数调用时入栈顺序有关,首先引用其他网友的一段文字,来描述函数调用,及参数入栈: ------------ 引用开始 ------------ C支持可变参数的函数,这里的意思是C支持函数带有可变数量的参数,最常见的例子就是我们十分熟悉的printf()系列函数.我们还知道在函数调用时参数是自右向左压栈的.如果可变参数函数的一般形式是:    f(p1, p2, p3, -)那么参数进栈(以及出栈)的顺

  • 实例讲解在C++的函数中变量参数及默认参数的使用

    包含变量参数列表的函数 如果函数声明中最后一个成员是省略号 (...),则函数声明可采用数量可变的参数.在这些情况下,C++ 只为显式声明的参数提供类型检查.即使参数的数量和类型是可变的,在需要使函数泛化时也可使用变量参数列表.函数的系列是一个使用变量参数列表的函数的示例.printfargument-declaration-list 包含变量参数的函数 若要访问声明后的参数,请使用包含在标准包含文件 STDARG.H 中的宏(如下所述). 采用数量可变的参数的函数声明至少需要一个占位符参数(即

  • Python为何不能用可变对象作为默认参数的值

    先来看一道题目: >>> def func(numbers=[], num=1): ... numbers.append(num) ... return numbers >>> func() [1] >>> func() [1, 1] >>> func() [1, 1, 1] 我们似乎发现了一个Bug,每次用相同的方式调用函数 func() 时,返回结果竟然不一样,而且每次返回的列表在不断地变长. >>> id(fu

  • Python中使用partial改变方法默认参数实例

    Python 标准库中 functools库中有很多对方法很有有操作的封装,partial Objects就是其中之一,他是对方法参数默认值的修改. 下面就看下简单的应用测试. 复制代码 代码如下: #!/usr/bin/env python # -*- coding: utf-8 -*- #python2.7x #partial.py #authror: orangleliu ''' functools 中Partial可以用来改变一个方法默认参数 1 改变原有默认值参数的默认值 2 给原来没

  • C++语法中的函数重载和默认参数

    C语言中没有函数重载 C++语言中有函数重载 函数名相同,参数个数不同.参数类型不同.参数顺序不同 例如下面就是函数重载 void sum(int a, int b){ cout << a+b << endl; } void sum(int a, double b){ cout << a+b << endl; } 返回值类型与函数重载无关 返回值类型与函数重载无关,下面代码不构成重载,编译会报错 //返回值类型与函数重载无关 int func(){ retu

  • IDEA之启动参数,配置文件默认参数的操作

    配置文件写法: isPay是key,如果启动参数不传,则采用默认值是1. 启动类传参: 补充:IDEA使用指定配置文件 在VM options 中添加 -Dspring.profiles.active=xxx 如 以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们.如有错误或未考虑完全的地方,望不吝赐教.

  • Python中的函数参数(位置参数、默认参数、可变参数)

    目录 一.位置参数 二.默认参数 三.可变参数 四.关键字参数 五.命名关键字参数 六.各种参数之间的组合 函数的参数:Python中函数定义非常简单,由于函数参数的存在,使函数变得非常灵活应用广泛:不但使得函数能够处理复杂多变的参数,还能简化函数的调用. Python中的函数参数有如下几种:位置参数.默认参数.可变参数.关键字参数和命名关键字参数 一.位置参数 位置参数(positional arguments)就是其他语言的参数,其他语言没有分参数的种类是因为只有这一种参数, 所有参数都遵循

  • 深入讲解Python函数中参数的使用及默认参数的陷阱

    C++里函数可以设置缺省参数,Java不可以,只能通过重载的方式来实现,python里也可以设置默认参数,最大的好处就是降低函数难度,函数的定义只有一个,并且python是动态语言,在同一名称空间里不能有想多名称的函数,如果出现了,那么后出现的会覆盖前面的函数. def power(x, n=2): s = 1 while n > 0: n = n - 1 s = s * x return s 看看结果: >>> power(5) 25 >>> power(5,3

随机推荐