欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-06-03 13:18:39
...

包含有可变参数列表的函数(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标准库的实现中起到至关重要的作用):
可变参数列表实现机制与printf()函数源码分析

那么除了这四个宏以外,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()的作用如下图:
可变参数列表实现机制与printf()函数源码分析
注意: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的外平栈):
可变参数列表实现机制与printf()函数源码分析
对于这个入栈过程,如下所示:
可变参数列表实现机制与printf()函数源码分析

三个参数(栈大小为12Byte):
可变参数列表实现机制与printf()函数源码分析
参考资料:
glibc-2.21、VC6.0SRC
printf内部实现
_INTSIZEOF(n)

相关标签: printf 源码