printf打印函数的原理浅析
printf的底层原理浅析
⽬录
前⾔
最近在学习linux内核的时候⽤到了⾃定义实现的printf,学习了⼀下,在此记录,希望有助于⼤家。
在C语⾔中,我们⽤到的最多的输出功能函数⼤概就是printf了。但以前只是调⽤C语⾔的库函数,具体printf是如何实现的呢?
说到底两件事:
(1)、函数变参
(2)、格式解析
下⾯将简要介绍以上两点内容,最后附⼀个简单的printf例⼦。
函数变参
何为函数变参?简要来说就是参数个数不固定的函数。⼤部分时间,我们⽤的、写的都是参数固定的函数。但是为了应对想printf这种参数不固定的函数(),C语⾔提供了⼀种变参函数的机制。printf怎么输出字符
int printf (const char*__format,...);
如上所⽰为pritnf的声明。其中format就是我们以前写的格式化字符串,其中包括我们想输出的内容和参数占位符(⽤%+格式化字符来占位)。后⾯的 ‘…’,就是代表不固定的参数。
我们调⽤的使⽤像下⾯这样进⾏调⽤:
printf(“This is a test:%d,%c,%s”,a,b,c);
这⾥的…,就代表了a,b,c三个参数。
说完了变参函数的概念,下⾯说说变参函数的实现原理。
我们知道,在调⽤函数的时候,函数的参数是在栈中分配的。 ⽐如说调⽤下⾯这个普通函数。
//函数声明
int Add(int a,int b);
//函数调⽤
Add(3,5);
其栈⼤概是向下⾯这样分配的。
即,⼀般来说,栈空间是从搞地址向低地址分配,函数参数从右依次向左分配。分配完成之后,在函数内部的操作就是对这栈空间的变量进⾏的操作,这也是为什么我们在函数内部改变传⼊参数的值,却不能够传到函数外部的原因(如果不使⽤指针或者地址的话)。
⽽对于变参函数来说,其基本的传参原则是和上⾯说的⼀致。但是 由于其函数声明参数并不固定,所以有些栈中的变量是没有名字的,我们如果想使⽤这段空间,必须由我们⾃⼰通过指针来实现。
是不是有点蒙圈,有点绕?
⼩⼆,来点栗⼦。
如果我这样调⽤printf函数
printf(“This is a test:%d,%c,%s”,10,'A',"helloworld");
看下⾯栈的内存分配图。
如上图所⽰,在栈中只有format变量(字符指针类型,在printf的声明函数中定义了此参数),是有名字的,其他的三个内存空间⾥⾯只有值,但是没有名字来指明它们。所以,我们只能通过地址变量来到(访问)它们。
这⾥要稍微补充⼀点,⼀般来说,对于变参函数来说,虽然其参数的个数是不固定的,但是其最少要有⼀个参数,就像printf函数中⾄少要有⼀个format参数⼀样(好像在宏定义变参函数中,可以由0个参数,这⾥不讨论)。
答:这个最少要有的⼀个参数⼀般就是⽤来定位栈顶空间的。就像我们在上⾯描述的那样,栈内存的分配是从右向左的,最左边的参数就是栈顶元素。相当于,我们知道了栈顶的地址,只要再根据变参中每个参数的类型(int、char型等),相应的进⾏地址偏移,就可以访问变参的内容。
哎呀,⼜有⼀个问题了,在被调⽤的函数内部我怎么知道变参的类型是什么呢?
嘿嘿,还真是,⼀般你还真是不知道。这种情况下就需要调⽤者和被调⽤者商量好了。对于printf函数来说,调⽤者通过%+格式字符的⽅式通知了被调⽤者(printf的实现者)。
怎么通知的呢?
答:就是通过第⼀个format参数了。因为%+格式字符都是在format参数⾥的啊。
格式解析
弄懂了上⾯所说的,剩下的就没什么好说的了。 简单提⼀下。
简单来说就是扫描format参数⾥的字符,如果是普通字符就打印输出,如果是%,就说明后⾯有可能是格式字符,需要进⾏检测,然后从栈顶(其实是第⼀个参数的位置)弹出指定类型的数据,按照指定格式(⼗进制、⼗六进制、指定宽度、指定精度等等)进⾏输出。
基本上是⼀个字符串解析的过程。后⾯代码有解析,在此就不详述了。
⼀个简单的printf⽰例
//从传递的栈中获取参数的⼀些设置
typedef char* va_list;
#define _INTSIZEOF(n) ((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) )
#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 )
#define BUFFER_SIZE 4096
static char print_buf[BUFFER_SIZE];
static char num_to_char[]="0123456789ABCDEF";
//将⼗进制数据转化为字符型数据
int fillD(char print_buf[],int k,int num,int base)
{
int i;
int tmp;
char tmp_str[BUFFER_SIZE]={0};
int tmp_index =0;
if(num ==0)
tmp_str[tmp_index++]='0';
else if(num <0)//如果是负数的话,记录下符号后转为相反的数
{
print_buf[k++]='-';
num =-num;
}
//将num转化为base进制的数据
while(num >0)
{
tmp = num % base;//取最低位元素
tmp_str[tmp_index++]= num_to_char[tmp];//⼊栈,填⼊字符型数字
num = num/ base;
}
//将字符型数字出栈倒⼊buf中
for(i = tmp_index-1;i>=0;--i)
{
print_buf[k++]= tmp_str[i];
}
}
return k;
}
//填充字符串
int fillStr(char print_buf[],int k,char* src)
{
int i =0;
for(;src[i]!='\0';++i)
{
print_buf[k++]= src[i];
}
return k;
}
//处理具体的解析,并输出到printf_buf中 //I , %c take %d years to %x fin %s ished it\n ; int my_vsnprintf(char print_buf[],int size,const char*fmt,va_list arg_list)
{
int i =0,k =0;
char tmp_c =0;
int tmp_int =0;
char*tmp_cp =NULL;
for(i =0;i<size && fmt[i]!='\0';++i)
{
if('%'!= fmt[i])//直接输出的普通格式字符
{
print_buf[k++]= fmt[i];
}
else//需要特殊处理的字符
{
if(i+1< size)
{
switch(fmt[i+1])
{
case'c'://处理字符型数据
tmp_c =va_arg(arg_list,char);//获得字符型参数
print_buf[k++]= tmp_c;
break;
case'd'://处理⼗进制数据
tmp_int =va_arg(arg_list,int);
k =fillD(print_buf,k,tmp_int,10);//填充⼗进制数据
break;
case'x'://处理⼗六进制数据
//填充16进制标志符号
print_buf[k++]='0';
print_buf[k++]='x';
tmp_int=va_arg(arg_list,int);//获取int型数据
k =fillD(print_buf,k,tmp_int,16);//填充⼗六进制数据
break;
case's'://处理字符串
tmp_cp =va_arg(arg_list,char*);//获得字符串型数据
k =fillStr(print_buf,k,tmp_cp);//填充字符串
break;
}
}
else
print_buf[k++]= fmt[i];//最后⼀个字符是%,直接读取即可
}
}
return k;//返回当前位置
}
//输出缓冲区⾥的字符
void__put_str(char print_buf[],int len)
{
int i =0;
for(;i<len;++i)
putchar(print_buf[i]);
}
void printk(char const*fmt,...)
{
int len =0;
va_list arg_list;
va_start(arg_list,fmt);//arg_list指向第⼀个参数的位置(不是fmt)
len =my_vsnprintf(print_buf,sizeof(print_buf),fmt,arg_list);//解析参数,并打印到输出中
va_end(arg_list);//变参结束
__put_str(print_buf,len);//转换成字符输出
}
结语
是不是懂了?做个作业呗。
1、以下代码段是C语⾔提供的变参函数的主要代码部分,你看看,能看的懂吗?
typedef char* va_list;
#define _INTSIZEOF(n) ((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) )
#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 )
2、⼀般情况下,在传递字符串参数的时候,为何在栈中保存的都是字符串的⾸地址,⽽不是整个字符串内容呢?
这么简单的问题,⼀定难不倒你了。
补充
按理说,函数形参的传递在栈内存中的分布应该是和我上⾯说的差不多,但是最近在做实验的时候,发现了不⼀样的结果。形式上是函数调⽤栈空间是从下往上的。具体原因,也不是很了解,如果有懂的⼤佬,希望帮我指点下迷津。
以下是我的测试代码:
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论