题外话
这篇博文主要围绕printf函数分析的,主要讲解printf 使用C的可变参数机制, printf是否可重入(是否线程安全),
printf函数的源码实现.
正文
1.C中可变参数机制
我们先举个例子,假如现在有这样一个需求 "需要一个不定参数整型求和函数".
具体实现代码如下
// 需要一个不定参数整型求和函数 int sum_add(int len, ...) { int sum = 0; va_list ap; va_start(ap, len); // 初始化 将ap参数指向 len 下一个参数位置处 while (len > 0) { int tmp = va_arg(ap, int); // 获取当前参数,并且将ap指向 下一个参数位置处 sum += tmp; --len; } va_end(ap); // 清除(销毁)ap变量 return sum; }
详细一点的测试代码如下
#include <stdio.h> #include <stdlib.h> #include <stdarg.h> // 需要一个不定参数整型求和函数,len表示参数个数 int sum_add(int len, ...); int main(int argc, char *argv[]) { int sum; sum = sum_add(1, 2); printf("sum = %d\n",sum); sum = sum_add(4,1,2,3,4); printf("sum = %d\n", sum); sum = sum_add(10, 1, 2, 3, 4,5,6,7,8,9,10); printf("sum = %d\n", sum); system("pause"); return 0; }
这里扯一点,对于system("pause"); 是调用系统shell 的pause命令,就是让当前cmd关闭停留一下,输出一段话等待一下. 效果图如下
这个功能在 Linux 有个 系统函数如下
#include <unistd.h>
// 函数说明:pause()会令目前的进程暂停(进入睡眠状态),直至信号(signal)所中断。
// 返回值:只返回-1 int pause(void);
有的时候 需要在多个平台,下 完成等待函数 ,就需要通过宏来判断,这是很恶心的.也许是个人觉得,可移植程序内部都是恶心丑陋的 腐尸堆积体.
下面介绍一个 自己写的一个通用函数 ,通用控制台学习的等待函数.
#include <stdio.h> //6.0 程序等待函数 extern void sh_pause(void); //6.0 等待的宏 这里 已经处理好了 #ifndef INIT_PAUSE #define _STR_PAUSEMSG "请按任意键继续. . ." #define INIT_PAUSE() \ atexit(sh_pause) #endif/* !INIT_PAUSE */ //系统等待函数 void sh_pause(void) { rewind(stdin); printf(_STR_PAUSEMSG); getchar(); }
思路是先清空输入流stdin ,再用getchar等待函数,等待用户输入回车结束这次控制台学习.
1.1 可变参数机制介绍
首先看摘录的源码,这里先分析Window 上源码,Linux上也一样.其实Linux源码更容易看,因为它简洁高效.都相似,重点看个人抉择.
// stdarg.h ... #define va_start __crt_va_start #define va_arg __crt_va_arg #define va_end __crt_va_end #define va_copy(destination, source) ((destination) = (source)) ... //vadefs.h ... typedef char* va_list; ... #define _ADDRESSOF(v) (&(v)) ... #elif defined _M_IX86 #define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1)) #define __crt_va_start_a(ap, v) ((void)(ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v))) #define __crt_va_arg(ap, t) (*(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t))) #define __crt_va_end(ap) ((void)(ap = (va_list)0)) #elif defined _M_ARM .... #define __crt_va_start(ap, x) __crt_va_start_a(ap, x) ...
在分析之前,摘了一个 表格,看一下也许会容易理解一点.如下
stdarg.h数据类型
类型名称
|
描述
|
相容
|
va_list
|
用来保存宏va_arg与宏va_end所需信息
|
C89
|
巨集名称
|
描述
|
相容
|
va_start
|
使va_list指向起始的参数
|
C89
|
va_arg
|
检索参数
|
C89
|
va_end
|
释放va_list
|
C89
|
va_copy
|
拷贝va_list的内容
|
C99
|
这里再扯一点,目前用的C标准最多是C89,流行编译器例如gcc,VS2015基本上都支持,C89和C99.
其中gcc支持的比VS要好.毕竟VS主打的是CSharp和CPlusPlus.
还有一个编译器Pelles C对C99支持的最好,对C11支持的还可以.有机会大家可以玩玩.做为小白 还希望C11推广开来.
因为C11标准对一些看法常用模块例如多线程,数学复数,新的安全库函数等等,缺点是太丑了.
下面继续回到 可变参数的话题上. 其实理解 上面 代码,主要是理解那几个宏是什么意思.
这里说一下一个隐含条件 是 C编译器对于可变参数函数 必须(默认) 是 __cdecl 修饰的,详细的一点解释如下:
__cdecl 是C Declaration的缩写(declaration,声明),
表示C语言默认的函数调用方法:所有参数从右到左依次入栈,这些参数由调用者清除,称为手动清栈。
被调用函数不会要求调用者传递多少参数,调用者传递过多或者过少的参数,甚至完全不同的参数都不会产生编译阶段的错误。
二次解释
参数从右向左入栈 => 最后一个参数先入栈,最后第一个参数在栈顶
调用者,被调用函数 => b() { a();} , a是被调用函数,b是调用者函数
调用者清除,称为手动清栈 => 在 b 汇编代码中 会插入 清空a函数栈的汇编代码
思考一下,只能这么搞,才能知道函数的入口在哪里,否则都找不见函数参数在那个位置. 这也是为什么可变参数需要第一个参数显示声明的原因.
而那些宏就是为了找到其它参数而设计的.核心是根据变量的内存布局,指针来回指.依次剖析如下:
// 定义 char* 类型,这个类型指针偏移量值为 1, // 例如 // char *a = NULL ; 此时 a地址是 0x0 // ++a; => 此时 a地址为 0x0 + 1*1 = 0x1位置处 typedef char* va_list; // // 定义获取变量地址的宏 // #define _ADDRESSOF(v) (&(v))
再来分析 地址偏移宏
// // 这个宏是为了编译器字节对齐用的,用sizeof(int) 字节数进行对齐 // // 简化一下 sizeof(int) - 1 假定为 3,(当前2015年11月22日就是3) // _INTSIZEOF(n) => ((sizeof(n) + 3 ) & ~3 ) // 举个例子 // _INTSIZEOF(int) => 4 // _INTSIZEOF(char) => 4 // _INTSIZEOF(double) => 8 // _INTSIZEOF(short) => 4 // 因为编译器有内存位置调整,具体参见 struct 内存布局,毕竟都是C基础.编译器这样做之后,访问速度回快一些,根据地址取值的次数会少一些. #define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
下面的宏就简单了
// ap 是va_list 声明变量,第一次时候调用 // v 表示 可变函数中第一个参数 // 执行完毕后 ap指向 v 变量后面的下一个 函数参数 #define __crt_va_start_a(ap, v) ((void)(ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v))) // t 只能是类型,int char double .... // 操作之后 ap又指向下一个函数参数,但是返回当前ap指向的位置处 // 讲完了,关键看自己多写,多读源码.有些大神都是不看注释 直接通过源码就入手框架了 #define __crt_va_arg(ap, t) (*(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t))) // 清空ap变量,等同于简单的清空野指针 #define __crt_va_end(ap) ((void)(ap = (va_list)0)) #define va_start __crt_va_start #define va_arg __crt_va_arg #define va_end __crt_va_end // 地址赋值 , 直接等于 主要用于 ap_two = ap_one // 具体 写法就是 va_copy(ap_two,va_one) , 目前基本是冷板凳 #define va_copy(destination, source) ((destination) = (source))
到这里C可变函数机制的源码分析完毕.
1.2 通过一个例子将可变参数机制结尾
我们的业务需求是这样的, 需要一个机器扫描 输入的字符串,输入的字符串个数是不确定的.
并从中找出 长度 小于 5的 字符串,输出 索引和当前串的内容.代码如下
#include <stdio.h> #include <stdlib.h> #include <stdarg.h> #include <string.h> //简单的日志宏 fmt必须是字面量字符串 #define ERRLOG(fmt,...) \ fprintf(stderr,"[%s:%s:%d]" fmt "\r\n",__FILE__,__FUNCTION__,__LINE__,##__VA_ARGS__) //简单系统等待函数 #define _STR_PAUSE "请按任意键继续. . ." #define SPAUSE() \ rewind(stdin),printf(_STR_PAUSE),getchar() // // 需要一个机器扫描 输入的字符串,输入的字符串个数是不确定的.并从中找出 长度 小于 5的 字符串, 输出 索引和当前串的内容 // #define _INT_FZ (5) // // 这里 最后一个参数 必须是 NULL,同 linux中execl函数簇参数要求 // sstr : 开始的串 // void with_stdin(const char *sstr, ...); int main(int argc, char *argv[]) { with_stdin(NULL); with_stdin("1","1234331","adfada","ds",NULL); with_stdin("a","ad","adf","asdfg","asdsdfdf","123131",NULL); with_stdin("1","3353432", "1234331", "adfada", "ds","dasafadfa","dasdas", NULL); SPAUSE();//等待函数 return 0; } void with_stdin(const char *sstr, ...) { static int __id; // 第一声明的时候赋值为0,理解成单例 va_list ap; const char *tmp; if (NULL == sstr) { ERRLOG("Warning check NULL == sstr."); return; } if (_INT_FZ > strlen(sstr)) printf("%d %s\n",__id,sstr); ++__id; va_start(ap, sstr); while ((tmp = va_arg(ap, const char*)) != NULL) { if (_INT_FZ > strlen(tmp)) printf("%d %s\n", __id, tmp); ++__id; } va_end(ap); }
2.printf 函数可重入讨论
首先我们需要搭建一个pthread 开发环境在 Window上,如果你是用Linux,稍微新一点的系统,现在都是默认pthread线程库.下面 我就讲解 pthread 如何搭建.
第一步 去官网上下载源码包
http://sourceware.org/pthreads-win32/
自己多点点点,下载最新版的目前是 2-9-1,好久没更新了,在window上使用,还有点麻烦,需要简单的修改源代码.
第二步 建一个C控制台
用VS2015 建一个 空的控制台.如下
第三步 在控制台中添加 一些文件
需要添加的文件如下:
需要添加到 刚才项目 (右击在文件夹下打开那个位置) 如下图
最后是这样的
这里配置的是x86 开发环境文件多,配置x64文件就很少了. 这个学会了 以后 就特别简单了.
第四步:修改头文件 去掉冲突
先添加那些头文件 shift + alt + A,将 三个头文件添加到项目里来,如下:
将 pthread.h 下面 299行 改成 下面这样,直接在当前目录下找头文件
#include "sched.h"
在315行 回车一下 添加下面宏声明,去掉重复结构定义
#define HAVE_STRUCT_TIMESPEC
第五步 添加一些文件包含
首先 添加 VS取消安全监测宏 _CRT_SECURE_NO_WARNINGS
在项目右击选择属性,或者 键盘右击键 + R
后面添加静态库
后面其它静态库,当找不见了自己添加. 当然如果 你想在 VS 通过代码添加静态库 ,代码 如下
// 添加 静态库 pthreadVC2.lib // 放在 文件一开始位置处,一般放在头文件中 #pragma comment(lib,"pthreadVC2.lib")
到这里环境就配置好了. 下面 直接切入正题 .
2.1 printf 函数测试
首先 测试 代码如下 ,需要同学自己敲一遍,关于pthread的代码 还是比较复杂,当然就算我们开发库用到的基本上是它中下难度部分api.
#include <stdio.h> #include <stdlib.h> #include "pthread.h" //简单的日志宏 fmt必须是字面量字符串 #define ERRLOG(fmt,...) \ fprintf(stderr,"[%s:%s:%d]" fmt "\r\n",__FILE__,__FUNCTION__,__LINE__,##__VA_ARGS__) //简单系统等待函数 #define _STR_PAUSE "请按任意键继续. . ." #define SPAUSE() \ rewind(stdin),printf(_STR_PAUSE),getchar() //每个线程打印的条数 #define _INT_CUTS (1000) //开启的线程数 #define _INT_PTHS (4) //线程一打印数据 #define _STR_ONES "1111111111111111111111111222222222222222222222222222222222222222222222222222222222223333333333333333333333333333333333333333333333333334444444444444444444444444444444444444445555555555555555555555555555666666666666666666666666666677777777777777777777777777777777777777777777777777778888888888888888888888888888888883333333333333333333333332222222222222222222222211111111111111888888888888888888888888888899999999999999999999999999999999999999990000000000000000000000000000000" //线程二打印数据 #define _STR_TWO "aaaaaaaaaaaaaaaaaaaaaaassssssssssssssssssssdddddddddddddddddddddddddddddddddddddddfffffffffffffffffffffffffffgggggggggggggggggggggggggggggggggghhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkfffffffffffffffffffffffffffffffffffffffffoooooooooooooooooooooooppppppppppppppppppppppppppppvvvvvvvvvvvvvvvvbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbdddddddddddddds" //线程三打印数据 #define _STR_THRE "AAAAAAAAAAAAAAAAAAAAQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOPPPPPPPPPPPPPPPPPPPPPPPBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBNNNNNNNNNNNNNNNNNNNNNNNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMDDDDDDDDDDDDDDDDDDDDDDDDDDDSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSCCCCCCCCCCCCCCCCCCCCCCGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGSSSCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCFFFFFFFFFFFFFFFF" //线程四打印数据 #define _STR_FIV "你好好的打打打假摔帝卡发的啥都就看见大大淡蓝色空间对手卡就考虑到就阿里'省空间打算加快递费的数量级匮乏绿豆沙圣诞快乐发送的房间打扫房间卡萨丁就卡机了速度快龙卷风撒娇考虑到房间里邓丽君分手的距离看法就立刻发家里睡觉了舒服大家啦的酸辣粉就看见了看法就李开复撒地方就拉近了看法就困啦风刀霜剑快乐付京东坑垃圾费即可复读机啊健康路附近啊范德萨晶晶啊加合法的考虑加对方说对啦地方睡觉了啥打法来空间浪费大家来看范德萨龙卷风就阿里你好好的打打打假摔帝卡发的啥都就看见大大淡蓝色空间对手卡就考虑到就阿里'省空间打算加快递费的数量级匮乏绿豆沙圣诞快乐发送的房间打扫房间卡萨丁就卡机了速度快龙卷风撒娇考虑到房间里邓丽君分手的距离看法就立刻发家里睡觉了舒服大家啦的酸辣粉就看见了看法就李开复撒地方就拉近了看法就困啦风刀霜剑快乐付京东坑垃圾费即可复读机啊健康路附近啊范德萨晶晶啊加合法的考虑加对方说对啦地方睡觉了啥打法来空间浪费大家来看范德萨龙卷风就阿里" //全局测试 static FILE *__txt; //写入测试文件路径 #define _STR_PATH "log.txt" //线程启动函数 void *start_printf(void *arg); int main(int argc, char *argv[]) { pthread_t ths[_INT_PTHS]; int i, j; int rt; puts("printf 线程是否安全测试开始"); if ((__txt = fopen(_STR_PATH, "w")) == NULL) { ERRLOG(_STR_PATH "文件打开失败"); exit(-1); } for (i = 0; i<_INT_PTHS; ++i) { rt = pthread_create(ths + i, NULL, start_printf, (void*)i); if (0 != rt) { ERRLOG("pthread_create run error %d!", rt); goto __for_join; } } __for_join: //等待线程结束 for (j = 0; j<i; ++j) pthread_join(ths[j], NULL);//索引访问错误 puts("printf 线程是否安全测试结束"); SPAUSE();//等待函数 return 0; } //线程启动函数 void * start_printf(void *arg) { int idx = (int)arg; int i; printf("线程%d已经启动!\n", idx); for (i = 0; i<_INT_CUTS; ++i) { switch (idx) { case 0: fprintf(__txt, _STR_ONES); break; case 1: fprintf(__txt, _STR_TWO); break; case 2: fprintf(__txt, _STR_THRE); break; case 3: fprintf(__txt, _STR_FIV); break; default: printf("idx => %d 取你吗的.\r\n", idx); } } printf("线程%d已经关闭!\n", idx); return (void*)idx; }
这里运行的结果如下:
当然还有生成的 log.txt 文件,
检查结果是没有出现乱序现象, 后面看 完<<posix 多线程程序设计>> 之后, 它那里有这么一句话,posix要求ANSI C 中标准输入输出函数式线程安全的.
所以这种老标准都安全,现在不用说了.
后来在 printf 源码中找见了
/* Lock stream. */ _IO_cleanup_region_start ((void (*) (void *)) &_IO_funlockfile, s); _IO_flockfile (s);
就是加锁的意思.所以printf 是可重入的函数.说了这么多,其实意思 以后 写文件可以直接拼一个大串直接printf 就可以了.
这个细节会让自己做的日志库轮子快一点.
3.printf函数的源码实现
这里同样我也以window 为例 . 具体见下面代码
int __cdecl printf ( const char *format, ... ) /* * stdout 'PRINT', 'F'ormatted */ { va_list arglist; int buffing; int retval; _VALIDATE_RETURN( (format != NULL), EINVAL, -1); va_start(arglist, format); _lock_str2(1, stdout); __try { buffing = _stbuf(stdout); retval = _output_l(stdout,format,NULL,arglist); _ftbuf(buffing, stdout); } __finally { _unlock_str2(1, stdout); } return(retval); }
是不是感觉很简单,先简单检测一下
后面获取fmt之后的参数,并且加锁 调用另一个系统输出函数_output_l
最后解锁 返回结果.
哈哈,其实 printf函数 源码 真的很简单,只要理解了 可变参数机制读上面代码很容易.它的复杂见另一个函数.
Linux上是vprintf函数,window上是_output_l函数,以vprintf为例,难点在于 格式语法解析,
它完成的功能相当于一个简单的 代码解析器. 总共实现代码2千多行. 看看觉得 Linux内核确实比较屌,单单这个vprintf.
实现就用了
C模板技术
状态表机制
底层文件读写,CPU变量优化,宏,指针,共用体漫天飞.但这个函数 还是可以搞得.主要思路是围绕 状态表(可以理解为业务表)
完成相应的功能,在完成过程中,对流进行控制,该保存的保存,该输出输入,改扩容的扩容,通过文件锁锁住 流输入输出.
其实有的时候 技术 还是有点难的, 更多国同行喜欢不是技术,而是 能够提高 人命币的 手段,顺带做一件其它事.
穷人没有选择,有的是生存和挣扎.长这么大才明白初中生物老师说的,物竞天择适者生存,呵呵大合唱.
后记
到这里基本就结束,有点虎头蛇尾,但是printf 2千行代码,要是解析起来,其实也就是说白话.熟悉了都是设计和业务.
肯定有错的,例如错别字,技术错误等等,欢迎交流指正,下次右机会分享pthread 开发专题.最后后面分享几个 本文参考的东西
1. C底层库源码 Window和Linux
2. posix 多线程程序设计