C语言Printf函数深入解析

C语言Printf函数深入解析

Printf这个函数让大家又爱又恨,第一次接触c语言编程,基本都是调用printf打印“Hellow World!”,但当真正深入使用编程后,才发现printf并不是一个简单的函数。尤其是从事嵌入式软件工作的开发人员,会经常接触printf挂接驱动、printf重入的问题。

本文详细解释printf函数的工作原理,希望对大家有所帮助。

一、函数栈

分析printf之前首先了解函数的工作机制,程序运行前需要分配好内存空间,如图1所示(本文给出一个简图,实际编译器分配的会更加细致):

图1

代码、全局变量、常量内存位置固定,堆可以用于分配动态内存,而栈区则用于程序的运行。函数调用时将形参从右向左压入栈,等函数运行完成,通过出栈,将形参的存储空间释放。不同的编译器对函数入栈、出栈的内容会有所区别,但是对于c语言,形参的格式遵循_cdedl调用规则,有以下特点:

函数形参入栈顺序是从右向左

函数形参存储空间为连续存储,且参数按照固定字节对齐;编译器根据程序运行平台的字长进行对齐,32位字长平台按照4字节对齐,64位的会按照8字节对齐。

二、printf函数栈

printf 函数原型为int printf(const char *fmt, ...),使用了可变参数的模式,我们通过图2例子来分析函数栈。

图2

fmt:“%d,%c,%c,%f\n”为常量字符串,存储在内存的常量字段,fmt为该字符串首地址;

可变形参1:与变量a类型和数值一致,为int类型;

可变形参2:与变量b类型和数值一致, 为char类型;

可变形参3:与变量c类型和数值一致,为char类型;

可变形参4:与变量d类型和数值一致,float的可变形参会被编译器强制转换为double类型;

假设该代码运行在32位字长的平台,且栈底->栈顶为“高地址->低地址”,函数栈中所有参数的存储地址按照4字节对齐存储,设fmt存储地址为0x30000000;则其函数栈如下图:

图3

三、printf代码解析

printf代码框架

printf代码及注释如下所示:

注:本例为32位平台,所以参数出入栈地址均为4字节对齐。

#ifndef _VALIST

#define _VALIST

typedef char *va_list;

#endif /* _VALIST */

typedef int acpi_native_int;

#define _AUPBND (sizeof (acpi_native_int) - 1) // 入栈4字节对齐

#define _ADNBND (sizeof (acpi_native_int) - 1) // 出栈4字节对齐

#define _bnd(X, bnd) (((sizeof (X)) + (bnd)) & (~(bnd))) // 4字节对齐

#define va_arg(ap, T) (*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND)))) // 按照4字节对齐取下一个可变参数,并且更新参数指针

#define va_end(ap) (void) 0 // 与va_start成对,避免有些编译器告警

#define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND)))) // 第一个可变形参指针

#endif /* va_arg */

static char sprint_buf[2408];

int printf(const char *fmt, ...)

{

va_list args;

int n;

// 第一个可变形参指针

va_start(args, fmt);

// 根据字符串fmt,将对应形参转换为字符串,并组合成新的字符串存储在sprint_buf[]缓存中,返回字符个数。

n = vsprintf(sprint_buf, fmt, args);

//c标准要求在同一个函数中va_start 和va_end 要配对的出现。

va_end(args);

// 调用相关驱动接口,将将sprintf_buf中的内容输出n个字节到设备,

// 此处可以是串口、控制台、Telnet等,在嵌入式开发中可以灵活挂接

if (console_ops.write)

console_ops.write(sprint_buf, n);

return n;

}

vsprintf解析模式详解

vsprintf采用%[flags][width][.prec][length][type]模式对各个参数进行解析各标志解析如下表:

标志(flags)

标志(flags)用于规定输出样式,含义如下:

flags(标志)

字符名称

描述

-

减号

在给定的字段宽度内左对齐,右边填充空格(默认右对齐)

+

加号

强制在结果之前显示加号或减号(+ 或 -),即正数前面会显示 + 号;默认情况下,只有负数前面会显示一个 - 号

(空格)

空格

输出值为正时加上空格,为负时加上负号

#

井号

specifier 是 o、x、X 时,增加前缀 0、0x、0X;

specifier 是 e、E、f、g、G 时,一定使用小数点;

specifier 是 g、G 时,尾部的 0 保留

0

数字零

对于所有的数字格式,使用前导零填充字段宽度(如果出现了减号标志或者指定了精度,则忽略该标志)

最小宽度(width)

最小宽度(width)

相关推荐

RackNerd洛杉矶DC02机房测评:速度、延迟、丢包、路由测试
印尼华人的历史渊源及现状详析
bet体育365怎么样

印尼华人的历史渊源及现状详析

📅 08-20 👁️ 1094
楚乔传分集剧情介绍(1-67集)大结局
365平台客服电话

楚乔传分集剧情介绍(1-67集)大结局

📅 07-26 👁️ 8964