可变参数列表实现机制与printf()函数源码分析

版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/Apollon_krj/article/details/79373966

包含有可变参数列表的函数(printf函数族、scanf函数族等),可以说是我们编程中接触最多的。但是关于可变参数列表的实现机制,却鲜有初学者去了解,即使是使用C/C++好几年的人也不全都了解。在秋招时,被问到printf()可变参数列表的实现机制,虽然之前有听说过有关va_list,但是没有做深入的了解,因此第一道题就被问死了。今天查找有关源码和相关资料,将该知识点进行整理,也算是亡羊补牢为时未晚。

一、glibc-2.21与VC6.0中printf()的源码:

在glibc-2.21中,printf()的实现如下:

int printf (const char *format, ...)
{
    va_list arg;
    int done;

    va_start (arg, format);
    done = vfprintf (stdout, format, arg);
    va_end (arg);

    return done;
}

而在VC6.0源码中,实现如下:

//C语言默认的调用约定是_cdecl而不是_stdcall。
//多数情况下,二者均可以使用,但此处只能使用_cdecl,不能用_stdcall
//_stdcall是由被调用函数清理堆栈(内平栈),而在不知道参数数量的时候,被调用者无法清理。
//_cdecl则是调用者清理堆栈(外平栈),调用者可以清楚地知道参数个数,因此函数返回后可以由调用者清理堆栈。
//换句话说,_stdcall不支持可变数量的参数,而_cdecl支持可变量参数。
int __cdecl printf (const char *format, ...)
/*
 * stdout 'PRINT', 'F'ormatted
 */
{
//VC6.0中实现看似复杂,实际上:
//_lock_str2()、_stbuf()、_ftbuf()、_unlock_str2()是为了线程安全做的处理,可以忽略
        va_list arglist;//va_list即char *
        int buffing;
        int retval;

        va_start(arglist, format);
        _ASSERTE(format != NULL);//判空,如果为空则出错,与assert()无异
        _lock_str2(1, stdout);
        buffing = _stbuf(stdout);
        retval = _output(stdout,format,arglist);
        _ftbuf(buffing, stdout);
        _unlock_str2(1, stdout);

        return(retval);
}

在以上两种不同平台的实现版本中(具体代码可参考文后“参考资料”),我们都看到va_list、va_start()等相同的部分。va_list、va-start()、va_arg()、va_end()是一个宏定义三个宏函数,这四个宏的存在,才使得可变参数列表能够实现。正如这四个宏所在的头文件所述:This file defines ANSI-style macros for accessing arguments of functions which take a variable number of arguments.(该文件定义了ANSI风格的宏,用于访问需要可变数量参数的函数的参数。)关于va_start()等的分布我们可以在整个VC6.0的SRC中搜索,以下是一小部分搜索结果(可见该宏在C标准库的实现中起到至关重要的作用):
这里写图片描述

那么除了这四个宏以外,vfprintf()函数和_output()函数则分别实现了glibc-2.21和VC6.0两种版本里对于可变参数列表的处理(函数内部是一个大的while()循环)。vfprintf()和_output()是printf()函数对于参数细节处理实现的核心,包含了printf函数族所有函数实现的代码。虽然是核心部分,但是由于代码量过大(仅仅一个函数实现近千行,且对于平台、版本等的预处理条件过多),且_outpur()和vfprintf()是对于printf整个函数族而言,所以我们的重点只放在va_list、va_start()、va_arg()、va_end()上来分析总结。以下列出_output()的变量定义(读者可根据定义的变量信息来大概猜测_output()的内容,也可以自己去看output.c源文件):

int hexadd;     /* offset to add to number to get 'a'..'f' */
TCHAR ch;       /* character just read */
int flags;      /* flag word -- see #defines above for flag values */
enum STATE state;   /* current state */
enum CHARTYPE chclass; /* class of current character */
int radix;      /* current conversion radix */
int charsout;   /* characters currently written so far, -1 = IO error */
int fldwidth;   /* selected field width -- 0 means default */
int precision;  /* selected precision  -- -1 means default */
TCHAR prefix[2];    /* numeric prefix -- up to two characters */
int prefixlen;  /* length of prefix -- 0 means no prefix */
int capexp;     /* non-zero = 'E' exponent signifient, zero = 'e' */
int no_output;  /* non-zero = prodcue no output for this specifier */
union {
    char *sz;   /* pointer text to be printed, not zero terminated */
    wchar_t *wz;
    } text;

int textlen;    /* length of the text in bytes/wchars to be printed.
                   textlen is in multibyte or wide chars if _UNICODE */
union {
    char sz[BUFFERSIZE];
#ifdef _UNICODE
    wchar_t wz[BUFFERSIZE];
#endif  /* _UNICODE */
    } buffer;
wchar_t wchar;      /* temp wchar_t */
int bufferiswide;   /* non-zero = buffer contains wide chars already */
char *heapbuf;      /* non-zero = test.sz using heap buffer to be freed */

textlen = 0;        /* no text yet */
charsout = 0;       /* no characters written yet */
state = ST_NORMAL;  /* starting state */
heapbuf = NULL;     /* not using heap-allocated buffer */

二、可变参数列表具体分析:

1、va_list、va_start、va_arg、va_end:

//stdarg.h
#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
//我们要考虑内存对齐,所以不能简单的用sizeof(v)
#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 )

在printf()实现的代码中:
va_start(arglist, format);
retval = _output(stdout,format,arglist);//或done = vfprintf (stdout, format, arg);
va_end(arglist);
三行是核心部分。

va_list是char *类型,即arglist指向参数列表首字节,在实际调取参数时要根据参数类型强制转换。

va_start()是初始化指向变参列表(即第二个参数)的指针arglist。为获取可变数目参数的函数的参数提供一种便捷手段。设置arglist为指向传给函数参数列表中的第一个可选参数的指针,且该参数必须是va_list类型。format是在参数列表中第一个可选参数前的必选参数。

va_arg()是用来确定后面每个参数的位置。因此va_arg()会在_output()之中多次使用。返回由arglist所指向的参数的值,且自增指向下一个参数的地址。t(type)为当前参数的类型,用来计算该参数的长度,确定下一个参数的起始位置。它可以在函数中应用多次,直到得到函数的所有参数为止,但必须在宏va_start()后面调用。

va_end()则是在获取所有的参数后,设置指针arglist为null。

关于所有参数所在栈中的位置以及va_start()和va_arg()的作用如下图:
这里写图片描述
注意:va_arg()执行完毕后的返回值和arglist不是同一个地址。返回值为当前参数地址(旧的arglist(ap)),arglist为下一个参数地址(新的arglist(ap))。

2、关于_output()函数其大体简化功能如下所示:

int __cdecl _output(FILE * stdout,const char * format,va_list ap)
{
    char ch;
    int charsout = 0;//目前写入的字符数

    //循环读取format字符串的每一个字符,当读取到'\0'时,即字符串末尾则循环结束
    //_T是一个宏,作用是让程序支持Unicode编码,因为Windows使用两种字符集:ANSI和UNICODE

    while((ch = *format++) != _T('\0') && charsout >= 0){
        if(ch!=‘%’)  {
            将ch写入到文件stdout中;
            continue;
        }
        switch(*format):
        case ’c’:{//%c
            将ap所指向的内容以字符形式写入文件stdout;
            ap指向下一个参数;
            break;
        }
        case ’d’:{//%d
            ……;
            format++;break;
        }
        case ‘f’:{//%f
            ……;
            format++;break;
        }
        …………//%x、%p...
    }
    return charsout;/* return value = number of characters written */
}

3、自己的可变参数列表的函数:

只要遵循
①包含stdarg.h头文件(或者自己写va_list、va_start()、va_arg()、va_end())
②va_list定义指针;
③va_start()初始化指针;
④va_arg()通过指针调用参数;
⑤va_end()指针置NULL,内存释放;
的使用顺序,我们自己也可以制作自己的可变参数列表函数。

如下是一个能够处理单个字符和字符串输出的简易的printf()函数:

#include <stdio.h>
//step ①
#include <stdarg.h>

void myprintf(const char *format, ...){
    //step ②
    va_list ap;
    char ch;
    //step ③
    va_start(ap, format);
    while(ch = *format++){
        if(ch!='%'){
            putchar(ch);
            continue;
        }
        switch(*format){
            case 'c':{
                //step ④
                char ch1 = va_arg(ap, char);
                putchar(ch1);
                format++;
                break;
            }
            case 's':{
                //step ④
                char *p = va_arg(ap, char *);
                fputs(p,stdout);
                format++;
                break;
            }
        }
    }
    //step ⑤
    va_end(ap);
}
int main(void)
{
    myprintf("%c\t%s\t%c\n",'A',"hello",'B');
    return 0;
}

关于程序执行过程中,可变参数列表的栈的变化,我们分别用不同个数参数来测试:

四个参数(栈大小为24Byte,可以看到回收栈资源属于调用者回收,即_cdecl的外平栈):
这里写图片描述
对于这个入栈过程,如下所示:
这里写图片描述

三个参数(栈大小为12Byte):
这里写图片描述
参考资料:
glibc-2.21、VC6.0SRC
printf内部实现
_INTSIZEOF(n)

猜你喜欢

转载自blog.csdn.net/Apollon_krj/article/details/79373966
今日推荐