C中參数个数可变的函数
一、什么是可变參数
我们在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中參数个数可变的函数的更多相关文章
- Web容器自己主动对HTTP请求中參数进行URLDecode处理
这篇文章转载自 : Web容器自己主动对HTTP请求中參数进行URLDecode处理 如题.在Java中或许非常多人都没有注意到当我们发送一个http请求时,假设附带的參数被URLEncode之后,到 ...
- jQuery.ajax()方法中參数具体解析
前言 在项目开发中,为了实现异步向服务端发起请求,最常常使用的就是jQuery.ajax方法了.刚開始需求比較简单,调用jQuery.ajax方法时要传的參数也就那几个常见的參数:url/data/d ...
- Java 参数个数可变的函数
示例: package my_package; public class Test { public static void main(String[] args) { out("重庆师范大 ...
- c++ 参数个数可变的函数
#include <stdio.h> #include <string.h> #include <stdarg.h> int addnum(int i,...) { ...
- python从excel中读取数据传给其他函数使用
首先安装xlrd库 pip install xlrd 方法1: 表格内容如下: 场景描述,读取该表格A列数据,然后打印出数据 代码何解析如下: import xlrd #引入xlrd库 def exc ...
- C语言利用va_list、va_start、va_end、va_arg宏定义可变參数的函数
在定义可变參数的函数之前,先来理解一下函数參数的传递原理: 1.函数參数是以栈这样的数据结构来存取的,在函数參数列表中,从右至左依次入栈. 2.參数的内存存放格式:參数的内存地址存放在内存的堆栈段中, ...
- c 语言函数可变參数的处理
/************************************************************************* > File Name: va_list.c ...
- OC可变參数的函数实现va_start、va_end、va_list的使用
一.简单介绍 我们常常在编程的时候看见类似这种代码,如图1.1 图1.1 或者是这种可变參数,如图1.2 图1.2 二.基本知识介绍 在学习怎样写这样的格式的函数前,先简介几个经常使用的宏: 下面摘自 ...
- C语言可变长參数实现原理
微博:http://weibo.com/u/2203007022 (1) C语言可变參数 我们能够从C语言的printf得出可变參数的作用.printf函数的原型例如 ...
随机推荐
- 【转】hibernate延迟加载和抓取策略
一.延迟加载 1.简单查询get,load 针对对象本身延迟或即时 当使用load方法来得到一个对象时,此时hibernate会使用延迟加载的机制来加载这个对象,即:当我们使用session.load ...
- 【Luogu】P2801教主的魔法(分块)
题目链接 激动qwq.这是我A的第一道分块. 分块之后对块内元素暴力sort.修改的时候对于整块打个标记,查询的时候只需要查C-tag就行了 对于非整块,暴力修改,改完之后sort 对于查询……非整块 ...
- kb-07线段树-12--二分查找区间边界
/* hdu4614 本题刚开始想能不能记录该区间最前面开始的点,最后面的点,区间空的数量:但是病不行 然后线段树的本质是区间操作,所以!这题主要就是区间的空的全放满,只要定出区间的边界就好办了: 这 ...
- jquery工具方法总结
$.extend 对象合并,支持深拷贝 $.each 相当于array.each或object.each,可以遍历数组和对象 $.grep 相当于array.filter $.map 相当于array ...
- 刷题总结——天使玩偶(bzoj2716)
题目: Description Input Output HINT 题解: 学了cdq后近期最后一道题···然而tm还是搞了1个半小时才tm搞出来······ 先说思路:对于绝对值,我们采取类似于旋转 ...
- [TJOI2017] 城市 (树的直径,贪心)
题目链接 Solution 这道题,调了我一晚上... 一直80分 >_<|| ... 考虑到几点: 分开任意一条边 \(u\) ,那么其肯定会断成两棵树. 肯定是分开直径上的边最优,否则 ...
- el-select绑定值为对象时,报错[Vue warn]: <transition-group> children must be keyed: <ElTag>
解决方法: <el-select v-model="syncParams.toSlaveList" multiple value-key="ip" pla ...
- django无法同步数据库 Error loading MySQLdb module: No module named ‘MySQLdb‘
最近在学习Python,打算先看两个在线教程,再在github上找几个开源的项目练习一下,在学到“被解放的姜戈”时遇到django同步数据库时无法执行的错误,记录一下. 错误现象: 执行python ...
- JS判断SharePoint页面编辑状态
这篇博客主要讲使用不同的客户端方式来判断页面的编辑模式. 1.当页面处于发布状态时,可以使用下面两种方式:if(g_disableCheckoutInEditMode == true) { ale ...
- vue2 如何通过router传递参数
当需要router-link传递参数的时候 vue2 如何做 记录下来备忘 1.通过vue页面传递参数 <router-link :to="{ path:'./attachment', ...