87.你知道printf函数的实现原理是什么吗?

87.你知道printf函数的实现原理是什么吗?

printf是格式化输出可以自己定义输出的格式;printf(“%d\n”,a),其中" "之间的是格式说明串。% 后的一个或两个字符是格式说明符,用它来控制输出变量值的形式,
printf可以输入以上两种格式:

  • 字符说明符%c 同于putchar;
  • 字符串说明符%s 同于puts;

首先,如果在程序中要使用 printf() 那么就必须要包含头文件 stdio.h。

1.实现

各平台实现都不一样

  • glibc-2.21
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);
}
  • windows下可以这样
extern "C" int __cdecl printf(const char * format, ...)
{
    char szBuff[1024];

    int retValue;

    DWORD cbWritten;
    va_list argptr;

    va_start( argptr, format );
    retValue = wvsprintf( szBuff, format, argptr );
    va_end( argptr );
    
    WriteFile(  GetStdHandle(STD_OUTPUT_HANDLE), szBuff, retValue, &cbWritten, 0 );

    return retValue;
}

1.1分析定义

int _cdecl  printf ( const char * format, ... );

_cdecl是C和C++程序的缺省调用方式

_CDEDL调用约定:

  • 参数从右到左依次入栈
    • 在C/C++中,对函数参数的扫描是从后向前的。C/C++的函数参数值通过压入堆栈的方式来给函数传参数的。
    • 最先压⼊的参数最后出来,在计算机的内存中,数据有 2 块,⼀块是堆,⼀块是栈(函数参数及局部变量在这⾥),⽽栈是从内存的⾼地址向低地址⽣⻓的,控制⽣⻓的就是堆栈指针了,最先压⼊的参数是在最上⾯,就是说在所有参数的最后⾯,最后压⼊的参数在最下⾯,结构上看起来是第⼀个,所以最后压⼊的参数总是能够被函数找到。
    • 因为它就在堆栈指针的上方。printf的第一个被找到的参数就是那个字符指针,就是被双引号括起来的那一部分,函数通过判断字符串里控制参数的个数来判断参数个数以及数据类型,通过这些可以算出数据需要的堆栈指针的偏移变量了。
    • 为什么要约定从右到左依次入栈? 对x86,栈的生长方向向下(高地址向低地址),_cdecl调用约定函数参数从右向左入栈,因此从第一个固定参数(format)的堆栈地址向前(向上,向高地址)移动就可得到其他变参的地址。
  • 调用者负责清理堆栈
  • 参数的数量类型不会导致编译阶段的错误

固定参数 format(格式控制字符串),包含了两种类型的对象:普通字符和转换说明

  • 在输出时,普通字符将原样不动地复制到标准输出
  • 转换说明并不直接输出而是用于控制 printf 中参数的转换和打印
    • 每个转换说明都由一个百分号字符(%)开始,以转换说明结束,从而说明输出数据的类型、宽度、精度等 。
    • printf 的格式控制字符串 format 中的转换说明组成如下,其中 [] 中的部分是可选的:
    • %[flags][width][.precision][length]specifier,即:%[标志][最小宽度][.精度][类型长度]说明符 。(其中,末尾的说明符字符是最重要的组成部分,因为它定义了类型及其相应实参的解释)

2.其他

从上面不同平台的实现中.,我们都看到va_list、va_start()等相同的部分。va_list、va-start()、va_arg()、va_end()是一个宏定义三个宏函数,这四个宏的存在,才使得可变参数列表能够实现。

除了这四个宏以外,vfprintf()函数和_output()函数则分别实现了glibc-2.21和VC6.0两种版本里对于可变参数列表的处理(函数内部是一个大的while()循环)。vfprintf()和_output()是printf()函数对于参数细节处理实现的核心,包含了printf函数族所有函数实现的代码。

参考资料来源:
(5条消息) C/C++面试:printf实现原理_c++ printf_OceanStar的学习笔记的博客-CSDN博客
版权声明:本文为CSDN博主「Apollon_krj」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Apollon_krj/article/details/79373966

posted @ 2023-07-11 15:25  CodeMagicianT  阅读(313)  评论(0编辑  收藏  举报