• linkedu视频
  • 平面设计
  • 电脑入门
  • 操作系统
  • 办公应用
  • 电脑硬件
  • 动画设计
  • 3D设计
  • 网页设计
  • CAD设计
  • 影音处理
  • 数据库
  • 程序设计
  • 认证考试
  • 信息管理
  • 信息安全
菜单
linkedu.com
  • 网页制作
  • 数据库
  • 程序设计
  • 操作系统
  • CMS教程
  • 游戏攻略
  • 脚本语言
  • 平面设计
  • 软件教程
  • 网络安全
  • 电脑知识
  • 服务器
  • 视频教程
  • JavaScript
  • ASP.NET
  • PHP
  • 正则表达式
  • AJAX
  • JSP
  • ASP
  • Flex
  • XML
  • 编程技巧
  • Android
  • swift
  • C#教程
  • vb
  • vb.net
  • C语言
  • Java
  • Delphi
  • 易语言
  • vc/mfc
  • 嵌入式开发
  • 游戏开发
  • ios
  • 编程问答
  • 汇编语言
  • 微信小程序
  • 数据结构
  • OpenGL
  • 架构设计
  • qt
  • 微信公众号
您的位置:首页 > 程序设计 >C语言 > 浅析C/C++中的可变参数与默认参数

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

作者: 字体:[增加 减小] 来源:互联网 时间:2017-05-28

通过本文主要向大家介绍了c++默认参数,c++缺省参数,c++ 函数参数,c++ 数组参数,c++ 参数等相关知识,希望对您有所帮助,也希望大家支持linkedu.com www.linkedu.com

千万要注意,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。

------------ 例子代码 ------------

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 );
}
</div>
上例代码功能是计算平均数,函数允许用户输入多个整型参数,要求作后一个参数必须是-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等其他系统实现略有不同,感兴趣的朋友可以自己研究):

#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 )
</div>
从代码可以看出,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)) - _INTS

分享到:QQ空间新浪微博腾讯微博微信百度贴吧QQ好友复制网址打印

您可能想查找下面的文章:

  • C++中的变长参数深入理解
  • c++实现通用参数解析类示例
  • 浅析C/C++中的可变参数与默认参数
  • C++函数参数取默认值的深入详解
  • C++可变参数的实现方法

相关文章

  • 2017-05-28C++中单链表的建立与基本操作
  • 2017-05-28浅谈socket TCP编程中connect的一些坑
  • 2017-05-28解析C#中不一样的大小写转换
  • 2017-05-28C语言 数据类型详细介绍
  • 2017-05-28VC中SDK与MFC的区别浅析
  • 2017-05-28C++中实现队列类链式存储与栈类链式存储的代码示例
  • 2017-05-28C语言 结构体数组详解及示例代码
  • 2017-05-28原创的C语言控制台小游戏
  • 2017-05-28图文详解C语言位运算基础知识
  • 2017-05-28C语言使用openSSL库DES模块实现加密功能详解

文章分类

  • JavaScript
  • ASP.NET
  • PHP
  • 正则表达式
  • AJAX
  • JSP
  • ASP
  • Flex
  • XML
  • 编程技巧
  • Android
  • swift
  • C#教程
  • vb
  • vb.net
  • C语言
  • Java
  • Delphi
  • 易语言
  • vc/mfc
  • 嵌入式开发
  • 游戏开发
  • ios
  • 编程问答
  • 汇编语言
  • 微信小程序
  • 数据结构
  • OpenGL
  • 架构设计
  • qt
  • 微信公众号

最近更新的内容

    • 详解C语言中的wait()函数和waitpid()函数
    • c语言B树深入理解
    • C++设计模式编程中使用Bridge桥接模式的完全攻略
    • c++编写简单的计算器程序
    • C++中将string类型转化为int类型
    • 成员函数的重载、覆盖与隐藏详细解析
    • VC外部符号错误_main,_WinMain@16,__beginthreadex解决方法
    • 用标准c++实现string与各种类型之间的转换
    • 手把手教你如何优化C语言程序
    • libxml教程(图文详解)

关于我们 - 联系我们 - 免责声明 - 网站地图

©2020-2025 All Rights Reserved. linkedu.com 版权所有