可变參数学习笔记

一、什么是可变參数

我们在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 函数的改写自己还没完毕,等所有做好再贴上代码



posted @ 2014-06-28 10:35  hrhguanli  阅读(215)  评论(0编辑  收藏  举报