一、什么是可变參数
我们在C语言编程中有时会遇到一些參数个数可变的函数,比如printf()函数,其函数原型为:
int printf( const char* format, ...);
它除了有一个參数format固定以外,后面跟的參数的个数和类型是可变的(用三个点"…"做參数占位符),实际调用时能够有以下的形式:
printf("%d",i);
printf("%s",s);
printf("the number is %d ,string is:%s", i, s);
以上这些东西已为大家所熟悉。可是到底怎样写可变參数的C函数以及这些可变參数的函数编译器是怎样实现,这个问题却一直困扰了我好久。本文就这个问题进行一些探讨,希望能对大家有些帮助. 二、可变參数在编译器中的处理
我们知道va_start,va_arg,va_end是在stdarg.h中被定义成宏的, 由于1)硬件平台的不同 2)编译器的不同,所以定义的宏也有所不同,以下看一下VC++6.0中stdarg.h里的代码(文件的路径为VC安装文件夹下的\vc98\include\stdarg.h
       typedef char *     va_list;
#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 )

以下我们解释这些代码的含义:

1、首先把va_list被定义成char*。这是由于在我们眼下所用的PC机上,字符指针类型能够用来存储内存单元地址。而在有的机器上va_list是被定义成void*的

2、定义_INTSIZEOF(n)主要是为了某些须要内存的对齐的系统.这个宏的目的是为了得到最后一个固定參数的实际内存大小。在我的机器上直接用sizeof运算符来取代,对程序的执行结构也没有影响。

(后文将看到我自己的实现)。

3、va_start的定义为 &v+_INTSIZEOF(v) ,这里&v是最后一个固定參数的起始地址,再加上事实上际占用大小后,就得到了第一个可变參数的起始内存地址。所以我们执行va_start(ap, v)以后,ap指向第一个可变參数在的内存地址,有了这个地址,以后的事情就简单了。

这里要知道两个事情:
⑴在intel+windows的机器上,函数栈的方向是向下的。栈顶指针的内存地址低于栈底指针,所以先进栈的数据是存放在内存的高地址处。
(2)在VC等绝大多数C编译器中。默认情况下,參数进栈的顺序是由右向左的,因此,參数进栈以后的内存模型例如以下图所看到的:最后一个固定參数的地址位于第一个可变參数之下,而且是连续存储的。
|--------------------------|
| 最后一个可变參数 | ->高内存地址处
|--------------------------|
|--------------------------|
| 第N个可变參数 | ->va_arg(arg_ptr,int)后arg_ptr所指的地方,
| | 即第N个可变參数的地址。 |--------------- |
|--------------------------|
| 第一个可变參数 | ->va_start(arg_ptr,start)后arg_ptr所指的地方
| | 即第一个可变參数的地址
|--------------- |
|--------------------------|
| |
| 最后一个固定參数 | -> start的起始地址
|-------------- -|  
 .................
|-------------------------- |
| |
|--------------- | -> 低内存地址处
(4) va_arg():有了va_start的良好基础,我们取得了第一个可变參数的地址。在va_arg()里的任务就是依据指定的參数类型取得本參数的值。而且把指针调到下一个參数的起始地址。

因此,如今再来看va_arg()的实现就应该心中有数了:
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
这个宏做了两个事情,
①用用户输入的类型名对參数地址进行强制类型转换,得到用户所须要的值
②计算出本參数的实际大小。将指针调到本參数的结尾。也就是下一个參数的首地址,以便兴许处理。 (5)va_end宏的解释:x86平台定义为ap=(char*)0;使ap不再 指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不会为va_end产生代码,比如gcc在linux的x86平台就是这样定义的. 在这里大家要注意一个问题:由于參数的地址用于va_start宏,所以參数不能声明为寄存器变量或作为函数或数组类型. 关于va_start, va_arg, va_end的描写叙述就是这些了,我们要注意的 是不同的操作系统和硬件平台的定义有些不同,但原理却是类似的. 三、可变參数在编程中要注意的问题
由于va_start, va_arg, va_end等定义成宏,所以它显得非常愚蠢, 可变參数的类型和个数全然在该函数中由程序代码控制,它并不能智能 地识别不同參数的个数和类型. 有人会问:那么printf中不是实现了智能识别參数吗? 那是由于函数 printf是从固定參数format字符串来分析出參数的类型,再调用va_arg 的来获取可变參数的.也就是说,你想实现智能识别可变參数的话是要通过在自己的程序里作推断来实现的. 比如,在C的经典教材《the c programming language》的7.3节中就给出了一个printf的可能实现方式,由于篇幅原因这里不再叙述。 四、小结:
1、标准C库的中的三个宏的作用仅仅是用来确定可变參数列表中每一个參数的内存地址。编译器是不知道參数的实际数目的。 2、在实际应用的代码中,程序猿必须自己考虑确定參数数目的办法,如
⑴在固定參数中设标志-- printf函数就是用这个办法。后面也有样例。 ⑵在预先设定一个特殊的结束标记。就是说多输入一个可变參数,调用时要将最后一个可变參数的值设置成这个特殊的值,在函数体中依据这个值推断是否达到參数的结尾。本文前面的代码就是採用这个办法.
不管採用哪种办法。程序猿都应该在文档中告诉调用者自己的约定。
3、实现可变參数的要点就是想办法取得每一个參数的地址,取得地址的办法由以下几个因素决定:
①函数栈的生长方向
②參数的入栈顺序
③CPU的对齐方式
④内存地址的表达方式
结合源码,我们能够看出va_list的实现是由④决定的,_INTSIZEOF(n)的引入则是由③决定的,他和①②又一起决定了va_start的实现,最后va_end的存在则是良好编程风格的体现。将不再使用的指针设为NULL,这样能够防止以后的误操作。
4、取得地址后,再结合參数的类型。程序猿就能够正确的处理參数了。 理解了以上要点,相信稍有经验的读者就能够写出适合于自己机器的实现来。
总览 (SYNOPSIS)
#include <stdio.h>
int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);
#include <stdarg.h>------------如今好像都在stdio.h中进行声明。
int vprintf(const char *format, va_list ap);
int vfprintf(FILE *stream, const char *format, va_list ap);
int vsprintf(char *str, const char *format, va_list ap);
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
这些函数返回打印的字符数量(不包含字符串结尾用的‘\0‘)。snprintf和vsnprintf的输出不会超过size 字节(包含了结尾的`\0'), 假设由于这个限制导致输出内容被截断, 则函数返回-1。
log(const char* format, ...)
{
va_list ap;
va_start (ap, format);
vfprintf (stderr, format, ap);
fprintf (stderr, "\n");
va_end (ap);
}

C中參数个数可变的函数的更多相关文章

  1. Web容器自己主动对HTTP请求中參数进行URLDecode处理

    这篇文章转载自 : Web容器自己主动对HTTP请求中參数进行URLDecode处理 如题.在Java中或许非常多人都没有注意到当我们发送一个http请求时,假设附带的參数被URLEncode之后,到 ...

  2. jQuery.ajax()方法中參数具体解析

    前言 在项目开发中,为了实现异步向服务端发起请求,最常常使用的就是jQuery.ajax方法了.刚開始需求比較简单,调用jQuery.ajax方法时要传的參数也就那几个常见的參数:url/data/d ...

  3. Java 参数个数可变的函数

    示例: package my_package; public class Test { public static void main(String[] args) { out("重庆师范大 ...

  4. c++ 参数个数可变的函数

    #include <stdio.h> #include <string.h> #include <stdarg.h> int addnum(int i,...) { ...

  5. python从excel中读取数据传给其他函数使用

    首先安装xlrd库 pip install xlrd 方法1: 表格内容如下: 场景描述,读取该表格A列数据,然后打印出数据 代码何解析如下: import xlrd #引入xlrd库 def exc ...

  6. C语言利用va_list、va_start、va_end、va_arg宏定义可变參数的函数

    在定义可变參数的函数之前,先来理解一下函数參数的传递原理: 1.函数參数是以栈这样的数据结构来存取的,在函数參数列表中,从右至左依次入栈. 2.參数的内存存放格式:參数的内存地址存放在内存的堆栈段中, ...

  7. c 语言函数可变參数的处理

    /************************************************************************* > File Name: va_list.c ...

  8. OC可变參数的函数实现va_start、va_end、va_list的使用

    一.简单介绍 我们常常在编程的时候看见类似这种代码,如图1.1 图1.1 或者是这种可变參数,如图1.2 图1.2 二.基本知识介绍 在学习怎样写这样的格式的函数前,先简介几个经常使用的宏: 下面摘自 ...

  9. C语言可变长參数实现原理

    微博:http://weibo.com/u/2203007022             (1)      C语言可变參数 我们能够从C语言的printf得出可变參数的作用.printf函数的原型例如 ...

随机推荐

  1. C#拆箱和装箱成本

    从原理上可以看出,装箱时,生成的是全新的引用对象,这会有时间损耗,也就是造成效率降低. 文章:.Net常见面试题整理(2)——装箱和拆箱 装箱,产生新的引用对象,并且赋值,然后引用. 拆箱,往往跟随着 ...

  2. UISearchController,SearchBar的教程-Swift

    如果你的应用程序里显示了大量的数据,滚动的查看大规模的列表会很慢,也会给人一种烦躁的感觉.在这种情况下,查询UISearchController, UISearchBar是极其重要的,可以让用户搜索特 ...

  3. java程序实现视频格式的转换

    http://blog.sina.com.cn/s/blog_96b60b0c01013mi5.html 原文地址:java程序实现视频格式的转换作者:笑看风云 flv格式转换--第一步 不定期更新. ...

  4. 【Luogu】P3950部落冲突(树链剖分)

    题目链接 状态奇差无比,sbt都能错一遍. 不动笔光想没有想到怎么做,画图之后发现一个很明显的性质…… 那就是两个开战的部落,其中一个是另一个的父亲. 所以在儿子那里加个权值.查询的时候树链剖分查询链 ...

  5. Redis 复制功能的几个重要方面

    Redis 复制功能的几个重要方面:1. 一个Master可以有多个Slave:2. Redis使用异步复制.从2.8开始,Slave会周期性(每秒一次)发起一个Ack确认复制流(replicatio ...

  6. 小程序语音红包开发中 汉字转拼音的问题 微信小程序红包开发遇到的坑

    公司最近在开发微信小程序的红包功能,语音红包需要用到文字转拼音的功能. 之前介绍过怎么将中文的汉字转为拼音的,具体看下面这篇文章. 微信语音红包小程序开发如何提高精准度 红包小程序语音识别精准度 微信 ...

  7. wireshark推荐书籍

    1 wireshark数据包分析实战 有中文版 2 wireshark网络分析 英文版 3 TCP/IP协议栈详解卷一

  8. javascript 函数重载另一种实现办法

    最近在读javascript忍者 感受下jquery作者 john Resig对于js的独到见解. 先上代码: function addMethod(object,name,fn){ var old ...

  9. Android 4.4 不休眠+不锁屏+默认中文+去除导航栏

    1.不休眠 frameworks/base/packages/SettingsProvider/res/values/defaults.xml 里面60000改成-1,就是不进入休眠. 这个文件还保存 ...

  10. Arduino可穿戴教程ArduinoIDE新建编辑源文件

    Arduino可穿戴教程ArduinoIDE新建编辑源文件 Arduino IDE新建源文件 Arduino IDE启动后默认就新建了一个源文件,如图2.20所示.新建的源文件名称是以sketch_开 ...