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 型参数的行为看起来很像 ...
随机推荐
- 【转】科普Spark,Spark是什么,如何使用Spark
本博文是转自如下链接,为了方便自己查阅学习和他人交流.感谢原博主的提供! http://www.aboutyun.com/thread-6849-1-1.html http://www.aboutyu ...
- hdoj 1898 Sempr == The Best Problem Solver?
Sempr == The Best Problem Solver? Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65535/3276 ...
- 一切皆WEB
所有应用都应该成为Web应用吗?当然不是.总有一些重要的例外,有些种类的软件跟网络也毫无关系.但是,这些是少数情况,是一些特殊应用.它们固然是重要的小生态环境,但不管怎么说,就只是“小生态”. 如果你 ...
- Win+R指令(2)
1. gpedit.msc-----组策略 2. sndrec32-------录音机 3. Nslookup-------IP地址侦测器 ,是一个 监测网络中 DNS 服务器是否能正确实现域名解析的 ...
- C++ 实现按随意键继续~~~
近期让学生敲代码交作业的时候要求他们仅仅给我交个cpp文件和一个exe文件,这样交上来的东西不至于太多,不占我的地方,可是有一个问题是exe它总是执行完后就直接关闭界面了,看不到执行结果的界面. 然后 ...
- Awake和Start
经过查阅资料和自己的理解整理出来的,欢迎大家指教. Awake和Start对比 awake比start先执行. 当有多个类的时候,所有类的awake执行完了才会执行start. awake里面一般放初 ...
- TCP/IP 中的二进制反码求和算法
对于这个算法,很多书上只是说一下思路,没有具体的实现.我在这里举个例子吧 以4bit(计算方便一点,和16bit是一样的)做检验和来验证. 建设原始数据为 1100 , 1010 , 0000(校验位 ...
- android学习资料
在线查看android源码 1. https://github.com/android 2. http://grepcode.com/project/repository.grepcode.com ...
- cocoapods使用指南
什么是cocoapods cocoapods是库管理工具. cocoapods的用途 解决库之间的依赖关系.如前文所述: 一个开源的项目可能是另一个项目的基础, A依赖B, B依赖C和D, D又依赖E ...
- Java基础知识强化之集合框架笔记70:模拟斗地主洗牌和发牌(ArrayList)
1. 模拟斗地主洗牌和发牌 分析: A:创建一个牌盒 B:装牌 C:洗牌 D:发牌 E:看牌 2. 代码实现: package cn.itcast_03; im ...