C语言--深入printf

我们首先来看一个非常简单的例子

#include <stdio.h>

int main()
{
    printf("Hello World!\n");
    return 0;
}

这是每个初学者都会学习的一个输出语句,但我们这一次想要深挖一下它的内部。在这个说明一下,本人在这里使用的环境是msys2配置的mingw64,使用VScode查看代码,不同的环境结果可能不同。

首先我们进入stdio.h文件,并搜索printf
在这里插入图片描述
找到printf的实现
在这里插入图片描述

__mingw_ovr
__attribute__((__format__ (gnu_printf, 1, 2))) __MINGW_ATTRIB_NONNULL(1)
int printf (const char *__format, ...)
{
  int __retval;
  __builtin_va_list __local_argv; __builtin_va_start( __local_argv, __format );
  __retval = __mingw_vfprintf( stdout, __format, __local_argv );
  __builtin_va_end( __local_argv );
  return __retval;
}

一.__mingw_ovr

这是一个 MinGW 特定的宏,用于覆盖系统默认的 printf 实现。按住ctrl键点击可以跳转到宏的声明
在这里插入图片描述

#  define __mingw_ovr static \
      __attribute__ ((__unused__)) __inline__ __cdecl

也就是说,这个宏是一个用于在 MinGW 环境中对函数进行特定的修饰。
static:将函数或变量的作用域限制在当前文件内(即文件内部静态)。这意味着该函数只能在定义它的源文件中使用,不会被其他文件链接,不会向其他编译单元暴露接口。
attribute ((unused)):告诉编译器即使该函数或变量没有被使用,也不要发出警告。例如:

#include <stdio.h>
static void func(){}
int main()
{
    int a=0;
    return 0;
}

编译后会出现警告
在这里插入图片描述
如果加上这个

#include <stdio.h>
static __attribute__ ((__unused__)) void func(){}
int main()
{
    int a=0;
    return 0;
}

再编译一遍,这个没有用到的静态函数就没有警告了
在这里插入图片描述
__inline__:建议编译器将该函数内联展开,而不是通过常规的函数调用机制调用。内联函数可以减少函数调用的开销,提高性能。
例如:我们编写一个使用__inline__的版本

// tes_inline.c
#include <stdio.h>
static __inline__ void func()
{
    printf("hello world\n");
}
int main()
{
    int a=0;
    func();
    return 0;
}

使用gcc编译为汇编代码

gcc -S tes_inline.c -o tes_inline.s  

再写一个对照方案

#include <stdio.h>
static void func()
{
    printf("hello world\n");
}
int main()
{
    int a=0;
    func();
    return 0;
}
gcc -S tes_noline.c -o tes_noline.s

查看两个.s文件,两者是一样的,原因是__inline__知识对编译器的一个建议,编译器根据不同的优化方案可以不遵守
在这里插入图片描述
我们做一个调整,在有__inline__的文件里加入__attribute__((always_inline))表示更加强烈的意愿

#include <stdio.h>
static __inline__  __attribute__((always_inline)) void func()
{
    printf("hello world\n");
}
int main()
{
    int a=0;
    func();
    return 0;
}

再编译一遍查看结果

我们会发现内联版本里面没有func函数的部分
__cdecl:指定函数使用 C 调用约定(C calling convention)。这是最常见的调用约定,参数从右到左压栈,由调用者清理栈。
总之,__mingw_ovr 宏的定义综合了多个属性,目的是为了:
将函数限制在文件内部使用(static)。
避免未使用函数的编译警告(__attribute__ ((__unused__)))。
提高性能,建议编译器内联该函数(__inline__)。
确保函数使用标准的 C 调用约定(__cdecl)。

二.__attribute__((__format__ (gnu_printf, 1, 2)))

这个属性告诉编译器,printf 函数的第一个参数(索引为1)是一个格式字符串,并且后续的变参(从索引2开始)是根据该格式字符串解析的参数。这有助于编译器进行格式检查,避免格式化字符串错误。

三.__MINGW_ATTRIB_NONNULL(1)

ctrl点击一下,找到声明它的地方
在这里插入图片描述
也就是说__MINGW_ATTRIB_NONNULL(1)实际上就是__attribute__ ((__nonnull__ (1)))
确保第一个参数(即格式字符串)不能为空,否则会触发编译警告或错误。

四.__builtin_va_list

使用__builtin_va_list定义一个变长参数列表的局部变量__local_arg。这个__builtin_va_list的具体实现应该是内嵌在gcc中的,它的具体实现没有找到,不过有一种实现可能是这样的

typedef struct {
    unsigned int gp_offset;           // 通用寄存器偏移量
    unsigned int fp_offset;           // 浮点寄存器偏移量
    void *overflow_arg_area;          // 溢出参数区指针
    void *reg_save_area;              // 寄存器保存区指针
} __va_list_tag;

typedef __va_list_tag __builtin_va_list[1];

五.__builtin_va_start

__builtin_va_start( __local_argv, __format );传递一个变长参数列表,还有一个是最后一个固定变量,即变长参数之前的一个参数,也就是int printf (const char *__format, …)中省略号之前的那个__format。这个函数的目的是初始化变长参数列表,从 __format 参数之后开始。

六.__retval = __mingw_vfprintf( stdout,__format,__local_argv );

我们可以发现其声明在这里
在这里插入图片描述
其中FILE结构体的实现在这里
在这里插入图片描述

#ifndef _FILE_DEFINED
  struct _iobuf {
#ifdef _UCRT
    void *_Placeholder;
#else
    char *_ptr;       // 当前缓冲区指针
    int _cnt;         // 缓冲区中剩余的字符数
    char *_base;      // 缓冲区基地址
    int _flag;        // 文件状态标志
    int _file;        // 文件描述符
    int _charbuf;     // 用于未缓冲字符的存储
    int _bufsiz;      // 缓冲区大小
    char *_tmpfname;  // 临时文件名(如果文件是临时文件)
#endif
  };
  typedef struct _iobuf FILE;
#define _FILE_DEFINED
#endif

FILE 类型的主要作用是提供对文件流的抽象,使得文件操作更加方便和高效。

__retval = __mingw_vfprintf( stdout,__format,__local_argv );

查找stdout,发现
在这里插入图片描述
发现stdout实际上调用的__acrt_iob_func函数,该函数的声明在这里
在这里插入图片描述
我们可以通过从Sourceforge上下载mingw64的源码来看实现
在这里插入图片描述
这个__acrt_iob_func函数实际上是再次调用的__iob_func_函数,而这个函数经过查找发现出现在好多动态链接库里面没有公开
在这里插入图片描述
但总体而言,__acrt_iob_func这个函数就是用来获取stdin,stdout和stderr这三个流的。

__retval = __mingw_vfprintf( stdout,__format,__local_argv );

之后便是格式字符串和需要的参数。我们可以进入mingw64源码查看__mingw_vfprintf的实现
在这里插入图片描述
首先发现__mingw_vfprintf实际上是__vfprintf,我们再找__vfprintf
在这里插入图片描述
过程为
1.锁定文件流,防止并发问题。
2.调用__pformat进行实际的格式化输出操作。
3.解锁文件流。
4.返回格式化操作的结果。
至于__pformat的实现比较复杂,如果感兴趣可以自行探索。

总而言之,__retval = __mingw_vfprintf( stdout,__format,__local_argv );这个语句返回了成功输入到stdout的字符数量。

七.__builtin_va_end( __local_argv );

使用__builtin_va_end清理变长参数列表。这个的实现可能在gcc里面,mingw64的源码中没有,感兴趣的可以自行探索。
在这里插入图片描述

八.总结

总而言之,printf函数的源码流程为
1.初始化:定义返回值变量__retval和变长参数列表__local_argv。
2.处理变长参数:使用__builtin_va_start初始化变长参数列表。
3.调用底层函数:调用__mingw_vfprintf进行实际的格式化输出,并将结果存储在__retval中。
4.清理:使用__builtin_va_end清理变长参数列表。
5.返回结果:返回格式化输出的结果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值