C/C++面试:printf实现原理

1059 篇文章 275 订阅

在C语言中,有三个函数可以用来在显示器上输出数据,它们分别是:

  • puts():只能输出字符串,并且输出结束后会自动换行。
  • putchar():只能输出单个字符。(char 类型)
  • printf():可以输出各种类型的数据。
    其中 printf() 是最灵活,最复杂,最常用的输出函数,其余两个函数都可以被 printf() 所替代!

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

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

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

实现

各平台实现都不一样

  • 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;

}

分析定义

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,即:%[标志][最小宽度][.精度][类型长度]说明符 。(其中,末尾的说明符字符是最重要的组成部分,因为它定义了类型及其相应实参的解释)

可变参数(用”…”表示)

  • 根据不同的 format 字符串,函数可能需要一系列的附加参数,每个参数包含了一个要被插入的值,替换了 format 参数中指定的每个 % 标签。参数的个数应与 % 标签的个数相同。
  • 返回值:如果函数执行成功,则返回所打印的字符总数(计数针对所有的打印字符,包括空格和不可见的换行字符),如果函数执行失败,则返回一个负数。

其他

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

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

  • https://blog.csdn.net/AngelDg/article/details/100003818
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值