一、什么是可变參数
我们在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. 九度oj 题目1171:C翻转

    题目描述: 首先输入一个5 * 5的数组,然后输入一行,这一行有四个数,前两个代表操作类型,后两个数x y代表需操作数据为以x y为左上角的那几个数据. 操作类型有四种:  1 2 表示:90度,顺时 ...

  2. 【转】hibernate映射(单向双向的一对多、多对一以及一对一、多对一)

    多对一关联映射:在多的一端加入一个外键指向一的一端,它维护的关系是多指向一 一对多关联映射:在多的一端加入一个外键指向一的一端,它维护的关系是一指向多 也就是说一对多和多对一的映射策略是一样的,只是站 ...

  3. pytorch下可采用visidom作为可视化工具

    2018/9/18更新  感觉tensorboardX插件更好用,已转用https://github.com/lanpa/tensorboardX 更新:新版visdom0.1.7安装方式为:cond ...

  4. BZOJ4517 [Sdoi2016]排列计数 【组合数 + dp】

    题目 求有多少种长度为 n 的序列 A,满足以下条件: 1 ~ n 这 n 个数在序列中各出现了一次 若第 i 个数 A[i] 的值为 i,则称 i 是稳定的.序列恰好有 m 个数是稳定的 满足条件的 ...

  5. poj1324 Holedox Moving

    Holedox Moving Time Limit: 5000MS   Memory Limit: 65536K Total Submissions: 16980   Accepted: 4039 D ...

  6. jenkins使用xvfb插件构建虚拟化显示屏自动化测试

    1.linux服务器安装xvfb,并启动 参考我的博客:http://www.cnblogs.com/lincj/p/5468505.html 或者网上搜索一下进行安装 2.jenkins安装xvfb ...

  7. 【python接口自动化】httpUtils

    # coding=utf8 import requests from common.logger import Logger import logging class httpUtils: logge ...

  8. Linq技巧1——关联实体查询排序

    假如想查询拖欠按揭超过30天的银行帐号,同时查询出他们的单据,并且需要按照单据日期进行排序,这样可以首先看到最近的单据,方便找出问题. 大多数人都知道EF可以使用Include()热加载关系实体,例如 ...

  9. SharepPoint 2013安装体会

    SharePoint 2013出来了,一直没有找到合适的机器来安装.前天腾出来一台内存8G的机器,决定在Hyper-V上安装在一台虚机,然后装个Windows 2012,再装SharePoint 20 ...

  10. UVa12333 Revenge of Fibonacci

    高精度 trie 暴力预处理出前100000个fibonacci数,将每个数的前40位数字串插入到trie中,记录每个结点最早可以由哪个数字串到达. 然后依次回答询问即可. 存fibonacci数的数 ...