printf打印函数的原理浅析

printf打印函数的原理浅析

printf的底层原理浅析

目录

printf的底层原理浅析前言函数变参格式解析一个简单的printf示例结语

补充

前言

最近在学习linux内核的时候用到了自定义实现的printf,学习了一下,在此记录,希望有助于大家。

在C语言中,我们用到的最多的输出功能函数大概就是printf了。但以前只是调用C语言的库函数,具体printf是如何实现的呢?

说到底两件事: (1)、函数变参 (2)、格式解析

下面将简要介绍以上两点内容,最后附一个简单的printf例子。

函数变参

何为函数变参?简要来说就是参数个数不固定的函数。大部分时间,我们用的、写的都是参数固定的函数。但是为了应对想printf这种参数不固定的函数(),C语言提供了一种变参函数的机制。

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

{

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

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、一般情况下,在传递字符串参数的时候,为何在栈中保存的都是字符串的首地址,而不是整个字符串内容呢?

这么简单的问题,一定难不倒你了。

补充

按理说,函数形参的传递在栈内存中的分布应该是和我上面说的差不多,但是最近在做实验的时候,发现了不一样的结果。形式上是函数调用栈空间是从下往上的。具体原因,也不是很了解,如果有懂的大佬,希望帮我指点下迷津。

以下是我的测试代码:

#include

void func(char a, short int b,int c,int d)

{

printf("a = %c,&a = %0x\n",a,&a);

printf("b = %d,&b = %0x\n",b,&b);

printf("c = %d,&c = %0x\n",c,&c);

printf("d = %d,&d = %0x\n",d,&d);

}

int main()

{

func('a',10,20,30);

return 0;

}

在window上结果是 这个结果是符合预期的。 但是,在linux上测试出现了下面的现象, 完全和上面的地址顺序相反。

了解吗?给我解释一下呗。

📚 相关推荐

借贷平台测评:蚂蚁借呗平台怎么样? ①公司背景:借呗是由蚂蚁金服旗下重庆市蚂蚁商诚小额贷款有限公司推出的一款用于个人消费的借款服务,背后股东是 蚂蚁集团 ,...
轻松掌握Git:如何巧妙设置和修改库名称技巧揭秘
邵庭遊日出事…嗅覺全消失!慌聞1物嚇壞全場
英雄联盟怎么降低ms LOL降低延迟方法介绍
生态环境部建设项目环境影响报告书(表)审批程序规定
《鲨鱼记账》修改备注方法