printf打印函数的原理浅析

2023-05-16

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

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

补充

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

以下是我的测试代码:

#include<stdio.h>

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上测试出现了下面的现象,
在这里插入图片描述
完全和上面的地址顺序相反。

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

printf打印函数的原理浅析 的相关文章

  • 使用“printf”返回字符串,而不是打印它

    这可能听起来很奇怪 但事实就是如此 我喜欢使用这种在 php 中构建字符串的技术 printf This is 1 s this is 2 s myFunction1 myFunction2 显然 每当调用函数时 这都会直接打印结果 但我想
  • 格式“%s”需要“char *”类型的参数

    为了锻炼我的 C 编程技能 我尝试自己编写 strncpy 函数 在这样做的过程中 我不断地遇到错误 最终解决了其中的大部分错误 但我却没有进一步的灵感继续下去 我收到的错误是 ex2 1 c 29 3 warning format s e
  • 在 Golang 中构建动态(条件)WHERE SQL 查询

    我正在使用 golang go reform PostgreSQL 我想做的是一个 REST 搜索实用程序 一切都很顺利 直到我遇到条件搜索查询 这里的 条件 意味着我在表中有 10 列要搜索 并且可能有大量的组合 所以我无法单独处理它们
  • printf 忽略单个反斜杠 '\'

    我有这个代码 int main int argc char argv int i printf d s argc argv 1 return 0 如果我运行这段代码a out a b 我在用C shell 其输出为 a b 有什么方法可以将
  • 对 getchar 和 scanf 感到困惑

    我真的很困惑的用法getchar and scanf 这两者有什么区别 我知道scanf 和家人 从用户 或文件 处获取一个字符一个字符并将其保存到一个变量中 但它是立即执行还是在按下某些内容后执行此操作 Enter 我不太理解这段代码 我
  • 为什么在打印语句之前调用函数?

    include
  • 如何使用“%f”将双精度值填充到具有正确精度的字符串中

    我正在尝试使用 a 来填充带有双精度值的字符串sprintf像这样 sprintf S f val 但精度被截断至小数点后六位 我需要大约 10 位小数来保证精度 如何才能做到这一点 宽度 精度 宽度应包括小数点 8 2表示8个字符宽 点前
  • C 中的类型转换会变得香蕉吗? [关闭]

    Closed 这个问题需要调试细节 help minimal reproducible example 目前不接受答案 看来 C 和我对这里的预期输出存在分歧 I have struct r struct int r i float r f
  • scanf("%d", &value) 中的字符输入[重复]

    这个问题在这里已经有答案了 简而言之 我的代码是 include
  • bash + for循环+输出索引号和元素

    这是我的数组 ARRAY one two three 如何打印数组以便得到如下输出 index i element i 使用printf or for我在下面使用的循环 1 one 2 two 3 three 一些笔记供我参考 打印数组的1
  • 如何使用 sprintf 附加字符串?

    我面临着一个严重的问题sprintf 假设我的代码片段是 sprintf Buffer Hello World sprintf Buffer Good Morning sprintf Buffer Good Afternoon 几百次冲刺
  • 在没有正确原型的情况下调用 printf 是否会引发未定义的行为?

    这个看起来无辜的程序是否会调用未定义的行为 int main void printf d n 1 return 0 是的 调用printf 没有适当的原型 来自标准头
  • 使用 print_r 或 php 中的任何其他函数打印多个数组

    我需要在代码中打印多个数组的内容 例如 function performOp n inputArr workArr printf Entered function value of n is d n print r inputArr pri
  • sprintf 风格字符串格式化的起源

    字符串格式化概念见sprintf如今几乎可以在任何语言中找到 你知道 用 s d f 等掩盖字符串 并提供变量列表来填充它们的位置 哪种语言最初具有提供此功能的库函数或语言结构 请指定某种来源参考以确认您的主张 以便我们避免纯粹的猜测或猜测
  • 使用 OpenMP 时无用的 printf 没有加速

    我刚刚编写了第一个 OpenMP 程序 它并行化了一个简单的 for 循环 我在双核机器上运行代码 发现从 1 个线程变为 2 个线程时速度有所提高 然而 我在学校 Linux 服务器上运行相同的代码并没有看到加速 在尝试了不同的事情之后
  • 从 CUDA 设备写入输出文件

    我是 CUDA 编程的新手 正在将 C 代码重写为并行 CUDA 新代码 有没有一种方法可以直接从设备写入输出数据文件 而无需将数组从设备复制到主机 我假设如果cuPrintf存在 一定有地方可以写一个cuFprintf 抱歉 如果答案已经
  • C++ 的 String.Format

    正在寻找 NET 的 String Format 等函数的 C 实现 显然有 printf 及其变体 但我正在寻找具有位置的东西 如下所示 String Format 您好 0 您是 1 岁 感觉如何 1 姓名 年龄 这是必要的 因为我们将
  • Java:将二维字符串数组打印为右对齐表格

    是什么best打印a的单元格的方法String 数组作为右对齐表 例如 输入 x xxx yyy y zz zz 应该产生输出 x xxx yyy y zz zz 这似乎是一个should能够完成使用java util Formatter
  • 如何使用 sprintf 函数在字符中添加前导“0”而不是空格?

    我正在尝试使用sprintf函数为字符添加前导 0 并使所有字符长度相同 然而我得到的是领先空间 My code a lt c 12 123 1234 sprintf 04s a 1 12 123 1234 我试图得到什么 1 0012 0
  • 需要在python中找到print或printf的源代码[关闭]

    很难说出这里问的是什么 这个问题是含糊的 模糊的 不完整的 过于宽泛的或修辞性的 无法以目前的形式得到合理的回答 如需帮助澄清此问题以便重新打开 访问帮助中心 help reopen questions 我正在做一些我不能完全谈论的事情 我

随机推荐

  • Jmeter之JAVA Request的应用

    当我们使用Jmeter进行接口测试的时候 xff0c 我们一定会遇到一个问题 xff0c 那就是如果这些接口不是http协议的 xff0c 是经过封装以后的接口 xff0c 用Jmeter该怎么解决呢 xff1f 当你想到这个问题 xff0
  • FreeRTOS例程3-串口中断接收不定长的数据与二值信号量的使用

    FreeRTOS例程3 串口中断接收不定长的数据与二值信号量的使用 知乎 zhihu com
  • 使用airtest实现UI自动化之环境搭建

    1 xff0c 安装python python版本为3 7 1 2 xff0c 安装airtest xff0c pocoui模块 在安装时碰到的问题 xff1a 1 xff09 使用pip命令报错 xff0c 报SSL证书无法识别错误 解决
  • HTTP Digest Authentication在实际应用中的问题

    作者 xff1a 老王 Basic认证实际上是明文传递密码 xff0c 所以 RFC2617里定义了Digest认证以取代它 xff0c 其计算方法如下 xff1a 其中HA1计算方法为 xff1a 如果qop选项的值为auth xff0c
  • 1天精通Apipost--全网最全gRPC调试和智能Mock讲解!

    gRPC 接口调试 grpc 作为一个老程序员 xff0c 最近公司技术架构用到了gPRC xff0c 但国内很少有支持这个的工具 xff0c 大部分都只是支持http 由于我同时也是Apipost骨灰级用户 xff0c 于是就在他们官网的
  • CAN总线波特率的设定——以STM32为例

    波特率的设定 首先是几个名词的含义 xff0c CAN里面1个位的构成如下 注意采样点的位置在PBS1和PBS2的中间 根据这个位时序就可以计算波特率了 最小时间单位 xff08 Tq xff0c Time Quantum xff09 同步
  • 2021电赛备赛心路历程(含代码例程)

    作为一个电子学院学生 xff0c 大二暑假才开始自学单片机知识 xff08 还是因为报名了电赛而不得不去学 xff09 xff0c 深感愧疚 从今年7月至8 4的将近四周时间内哩哩啦啦学了一些基础模块 xff08 其中光是练习点灯和其他基础
  • 20201114-三轴云台storm32 BGC HAKRC调试+

    storm32 BGC HAKRC 2轴云台支持俯仰 xff08 抬头低头 xff09 以及横滚 xff1b 三轴多了一个航向 支持锁头模式 xff0c 拍摄更方便 可以控制俯仰通过接收机或者其他单独PWM通道 可以设置跟随模式或者锁定模式
  • KEIL5中头文件路劲包含问题

    方式1 xff1a 1 Keil中添加头文件相对路劲的方法 在c c 43 43 配置中添加路劲 xff0c 最终是将添加的绝对路径转化为相对路径 xff1b 注意 xff1a 相对路径的当前位置指 uvproj文件所在位置 在C C 43
  • php curl函数应用方法之模拟浏览器

    curl 是使用URL语法的传送文件工具 xff0c 支持FTP FTPS HTTP HTPPS SCP SFTP TFTP TELNET DICT FILE和LDAP curl 支持SSL证书 HTTP POST HTTP PUT FTP
  • WireShark基本抓包数据分析

    WireShark抓包数据分析 xff1a 1 TCP报文格式 源端口 目的端口 xff1a 16位长 标识出远端和本地的端口号 顺序号 xff1a 32位长 表明了发送的数据报的顺序 确认号 xff1a 32位长 希望收到的下一个数据报的
  • VScode下运行调试C++文件

    1 下载vscode 这个可以直接去官网下载 2 下载mingw64 下载mingw64就是下载C 43 43 的编译器g 43 43 和调试器gdb 这个也可以去官网下载 xff0c 下载安装完成后去配置环境变量 我将mingw64安装在
  • c++模板的优点和缺点

    作为C 43 43 语言的新组成部分 xff0c 模板引入了基于通用编程的概念 通用编程是一种无须考虑特定对象的描述和发展算法的方法 xff0c 因此它与具体数据结构无关 但在决定使用C 43 43 模板之前 xff0c 让我们分析一下使用
  • 基于导航网格的A星寻路(Navigation mesh)

    最近花了几个月的时间实现了导航网格寻路和导航网格自动生成 导航网格数据结构定义 由于数据之间有着层级关系 xff0c 所以采用XML进行定义 navmesh基本元素 xff1a 顶点 Verts 43 可走边 Edges 43 凸多边形 P
  • cmake 引入 动态库、静态库

    动态库 xff1a link directories PROJECT SOURCE DIR lib 添加动态连接库的路径 target link libraries project name lMNN 添加libMNN so 静态库 xff
  • 利用JSP内置对象Request和Application实现用户名密码登录注册进入首页显示

    学习目标 xff1a 实验名称 xff1a JSP内置对象目的 xff1a 掌握JSP页面的全部语法 能够编写基本的JSP网页 学习内容 xff1a 1 实验 地点 周三2单元 xff0c 10617综合一实验室 自带电脑 目的 掌握各种内
  • 【如何写CMake】一个解决方案,多个工程

    文章目录 代码参考 一个解决方案 xff0c 多个工程 xff0c 即多个exe dll lib等 代码 多加几个ADD EXECUTABLE即可 1 cmake verson xff0c 指定cmake版本 cmake minimum r
  • 指针、寄存器、位操作

    定义寄存器的绝对地址 xff0c 并转换为指针进行位操作 1 位操作示例一 define PERIPH BASE unsigned int 0x40000000 define APB2PERIPH BASE PERIPH BASE 43 0
  • 详解TCP连接的建立

    TCP首部格式 TCP报文段首部的前20个字节是固定的 xff0c 后面有4N字节是根据需要而增加的选项 xff0c 因此TCP报文段的最小长度为20字节 首部固定部分的各字段的意义如下 xff1a 1 源端口和目的端口 xff1a 加上I
  • printf打印函数的原理浅析

    printf的底层原理浅析 目录 printf的底层原理浅析前言函数变参格式解析一个简单的printf示例结语 补充 前言 最近在学习linux内核的时候用到了自定义实现的printf xff0c 学习了一下 xff0c 在此记录 xff0