可变參数学习笔记

一、什么是可变參数

我们在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函数以及这些可变參数的函数编译器是怎样实现,这个问题却一直困扰了我好久。本文就这个问题进行一些探讨,希望能对大家有些帮助.

二、写一个简单的可变參数的C函数

先看样例程序。该函数至少有一个整数參数,其后占位符…,表示后面參数的个数不定. 在这个样例里,全部的输入參数必须都是整数,函数的功能仅仅是打印全部參数的值。这也是老师布置的作业

先看源码:

#include <stdio.h>
#include "stdarg.h"

int count(int start, ...){
 va_list valist;
 int count = 0;
 int va_val = start;
 va_start(valist, start);

 do{
	count++;
	printf("%d ", va_val);
	va_val = va_arg(valist, int);
 }while(va_val != -1);

 printf("\n");
return count;
}

从这个函数的实现能够看到,我们使用可变參数应该有下面步骤:

⑴因为在程序中将用到下面这些宏:

void va_start( va_list arg_ptr, prev_param );

type va_arg( va_list arg_ptr, type );

void va_end( va_list arg_ptr );

va在这里是variable-argument(可变參数)的意思.

这些宏定义在stdarg.h中,所以用到可变參数的程序应该包括这个头文件.

⑵函数里首先定义一个va_list型的变量,这里是arg_ptr,这个变

量是存储參数地址的指针.由于得到參数的地址之后,再结合參数的类型,才干得到參数的值。

⑶然后用va_start宏初始化⑵中定义的变量arg_ptr,这个宏的第二个參数是可变參数列表的前一个參数,即最后一个固定參数.

⑷然后依次用va_arg宏使arg_ptr返回可变參数的地址,得到这个地址之后,结合參数的类型,就能够得到參数的值。

⑸设定结束条件,这里的条件就是推断參数值是否为-1。注意被调的函数在调用时是不知道可变參数的正确数目的,程序猿必须自己在代码中指明结束条件。至于为什么它不会知道參数的数目,读者在看完这几个宏的内部实现机制后,自然就会明确。

(二)可变參数在编译器中的处理

我们知道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、取得地址后,再结合參数的类型,程序猿就能够正确的处理參数了。理解了以上要点,相信稍有经验的读者就能够写出适合于自己机器的实现来。以下就是一个样例

(五)扩展——自己实现简单的可变參数的函数。

以下是一个简单的printf函数的实现,參考了<The C Programming Language>中的156页的样例,读者能够结合书上的代码与本文參照。

#include "stdio.h"

#include "stdlib.h"

void myprintf(char* fmt, ...)        //一个简单的相似于printf的实现,//參数必须都是int 类型

{ 

    char* pArg=NULL;               //等价于原来的va_list 

    char c;

    pArg = (char*) &fmt;          //注意不要写成p = fmt !!由于这里要对//參数取址,而不是取值

    pArg += sizeof(fmt);         //等价于原来的va_start          

    do

    {

        c =*fmt;

        if (c != ‘%‘)

        {

            putchar(c);            //照原样输出字符

        }

        else

{

//按格式字符输出数据

            switch(*++fmt) 

{

            case ‘d‘:

                printf("%d",*((int*)pArg));           

                break;

            case ‘x‘:

                printf("%#x",*((int*)pArg));

                break;

            default:

                break;

            } 

            pArg += sizeof(int);               //等价于原来的va_arg

        }

        ++fmt;

    }while (*fmt != ‘\0‘); 

    pArg = NULL;                               //等价于va_end

    return; 

}

int main(int argc, char* argv[])

{

    int i = 1234;

    int j = 5678;

    myprintf("the first test:i=%d\n",i,j); 

    myprintf("the secend test:i=%d; %x;j=%d;\n",i,0xabcd,j); 

    system("pause");

    return 0;

}

在intel+win2k+vc6的机器运行结果例如以下:

the first test:i=1234

the secend test:i=1234; 0xabcd;j=5678;

參考:http://bbs.csdn.net/topics/120005809

另外最后那个 printf 函数的改写自己还没完毕,等所有做好再贴上代码

可变參数学习笔记

时间: 2024-12-18 22:22:38

可变參数学习笔记的相关文章

python可变參数调用函数问题

一直使用python实现一些想法,近期在使用python的过程中出现这样一个需求,定义了一个函数.第一个是普通參数.第二个是默认參数,后面还有可变參数,在最初学习python的时候,都知道非keyword可变參数和keyword可变參数两种,调用的方式或许多种多样,这里主要提出一个比較隐含的问题.并将各种可能出现的情况进行了探讨. 函数声明格式 python尽管不支持函数重载,可是通过对函数參数的众多特性的支持基本弥补了. 函数声明的通式例如以下: def func(argv1,argv2...

java之 ------ 可变參数和卫条件

可变參数:适用于參数个数不确定.类型确定的情况,java把可变參数当做数组处理. 可变參数必须位于最后一项.当可变參数个数多于一个时,必将有一个不是最后一项,所以仅仅支持有一个可变參数. 可变參数的书写形式如: int add(int x,int ...args){  方法体 } ... 位于变量类型和变量名之间.前后有无空格都能够: 调用可变參数的方法时.编译器为该可变參数隐含创建一个数组,在方法体中以数组的形式訪问可变參数. 条件逻辑推断型的应用(模拟人工推理) /* 有人邀请A,B,C,D

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

/************************************************************************* > File Name: va_list.c > Author: zshh0604 > Mail: [email protected] > Created Time: 2014年10月14日 星期二 15时16分09秒 **********************************************************

iOS 处理方法中的可变參数

## iOS 处理方法中的可变參数 近期写了一个自己定义的对话框的demo,想模仿系统的UIAlertView的实现方式.对处理可变參数的时候,遇到了小问题,于是谷歌了一下.写下了处理问题的方法.记录下来,以备后需. 代码实现 - (instancetype)initWithTitle:(NSString *)title message:(NSString *)message delegate:(id)delegate cancelButtonTitle:(NSString *)cancelBu

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

在定义可变參数的函数之前,先来理解一下函数參数的传递原理: 1.函数參数是以栈这样的数据结构来存取的,在函数參数列表中,从右至左依次入栈. 2.參数的内存存放格式:參数的内存地址存放在内存的堆栈段中,在运行函数的时候,从最后一个(最右边)參数開始入栈.因此栈底高地址,栈顶低地址,举个样例说明一下: void test(int a, float b, char c); 那么,在调用test函数的时候,实參char c先进栈,然后是float b,最后才是int a,因此在内存中变量的存放次序是c-

可变參数

C++中传递參数时,有时候不确定參数的数量,这时候能够使用可变參数.例如以下: 1.std::initializer_list C++11标准,使用方法类似vector,參数类型需同样.使用方法例如以下: #include <initializer_list> void  method(std::initializer_list<int> il) { for(auto i: il) { printf("%d", i) ; } } int _tmain(int a

java课堂练习之可变參数与卫条件

/* 有人邀请A,B,C,D,E,F 6个人參加一项会议,这6个人有些奇怪.由于他们有非常多要求,已知: 1)A,B两人至少有1人參加会议: 2)A,E,F 3人中有2人參加会议. 3)B和C两人一致决定,要么两人都去,要么两人都不去: 4)A,D两人中仅仅1人參加会议. 5)C,D两人中也仅仅要1人參加会议: 6)假设D不去,那么E也决定不去. 那么最后到底有哪几个人參加了会议呢? */ 可变參数必须位于最后一项. 当可变參数个数多于一个时.必将有一个不是最后一项.所以仅仅支持有一个可变參数.

宏定义之二(连接符,可变參数,文件名称,行号,函数名)

宏的高级使用--##,__VA_ARGS__, __FILE__, __FUNCTION__等 转自 http://blog.csdn.net/yiya1989/article/details/7849588 先说一下本文中会提到的内容:##,__VA_ARGS__, __FILE__, __LINE__ , __FUNCTION__等 宏变量: 先举一个样例,会用到上面这些宏: [cpp] view plaincopy #define myprintf(...) printk("[lch]:F

可变參数列表

再c++里有函数重载.在c里有可变參数列表,用stdarg宏实现定义在<stdarg.h>的头文件里,当中有3个宏:va_start(va_list的变量名,省略号前最后一个有名字的參数)用于初始化,va_arg(va_list的变量名,可变參数类型)用于取出可变參数.va_end(va_list的变量名)用于表明结束取值.用va_list声明一个类型为va_list的变量如arg #include<stdio.h> #include<stdarg.h> float a