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 型参数的行为看起来很像 ...
随机推荐
- POJ1182食物链 (并查集)
第一反应就是和那个搞基的虫子的题很像(poj2492 http://www.cnblogs.com/wenruo/p/4658874.html),不过是把种类从2变成了3. 错在很白痴的地方,卡了好久 ...
- 100个常用的linux命令
1,echo “aa” > test.txt 和 echo “bb” >> test.txt //>将原文件清空,并且内容写入到文件中,>>将内容放到文件的尾部 2 ...
- PHP中如何防止SQL注入
这是StackOverFlow上一个投票非常多的提问 How to prevent SQL injection in PHP? 我把问题和赞同最多的答题翻译了下来. 提问:如果用户的输入能直接插入到 ...
- java的定时器用法
java定时器的使用 定时器类Timer在java.util包中.使用时,先实例化,然后使用实例的schedule(TimerTask task, long delay)方法,设定指定的任务task在 ...
- 在Android中访问内置SE和基于SE的卡模拟(一)
2013-10-10 编写 前言 在“十问Android NFC手机上的卡模拟”文中仅仅简单的介绍了一下相关的概念,如果需要了解基于SE的卡模拟的更多细节,也就是,究竟在Android的NFC手机上, ...
- Html的空格显示
一.使用全角空格 全角空格被解释为汉字,所以不会被被解释为HTML分隔符,能够依照实际的空格数显示. 二.使用空格的替代符号 替代符号就是在须要显示空格的地方添�替代符号,这些符号会被浏览器解释为空格 ...
- [TypeScript] 1. Catching JavaScript Mistakes with TypeScript
The TypeScript compiler is a powerful tool which catches mistakes even in vanilla JavaScript. Try it ...
- Java 加密 AES 对称加密算法
版权声明:本文为博主原创文章,未经博主允许不得转载. [AES] 一种对称加密算法,DES的取代者. 加密相关文章见:Java 加密解密 对称加密算法 非对称加密算法 MD5 BASE64 AES R ...
- C#泛型对类型参数的推断
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.T ...
- android开发之shape详解
很多时候,使用shape能够实现的效果,你用一张图片也能够实现,但问题是一张图片无论你怎么压缩,它都不可能比一个xml文件小,因此,为了获得一个高性能的手机App,我们在开发中应该遵循这样一个原则:能 ...