C语言的可变参数
可变参数给编程带来了很大的方便,在享受它带来的方便的同时,很有必要了解一下其实现方式,在了解编程语言的同时,也可以扩展编程的思路。
可变参数需要用到3个宏函数和一个类型,他们都定义在<stdarg.h>中,分别是:
va_start(vl)
va_arg(vl, type)
va_end(vl)
其中vl是va_list类型,type就是对象类型(如int, double或 自定义的struct之类的)。
va_start函数用来初始化vl
va_arg(vl, type)用来取得type类型的变量值,这个宏会不可逆的改变vl,所以调用va_arg是要有顺序,不能乱搞,具体顺序就是参数顺序。不可逆的意思就是只能按顺序调用一次。当然你可以再调用va_start,然后又按顺序遍历一次。
va_end就是使得vl无效。表示参数获取完毕
一个简单的例子:
#include <stdio.h>
void VariableArgumentMethod(int argc, ...);
int main(){
VariableArgumentMethod(, , , , , , );
return ;
}
void VariableArgumentMethod(int argc, ...){
// 声明一个指针, 用于持有可变参数
va_list pArg;
// 将 pArg 初始化为指向第一个参数
va_start(pArg, argc);
// 输出参数
for(int i = ; i != argc; ++i){
// 获取 pArg 所指向的参数并输出
printf("%d, ", va_arg(pArg, int) );
}
va_end(pArg);
}
可变参数的实现其实就是利用第一个参数的地址,以及其余参数的类型来确定他们的地址。或者说知道一个基地址(由第一个参数提供),以及一个参数的相对于基地址的偏移量(由参数类型提供),自然就这个参数的地址啦。
因此,可变函数有两个必要条件,1.第一个参数必须是显式提供的,这样才能知道参数在栈中的基地址。2.所有参数类型必须要知道。(printf的第一个参数format中的%d,%lf呀 的作用就是提供参数类型,以确定参数位置)
之后要理解这些宏,需要清楚调用函数时是如何传参的,传参顺序是什么,以及地址对齐。详见:
http://www.cnblogs.com/cpoint/p/3368993.html
简而言之,传参顺序在stdcalll下是从右往左入栈,栈底处于高地址,栈顶处于低地址,栈的增长方向是高到低。如func(arg1, arg2, arg3),它们地址是:&arg1 < &arg2 < &arg3并且连续。
(值得一提的是,我在gcc中查看固定参数的函数时,如func(arg1, arg2, arg3),它们的地址居然是&arg1 > &arg2 > &arg3。 后来查看汇编才知道原来编译器又用了个局部变量来存参数,printf打印的是局部变量的地址)
这里我只写一些我对这些宏的理解。
首先给出实现细节:
typedef char * va_list;
#define _ADDRESSOF(v) ( &(v) )
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
#define va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap) ( ap = (va_list)0 )
_INTSIZEOF(n) 表示n这个类型是int类型的几倍(不是整数倍的向上取整)。比如说double类型是int的2倍,char类型是int的1倍,一个 sizeof(struct my_struct)=10的类型是Int类型的3倍。为什么要是int的整数倍,待会再说。
va_start(ap, v) ap为va_list类型,v为第一个参数。它的作用是将ap复制为第二个参数的起始地址。
va_arg(ap, t) ap为va_list类型,t为你想要获得的参数的类型。作用是先将返回ap值,再将ap设为下一个参数的起始地址。
va_end(ap) 设为空指针。
其中va_start和va_arg都调用了_INTSIZEOF(n), _INTSIZEOF(n)这个宏的作用其实就是确定n这个类型的实际占用的内存空间(也就是考虑了地址对齐)。为什么它必须是int的整数倍呢?按普通的对齐规则,char无论如何都只要1个字节的空间。然而在传参数的时候
却有例外,要4个字节。这是因为传参的时候还发生了类型提升。
在C语言中,调用一个不带原型声明的函数时,调用者会对每个参数执行“默认实际参数提升(default argument promotions)”。该规则同样适用于可变参数函数——对可变长参数列表超出最后一个有类型声明的形式参数之后的每一个实际参数,也将执行上述提升工作。
提升工作如下:
——float类型的实际参数将提升到double
——char、short和相应的signed、unsigned类型的实际参数提升到int
——如果int不能存储原值,则提升到unsigned int
因此下面的代码是错误的,运行时得不到预期的结果:
view plaincopy to clipboardprint?
va_start(pArg, plotNo);
fValue = va_arg(pArg, float); // 类型应改为double,不支持float
va_end(pArg);
va_start(pArg, plotNo);
fValue = va_arg(pArg, float); // 类型应改为double,不支持float
va_end(pArg);
弄明白了_INTSIZEOF含义后。继续解释va_start (ap, v) 它是令ap为第二个参数的起始地址。(v即提供了基地址,又提供了它所占的内存空间,而第二个参数紧跟在第一个参数后面,很自然就能得到第二个参数的位置)
va_arg(ap, t) 实际它和第一个宏原理是一样的。只是有点小变化,基地址ap已经由va_start得到了,t表示参数类型。
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
这个宏刚开始看肯定觉得有点奇怪。 先将ap+_INTSIZEOF(t), 再减去_INTSIZEOF(t)那么还是ap原来的值吗?呵呵,其实 关键点在于+= , 这个宏其实有个副作用,返回的值确实是原来的值没变,但是ap本身+_INTSIZEOF(t) 到了下一个参数的起始地址.
va_end(ap) 没什么好讲的。呵呵
最后附上一个printf简易版本:
#include<stdarg.h> void minprintf(char *fmt, ...)
{
va_list ap;
char *p, *sval;
int ival;
double dval; va_start(ap, fmt);
for (p = fmt; *p; p++) {
if(*p != '%') {
putchar(*p);
continue;
}
switch(*++p) {
case 'd':
ival = va_arg(ap, int);
printf("%d", ival);
break;
case 'f':
dval = va_arg(ap, double);
printf("%f", dval);
break;
case 's':
for (sval = va_arg(ap, char *); *sval; sval++)
putchar(*sval);
break;
default:
putchar(*p);
break;
}
}
va_end(ap);
}
参考链接:
http://blog.chinaunix.net/uid-27666459-id-3772622.html
http://www.cnblogs.com/cpoint/p/3368993.html
http://www.cnblogs.com/Anker/archive/2012/12/27/2836495.html
C语言的可变参数的更多相关文章
- C语言的可变参数在Linux(Ubuntu)与Windows下注意点
基本上C语言的可变参数原理在不同平台和不同编译器下基本类似(通过函数入栈,从右向左,从高位到低位地址),不过部分实现会有所不同:在使用中需要注意的是: va_list 为char 类型指针,部分调用如 ...
- C语言中可变参数的函数(三个点,“...”)
C语言中可变参数的函数(三个点,“...”) 本文主要介绍va_start和va_end的使用及原理. 在以前的一篇帖子Format MessageBox 详解中曾使用到va_start和va_end ...
- [11 Go语言基础-可变参数函数]
[11 Go语言基础-可变参数函数] 可变参数函数 什么是可变参数函数 可变参数函数是一种参数个数可变的函数. 语法 如果函数最后一个参数被记作 ...T ,这时函数可以接受任意个 T 类型参数作为最 ...
- C语言中可变参数函数实现原理
C函数调用的栈结构 可变参数函数的实现与函数调用的栈结构密切相关,正常情况下C的函数参数入栈规则为__stdcall, 它是从右到左的,即函数中的最右边的参数最先入栈.例如,对于函数: void fu ...
- C语言中可变参数的用法
原文地址: http://blog.csdn.net/wooin/archive/2006/04/29/697106.aspx 我们在C语言编程中会遇到一些参数个数可变的函数,例如printf() ...
- C语言函数可变参数列表
C语言允许使用可变参数列表,我们常用的printf函数即为可变参数函数,C标准库提供了stdarg.h为我们提供了这方面支持:该头文件提供了一些类型和宏来支持可变参数列表,包括类型va_list,宏v ...
- C语言中可变参数的使用
在C语言程序编写中我们使用最多的函数一定包括printf以及很多类似的变形体.这个函数包含在C库函数中,定义为 int printf( const char* format, ...); 除了一个格式 ...
- C语言中可变参数的原理——printf()函数
函数原型: int printf(const char *format[,argument]...) 返 回 值: 成功则返回实际输出的字符数,失败返回-1. 函数说明: 使用过C语言的人所再熟悉不过 ...
- [日常] Go语言圣经-可变参数习题
1.参数数量可变的函数称为为可变参数函数,例子就是fmt.Printf和类似函数2.参数列表的最后一个参数类型之前加上省略符号“...”3.虽然在可变参数函数内部,...int 型参数的行为看起来很像 ...
随机推荐
- JavaScript constructors, prototypes, and the `new` keyword
Are you baffled(阻碍:使迷惑) by the new operator in JavaScript? Wonder what the difference between a func ...
- twitter的snowflake算法(C#版本)
转自:http://blog.csdn.net/kinwyb/article/details/50238505 使用twitter的snowflake算法生成唯一ID. 在分布式系统中,需要生成全局U ...
- J2EE 全面简介
原文地址:http://www.ibm.com/developerworks/cn/java/j2ee/ J2EE的概念 目前,Java 2平台有3个版本,它们是适用于小型设备和智能卡的Java 2平 ...
- [Javascript] Get Started with LeafletJS Mapping
Leaflet makes creating maps in the browser dead simple. With some HTML and 3 lines of JavaScript, we ...
- HBase中的备份和故障恢复方法
本文将对Apache HBase可用的数据备份机制和大量数据的故障恢复/容灾机制做简要介绍. 随着HBase在重要的商业系统中应用的大量添加,很多企业须要通过对它们的HBase集群建立健壮的备份和故障 ...
- oracle数据库导入导出的dmp(转)
window下: imp必须要dba用户,所以用sysdba用户登陆,然后给予chnlmgr用户dba权限 grant connect,resource,dba to chnlmgr; 全部导入 im ...
- svn各种问题总结
1.out of date 就是说别人已经更新到服务器了,而我修改的还不是服务器最新的. 直接Replace with 资源库的最新内容
- HDU1518(dfs)java/ c++
Square Time Limit: 10000/5000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others)Total Sub ...
- css :after和:before
:before是css中的一种伪元素,可用于在某个元素之前插入某些内容.:after是css中的一种伪元素,可用于在某个元素之后插入某些内容. 举例: 1.结合border写个对话框的样式. < ...
- 移动终端学习一:css3 Media Queries简介
移动终端学习之一 css3 Media Queries简介 1.简介 别人写过的我就不重复了,来个链接:http://www.w3cplus.com/content/css3-media-querie ...