C中printf函数的实现原理

一、printf函数的实现原理

在C/C++中,对函数参数的扫描是从后向前的。C/C++的函数参数是通过压入堆栈的方式来给函数传参数的(堆栈是一种先进后出的数据结构),最先压入的参数最后出来,在计算机的内存中,数据有2块,一块是堆,一块是栈(函数参数及局部变量在这里),而栈是从内存的高地址向低地址生长的,控制生长的就是堆栈指针了,最先压入的参数是在最上面,就是说在所有参数的最后面,最后压入的参数在最下面,结构上看起来是第一个,所以最后压入的参数总是能够被函数找到,因为它就在堆栈指针的上方。printf的第一个被找到的参数就是那个字符指针,就是被双引号括起来的那一部分,函数通过判断字符串里控制参数的个数来判断参数个数及数据类型,通过这些就可算出数据需要的堆栈指针的偏移量了,下面给出printf("%d,%d",a,b);(其中a、b都是int型的)的汇编代码

.section
.data
string out = "%d,%d"
push b
push a
push $out
call printf

你会看到,参数是最后的先压入栈中,最先的后压入栈中,参数控制的那个字符串常量是最后被压入的,所以这个常量总是能被找到的。

二、可变参数表函数的设计

标准库提供的一些参数的数目可以有变化的函数。例如我们很熟悉的printf,它需要有一个格式串,还应根据需要为它提供任意多个“其他参数”。这种函数被称作“具有变长度参数表的函数”,或简称为“变参数函数”。我们写程序中有时也可能需要定义这种函数。要定义这类函数,就必须使用标准头文件<stdarg.h>,使用该文件提供的一套机制,并需要按照规定的定义方式工作。本节介绍这个头文件提供的有关功能,它们的意义和使用,并用例子说明这类函数的定义方法。

C中变长实参头文件stdarg.h提供了一个数据类型va_list和三个宏(va_start、va_arg和va_end),用它们在被调用函数不知道参数个数和类型时对可变参数表进行测试,从而为访问可变参数提供了方便且有效的方法。va_list是一个char类型的指针,当被调用函数使用一个可变参数时,它声明一个类型为va_list的变量,该变量用来指向va_arg和va_end所需信息的位置。下面给出va_list在C中的源码:

typedef char *  va_list;

void va_start(va_list ap,lastfix)是一个宏,它使va_list类型变量ap指向被传递给函数的可变参数表中的第一个参数,在第一次调用va_arg和va_end之前,必须首先调用该宏。va_start的第二个参数lastfix是传递给被调用函数的最后一个固定参数的标识符。va_start使ap只指向lastfix之外的可变参数表中的第一个参数,很明显它先得到第一个参数内存地址,然后又加上这个参数的内存大小,就是下个参数的内存地址了。下面给出va_start在C中的源码:

#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
#define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF(v) )   //得到可变参数中第一个参数的首地址

type va_arg(va_list ap,type)也是一个宏,其使用有双重目的,第一个是返回ap所指对象的值,第二个是修改参数指针ap使其增加以指向表中下一个参数。va_arg的第二个参数提供了修改参数指针所必需的信息。在第一次使用va_arg时,它返回可变参数表中的第一个参数,后续的调用都返回表中的下一个参数,下面给出va_arg在C中的源码:

#define va_arg(ap,type)    ( *(type *)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)) )    //将参数转换成需要的类型,并使ap指向下一个参数

在使用va_arg时,要注意第二个参数所用类型名应与传递到堆栈的参数的字节数对应,以保证能对不同类型的可变参数进行正确地寻址,比如实参依次为char型、char* 型、int型和float型时,在va_arg中它们的类型则应分别为int、char *、int和double.

void va_end(va_list ap)也是一个宏,该宏用于被调用函数完成正常返回,功能就是把指针ap赋值为0,使它不指向内存的变量。下面给出va_end在C中的源码:

#define va_end(ap)      ( ap = (va_list)0 )

va_end必须在va_arg读完所有参数后再调用,否则会产生意想不到的后果。特别地,当可变参数表函数在程序执行过程中不止一次被调用时,在函数体每次处理完可变参数表之后必须调用一次va_end,以保证正确地恢复栈。

一个变参数函数至少需要有一个普通参数,其普通参数可以具有任何类型。在函数定义中,这种函数的最后一个普通参数除了一般的用途之外,还有其他特殊用途。下面从一个例子开始说明有关的问题。

假设我们想定义一个函数sum,它可以用任意多个整数类型的表达式作为参数进行调用,希望sum能求出这些参数的和。这时我们应该将sum定义为一个只有一个普通参数,并具有变长度参数表的函数,这个函数的头部应该是(函数原型与此类似):

int sum(int n, ...)

我们实际上要求在函数调用时,从第一个参数n得到被求和的表达式个数,从其余参数得到被求和的表达式。在参数表最后连续写三个圆点符号,说明这个函数具有可变数目的参数。凡参数表具有这种形式(最后写三个圆点),就表示定义的是一个变参数函数。注意,这样的三个圆点只能放在参数表最后,在所有普通参数之后。

下面假设函数sum里所用的va_list类型的变量的名字是vap。在能够用vap访问实际参数之前,必须首先用宏va_start对这个变量进行初始化。宏va_start的类型特征可以大致描述为:

va_start(va_list vap, 最后一个普通参数)

在函数sum里对vap初始化的语句应当写为:

va_start(vap, n); 相当于  char *vap= (char *)&n + sizeof(int);

此时vap正好指向n后面的可变参数表中的第一个参数。

在完成这个初始化之后,我们就可以通过另一个宏va_arg访问函数调用的各个实际参数了。宏va_arg的类型特征可以大致地描述为:

类型 va_arg(va_list vap, 类型名)

在调用宏va_arg时必须提供有关实参的实际类型,这一类型也将成为这个宏调用的返回值类型。对va_arg的调用不仅返回了一个实际参数的值(“当前”实际参数的值),同时还完成了某种更新操作,使对这个宏va_arg的下次调用能得到下一个实际参数。对于我们的例子,其中对宏va_arg的一次调用应当写为:

v = va_arg(vap, int);

这里假定v是一个有定义的int类型变量。

在变参数函数的定义里,函数退出之前必须做一次结束动作。这个动作通过对局部的va_list变量调用宏va_end完成。这个宏的类型特征大致是:

void va_end(va_list vap);

三、栈中参数分布以及宏使用后的指针变化说明

在这里插入图片描述
下面是函数sum的完整定义,从中可以看到各有关部分的写法:

#include<iostream>
using namespace std;
#include<stdarg.h>
 
int sum(int n,...)
{
	int i , sum = 0;
	va_list vap;
	va_start(vap , n);     //指向可变参数表中的第一个参数
	for(i = 0 ; i < n ; ++i)
		sum += va_arg(vap , int);     //取出可变参数表中的参数,并修改参数指针vap使其增加以指向表中下一个参数
	va_end(vap);    //把指针vap赋值为0
	return sum;
}
int main(void)
{
	int m = sum(3 , 45 , 89 , 72);
	cout<<m<<endl;
	return 0;
}

这里首先定义了va_list变量vap,而后对它初始化。循环中通过va_arg取得顺序的各个实参的值,并将它们加入总和。最后调用va_end结束。

下面是调用这个函数的几个例子:

k = sum(3, 5+8, 7, 26*4);
m = sum(4, k, k*(k-15), 27, (k*k)/30);

函数sum中首先定义了可变参数表指针vap,而后通过va_start ( vap, n )取得了参数表首地址(赋值给了vap),其后的for循环则用来遍历可变参数表。这种遍历方式与我们在数据结构教材中经常看到的遍历方式是类似的。

函数sum看起来简洁明了,但是实际上printf的实现却远比这复杂。sum函数之所以看起来简单,是因为:

  • sum函数可变参数表的长度是已知的,通过num参数传入;
  • sum函数可变参数表中参数的类型是已知的,都为int型。

而printf函数则没有这么幸运。首先,printf函数可变参数的个数不能轻易的得到,而可变参数的类型也不是固定的,需由格式字符串进行识别(由%f、%d、%s等确定),因此则涉及到可变参数表的更复杂应用。

在这个函数中,需通过对传入的格式字符串(首地址为lpStr)进行识别来获知可变参数个数及各个可变参数的类型,具体实现体现在for循环中。譬如,在识别为%d后,做的是va_arg ( vap, int ),而获知为%l和%lf后则进行的是va_arg ( vap, long )、va_arg ( vap, double )。格式字符串识别完成后,可变参数也就处理完了。

在编写和使用具有可变数目参数的函数时,有几个问题值得注意。

  • 第一:调用va_arg将更新被操作的va_list变量(如在上例的vap),使下次调用可以得到下一个参数。在执行这个操作时,va_arg并不知道实际有几个参数,也不知道参数的实际类型,它只是按给定的类型完成工作。因此,写程序的人应在变参数函数的定义里注意控制对实际参数的处理过程。上例通过参数n提供了参数个数的信息,就是为了控制循环。标准库函数printf根据格式串中的转换描述的数目确定实际参数的个数。如果这方面信息有误,函数执行中就可能出现严重问题。编译程序无法检查这里的数据一致性问题,需要写程序的人自己负责。在前面章节里,我们一直强调对printf等函数调用时,要注意格式串与其他参数个数之间一致性,其原因就在这里。
  • 第二:编译系统无法对变参数函数中由三个圆点代表的那些实际参数做类型检查,因为函数的头部没有给出这些参数的类型信息。因此编译处理中既不会生成必要的类型转换,也不会提供类型错误信息。考虑标准库函数printf,在调用这个函数时,不但实际参数个数可能变化,各参数的类型也可能不同,因此不可能有统一方式来描述它们的类型。对于这种参数,C语言的处理方式就是不做类型检查,要求写程序的人保证函数调用的正确性。

假设我们写出下面的函数调用:

k = sum(6, 2.4, 4, 5.72, 6, 2);

编译程序不会发现这里参数类型不对,需要做类型转换,所有实参都将直接传给函数。函数里也会按照内部定义的方式把参数都当作整数使用。编译程序也不会发现参数个数与6不符。这一调用的结果完全由编译程序和执行环境决定,得到的结果肯定不会是正确的。

四、源代码

/*
* =====================================================================================
*
*       Filename:  printf.c
*
*    Description:  printf 函数的实现
*
*        Version:  1.0
*        Created:  2010年12月12日 14时48分18秒
*       Revision:  none
*       Compiler:  gcc
*
*         Author:  Yang Shao Kun (), cdutyangshaokun@163.com
*        Company:  College of Information Engineering of CDUT
*
* =====================================================================================
*/
要了解变参函数的实现,首先我们的弄清楚几个问题:
1:该函数有几个参数。
2:该函数增样去访问这些参数。
3:在访问完成后,如何从堆栈中释放这些参数。
对于c语言,它的调用规则遵循_cdedl调用规则。
在_cdedl规则中:1.参数从右到左依次入栈
                2.调用者负责清理堆栈
                3.参数的数量类型不会导致编译阶段的错误
要弄清楚变参函数的原理,我们需要解决上述的3个问题,其中的第三个问题,根据调
用原则,那我们现在可以不管。
要处理变参函数,需要用到 va_list 类型,和 va_start,va_end,va_arg 宏定义。我
看网上的许多资料说这些参数都是定义在stdarg.h这个头文件中,但是在我的linux机
器上,我的版本是fedorea 14,用vim访问的时候,确是在 acenv.h这个头文件中,估
计是内核的版本不一样的原因吧!!!
上面的这几个宏和其中的类型,在内核中是这样来实现的:
#ifndef _VALIST
#define _VALIST
typedef char *va_list;
#endif                /* _VALIST */
/*
* Storage alignment properties
*/
#define  _AUPBND                (sizeof (acpi_native_int) - 1)
#define  _ADNBND                (sizeof (acpi_native_int) - 1)
/*
* Variable argument list macro definitions
*/
#define _bnd(X, bnd)            (((sizeof (X)) + (bnd)) & (~(bnd)))
#define va_arg(ap, T)           (*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND))))
#define va_end(ap)              (void) 0
#define va_start(ap, A)         (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))
#endif                /* va_arg */
首先来看 va_list 类型,其实这是一个字符指针。
va_start,是使ap指针指向变参函数中的下一个参数。
我们现在来看_bnd 宏的实现:
首先:
typedef s32 acpi_native_int;
typedef int            s32;
看出来,acpi_native_int 其实就是 int 类型,那么,
#define  _AUPBND                (sizeof (acpi_native_int) - 1)
#define  _ADNBND                (sizeof (acpi_native_int) - 1)
这两个值就应该是相等的,都-等于:3==0x00000003,按位取反后的结果就是:0xfffff
ffc,因此,
_bnd(x,bnd)宏在32位机下就是
(((sizeof (X)) + (3)) & (0xfffffffc)),那么作用就很明显是取4的整数,就相当与
整数除法后取ceiling--向上取整。
回过头来看 va_start(ap,A),初始化参数指针ap,将函数参数A右边右边第一个参数地
址赋值给ap,A必须是一个参数的指针,所以,此种类型函数至少要有一个普通的参数
,从而提供给va_start ,这样va_start才能找到可变参数在栈上的位置。
va_arg(ap,T),获得ap指向参数的值,同时使ap指向下一个参数,T用来指名当前参数类
型。
va_end 在有些简单的实现中不起任何作用,在有些实现中可能会把ap改成无效值,这
里,是把ap指针指向了 NULL。
c标准要求在同一个函数中va_start 和va_end 要配对的出现。
那么到现在,处理多参数函数的步骤就是
1:首先是要保证该函数至少有一个参数,同时用...参数申明函数是变参函数。
2:在函数内部以va_start(ap,A)宏初始化参数指针。
3:用va_arg(ap,T)从左到右逐个取参数值。
printf()格式转换的一般形式如下:
%[flags][width][.prec][type]
prec有一下几种情况:
                    正整数的最小位数
                    在浮点数中表示的小数位数
                    %g格式表示有效为的最大值
                    %s格式表示字符串的最大长度
                    若为*符号表示下个参数值为最大长度
width:为输出的最小长度,如果这个输出参数并非数值,而是*符号,则表示以下一个参数当做输出长度。
现在来看看我们的printf函数的实现,在内核中printf函数被封装成下面的代码:
static char sprint_buf[1024];
int printf(const char *fmt, ...)
{
    va_list args;
    int n;
    va_start(args, fmt);//初始化参数指针
    n = vsprintf(sprint_buf, fmt, args);/*函数放回已经处理的字符串长度*/
    va_end(args);//与va_start 配对出现,处理ap指针
    if (console_ops.write)
        console_ops.write(sprint_buf, n);/*调用控制台的结构中的write函数,将sprintf_buf中的内容输出n个字节到设备*/
    return n;
}
vs_printf函数的实现代码是:
int vsprintf(char *buf, const char *fmt, va_list args)
{
    int len;
    unsigned long long num;
    int i, base;
    char * str;
    const char *s;/*s所指向的内存单元不可改写,但是s可以改写*/
    int flags;        /* flags to number() */
    int field_width;    /* width of output field */
    int precision;        /* min. # of digits for integers; max
                   number of chars for from string */
    int qualifier;        /* 'h', 'l', or 'L' for integer fields */
                            /* 'z' support added 23/7/1999 S.H.    */
                /* 'z' changed to 'Z' --davidm 1/25/99 */
    for (str=buf ; *fmt ; ++fmt)
    {
        if (*fmt != '%') /*使指针指向格式控制符'%,以方便以后处理flags'*/
        {
            *str++ = *fmt;
            continue;
        }
        /* process flags */
        flags = 0;
        repeat:
            ++fmt;        /* this also skips first '%'--跳过格式控制符'%' */
            switch (*fmt)
            {
                case '-': flags |= LEFT; goto repeat;/*左对齐-left justify*/
                case '+': flags |= PLUS; goto repeat;/*p plus with ’+‘*/
                case ' ': flags |= SPACE; goto repeat;/*p with space*/
                case '#': flags |= SPECIAL; goto repeat;/*根据其后的转义字符的不同而有不同含义*/
                case '0': flags |= ZEROPAD; goto repeat;/*当有指定参数时,无数字的参数将补上0*/
            }
//#define ZEROPAD    1        /* pad with zero */
//#define SIGN    2        /* unsigned/signed long */
//#define PLUS    4        /* show plus */
//#define SPACE    8        /* space if plus */
//#define LEFT    16        /* left justified */
//#define SPECIAL    32        /* 0x */
//#define LARGE    64        /* use 'ABCDEF' instead of 'abcdef' */
        /* get field width ----deal 域宽 取当前参数字段宽度域值,放入field_width 变量中。如果宽度域中是数值则直接取其为宽度值。 如果宽度域中是字符'*',表示下一个参数指定宽度。因此调用va_arg 取宽度值。若此时宽度值小于0,则该负数表示其带有标志域'-'标志(左靠齐),因此还需在标志变量中添入该标志,并将字段宽度值取为其绝对值。  */
        field_width = -1;
        if ('0' <= *fmt && *fmt <= '9')
            field_width = skip_atoi(&fmt);
        else if (*fmt == '*')
        {
            ++fmt;/*skip '*' */
            /* it's the next argument */
            field_width = va_arg(args, int);
            if (field_width < 0) {
                field_width = -field_width;
                flags |= LEFT;
            }
        }
        /* get the precision-----即是处理.pre 有效位 */
        precision = -1;
        if (*fmt == '.')
        {
            ++fmt;
            if ('0' <= *fmt && *fmt <= '9')
                precision = skip_atoi(&fmt);
            else if (*fmt == '*') /*如果精度域中是字符'*',表示下一个参数指定精度。因此调用va_arg 取精度值。若此时宽度值小于0,则将字段精度值取为0。*/
            {
                ++fmt;
                /* it's the next argument */
                precision = va_arg(args, int);
            }
            if (precision < 0)
                precision = 0;
        }
        /* get the conversion qualifier 分析长度修饰符,并将其存入qualifer 变量*/
        qualifier = -1;
        if (*fmt == 'l' && *(fmt + 1) == 'l')
        {
            qualifier = 'q';
            fmt += 2;
        }
        else if (*fmt == 'h' || *fmt == 'l' || *fmt == 'L'|| *fmt == 'Z')
        {
            qualifier = *fmt;
            ++fmt;
        }
        /* default base */
        base = 10;
        /*处理type部分*/
        switch (*fmt)
        {
            case 'c':
                if (!(flags & LEFT))/*没有左对齐标志,那么填充field_width-1个空格*/
                    while (--field_width > 0)
                    *str++ = ' ';
                    *str++ = (unsigned char) va_arg(args, int);
                    while (--field_width > 0)/*不是左对齐*/
                    *str++ = ' ';/*在参数后输出field_width-1个空格*/
                    continue;
            /*如果转换参数是s,则,表示对应的参数是字符串,首先取参数字符串的长度,如果超过了精度域值,则取精度域值为最大长度*/
            case 's':
                s = va_arg(args, char *);
                if (!s)
                    s = "";
                    len = strnlen(s, precision);/*字符串的长度,最大为precision*/
                    if (!(flags & LEFT))
                    while (len < field_width--)/*如果不是左对齐,则左侧补空格=field_width-len个空格*/
                    *str++ = ' ';
                    for (i = 0; i < len; ++i)
                    *str++ = *s++;
                    while (len < field_width--)/*如果是左对齐,则右侧补空格数=field_width-len*/
                    *str++ = ' ';
                    continue;
/*如果格式转换符是'p',表示对应参数的一个指针类型。此时若该参数没有设置宽度域,则默认宽度为8,并且需要添零。然后调用number()*/
            case 'p':
                if (field_width == -1)
                {
                    field_width = 2*sizeof(void *);
                    flags |= ZEROPAD;
                }
                str = number(str,(unsigned long) va_arg(args, void *), 16,
                        field_width, precision, flags);
                continue;
        // 若格式转换指示符是'n',则表示要把到目前为止转换输出的字符数保存到对应参数指针指定的位置中。
        // 首先利用va_arg()得该参数指针,然后将已经转换好的字符数存入该指针所指的位置
            case 'n':
            if (qualifier == 'l')
            {
                long * ip = va_arg(args, long *);
                *ip = (str - buf);
            }
            else if (qualifier == 'Z')
            {
                size_t * ip = va_arg(args, size_t *);
                *ip = (str - buf);
            }
            else
            {
                int * ip = va_arg(args, int *);
                *ip = (str - buf);
            }
            continue;
    //若格式转换符不是'%',则表示格式字符串有错,直接将一个'%'写入输出串中。
    // 如果格式转换符的位置处还有字符,则也直接将该字符写入输出串中,并返回到继续处理
    //格式字符串。
            case '%':
            *str++ = '%';
            continue;
            /* integer number formats - set up the flags and "break" */
            case 'o':
            base = 8;
            break;
            case 'X':
            flags |= LARGE;
            case 'x':
            base = 16;
            break;
// 如果格式转换字符是'd','i'或'u',则表示对应参数是整数,'d', 'i'代表符号整数,因此需要加上
// 带符号标志。'u'代表无符号整数
            case 'd':
            case 'i':
            flags |= SIGN;
            case 'u':
            break;
            default:
            *str++ = '%';
            if (*fmt)
                *str++ = *fmt;
            else
                --fmt;
            continue;
        }
        /*处理字符的修饰符,同时如果flags有符号位的话,将参数转变成有符号的数*/
        if (qualifier == 'l')
        {
            num = va_arg(args, unsigned long);
            if (flags & SIGN)
                num = (signed long) num;
        }
        else if (qualifier == 'q')
        {
            num = va_arg(args, unsigned long long);
            if (flags & SIGN)
                num = (signed long long) num;
        }
        else if (qualifier == 'Z')
        {
            num = va_arg(args, size_t);
        }
        else if (qualifier == 'h')
        {
            num = (unsigned short) va_arg(args, int);
            if (flags & SIGN)
                num = (signed short) num;
        }
        else
        {
            num = va_arg(args, unsigned int);
            if (flags & SIGN)
                num = (signed int) num;
        }
        str = number(str, num, base, field_width, precision, flags);
    }
    *str = '/0';/*最后在转换好的字符串上加上NULL*/
    return str-buf;/*返回转换好的字符串的长度值*/
}

原文链接:https://blog.csdn.net/hackbuteer1/article/details/7558979#

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值