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.返回结果:返回格式化输出的结果。