可变参数给编程带来了很大的方便,在享受它带来的方便的同时,很有必要了解一下其实现方式,在了解编程语言的同时,也可以扩展编程的思路。

可变参数需要用到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语言的可变参数的更多相关文章

  1. C语言的可变参数在Linux(Ubuntu)与Windows下注意点

    基本上C语言的可变参数原理在不同平台和不同编译器下基本类似(通过函数入栈,从右向左,从高位到低位地址),不过部分实现会有所不同:在使用中需要注意的是: va_list 为char 类型指针,部分调用如 ...

  2. C语言中可变参数的函数(三个点,“...”)

    C语言中可变参数的函数(三个点,“...”) 本文主要介绍va_start和va_end的使用及原理. 在以前的一篇帖子Format MessageBox 详解中曾使用到va_start和va_end ...

  3. [11 Go语言基础-可变参数函数]

    [11 Go语言基础-可变参数函数] 可变参数函数 什么是可变参数函数 可变参数函数是一种参数个数可变的函数. 语法 如果函数最后一个参数被记作 ...T ,这时函数可以接受任意个 T 类型参数作为最 ...

  4. C语言中可变参数函数实现原理

    C函数调用的栈结构 可变参数函数的实现与函数调用的栈结构密切相关,正常情况下C的函数参数入栈规则为__stdcall, 它是从右到左的,即函数中的最右边的参数最先入栈.例如,对于函数: void fu ...

  5. C语言中可变参数的用法

    原文地址: http://blog.csdn.net/wooin/archive/2006/04/29/697106.aspx   我们在C语言编程中会遇到一些参数个数可变的函数,例如printf() ...

  6. C语言函数可变参数列表

    C语言允许使用可变参数列表,我们常用的printf函数即为可变参数函数,C标准库提供了stdarg.h为我们提供了这方面支持:该头文件提供了一些类型和宏来支持可变参数列表,包括类型va_list,宏v ...

  7. C语言中可变参数的使用

    在C语言程序编写中我们使用最多的函数一定包括printf以及很多类似的变形体.这个函数包含在C库函数中,定义为 int printf( const char* format, ...); 除了一个格式 ...

  8. C语言中可变参数的原理——printf()函数

    函数原型: int printf(const char *format[,argument]...) 返 回 值: 成功则返回实际输出的字符数,失败返回-1. 函数说明: 使用过C语言的人所再熟悉不过 ...

  9. [日常] Go语言圣经-可变参数习题

    1.参数数量可变的函数称为为可变参数函数,例子就是fmt.Printf和类似函数2.参数列表的最后一个参数类型之前加上省略符号“...”3.虽然在可变参数函数内部,...int 型参数的行为看起来很像 ...

随机推荐

  1. 高效Linux用户需要了解的命令行技能

    最近在Quora上看到一个问答题目,关于在高效率Linux用户节省时间Tips. 将该题目的回答进行学习总结,加上自己的一些经验,记录如下,方便自己和大家参考. 下面介绍的都是一些命令行工具,这些工具 ...

  2. 组件局域网中的无集线器、Windows XP、Windows 7、Windows 8的对等网

     为什么要用对等网? 答:对等网采用分散管理的方式,网络中的每台计算机既作为客户机又可作为服务器来工作,每个用户都管理自己机器上的资源. 组建局域网中无集线器的对等网 组建局域网中Windows XP ...

  3. CentOS上安装FastDFS分布式文件系统

    鱼大自己写的项目简介:http://bbs.chinaunix.net/thread-1920470-1-1.html 架构简介:http://www.programmer.com.cn/4380/ ...

  4. A Tour of Go For is Go's "while"

    At that point you can drop the semicolons(分号): C's while is spelled for in Go. package main import & ...

  5. 在C#中我们能调用一个类的私有方法吗

    博客搬到了fresky.github.io - Dawei XU,请各位看官挪步.最新的一篇是:在C#中我们能调用一个类的私有方法吗.

  6. 为什么Form.Timer的event handler在Form被Dispose之后还是被调到了?

    博客搬到了fresky.github.io - Dawei XU,请各位看官挪步.最新的一篇是:为什么Form.Timer的event handler在Form被Dispose之后还是被调到了?.

  7. 你真的知道C#的TryParse吗?

    博客搬到了fresky.github.io - Dawei XU,请各位看官挪步.最新的一篇是:你真的知道C#的TryParse吗?.

  8. Enum枚举类|注解Annotation

    Enum枚举类 ①枚举类和普通类的差别: 使用 enum 定义的枚举类默认继承了 java.lang.Enum 类 枚举类的构造器仅仅能使用 private 訪问控制符 枚举类的全部实例必须在枚举类中 ...

  9. “cvSnakeImage”: 找不到标识符

    1>g:\project\opencv\helloopencv\helloopencv\helloopencv.cpp(74) : error C2065: "CV_VALUE&quo ...

  10. 混合模式程序集是针对“v1.1.4322”版的执行时生成的,在没有配置其它信息的情况下,无法在 4.0 执行时中载入该程序集。

    看到一个kinect大牛编写的一个水果忍者的体感游戏版本号,让我为自己一直以来仅仅用现有的网页游戏来模拟kinect体感游戏控制感到羞愧,没办法.我还是菜鸟.学习一段后自己模仿星际大战这个游戏.自己写 ...