题外话 

这篇博文主要围绕printf函数分析的,主要讲解printf 使用C的可变参数机制, printf是否可重入(是否线程安全),

printf函数的源码实现.

正文

1.C中可变参数机制

我们先举个例子,假如现在有这样一个需求 "需要一个不定参数整型求和函数".

具体实现代码如下

// 需要一个不定参数整型求和函数
int
sum_add(int len, ...)
{
int sum = ;
va_list ap; va_start(ap, len); // 初始化 将ap参数指向 len 下一个参数位置处
while (len > ) {
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(, );
printf("sum = %d\n",sum); sum = sum_add(,,,,);
printf("sum = %d\n", sum); sum = sum_add(, , , , ,,,,,,);
printf("sum = %d\n", sum); system("pause");
return ;
}

这里扯一点,对于system("pause"); 是调用系统shell 的pause命令,就是让当前cmd关闭停留一下,输出一段话等待一下. 效果图如下

这个功能在 Linux 有个 系统函数如下

#include <unistd.h>

// 函数说明:pause()会令目前的进程暂停(进入睡眠状态),直至信号(signal)所中断。
// 返回值:只返回-
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
 
stdarg.h宏
巨集名称
描述
相容
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("","","adfada","ds",NULL);
with_stdin("a","ad","adf","asdfg","asdsdfdf","",NULL);
with_stdin("","", "", "adfada", "ds","dasafadfa","dasdas", NULL); SPAUSE();//等待函数
return ;
} 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(-);
} for (i = ; i<_INT_PTHS; ++i) {
rt = pthread_create(ths + i, NULL, start_printf, (void*)i);
if ( != rt) {
ERRLOG("pthread_create run error %d!", rt);
goto __for_join;
}
} __for_join:
//等待线程结束
for (j = ; j<i; ++j)
pthread_join(ths[j], NULL);//索引访问错误 puts("printf 线程是否安全测试结束"); SPAUSE();//等待函数
return ;
} //线程启动函数
void *
start_printf(void *arg)
{
int idx = (int)arg;
int i; printf("线程%d已经启动!\n", idx);
for (i = ; i<_INT_CUTS; ++i) {
switch (idx) {
case :
fprintf(__txt, _STR_ONES);
break;
case :
fprintf(__txt, _STR_TWO);
break;
case :
fprintf(__txt, _STR_THRE);
break;
case :
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, -); va_start(arglist, format); _lock_str2(, stdout);
__try {
buffing = _stbuf(stdout); retval = _output_l(stdout,format,NULL,arglist); _ftbuf(buffing, stdout); }
__finally {
_unlock_str2(, 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 多线程程序设计

C 中 关于printf 函数中度剖析的更多相关文章

  1. MFC 中的 “printf” 函数

    怀念C语言的我,MFC没法使用的C语言printf函数,于是: int MFCprintf(const char* m_data, ...){ CString str; char printf_buf ...

  2. 在keil中使用printf()函数的要点

    在keil中printf默认是向串口中发送数据的,所以,如果应用该函数,必须先初始化串口,否则可能引起死机的情况,并且在printf之前应该先将TI置位,摘抄原因如下: 1.printf函数是调用pu ...

  3. C语言中的可变参数函数的浅析(以Arm 程序中的printf()函数实现为例) .

    我们在C语言编程中会遇到一些参数个数可变的函数,一般人对它的实现不理解.例如Printf(): Printf()函数是C语言中非常常用的一个典型的变参数函数,它 的原型为: int printf( c ...

  4. trueStudio中使用printf函数

    1.通过printf输出浮点数需要如下设置: 在工程属性下找到C/C++ build->Settings->Tool Settings->C Linker->Miscellan ...

  5. 关于在MDK中使用 printf 函数

    microlib 提供了一个有限的 stdio 子系统,它仅支持未缓冲的 stdin.stdout 和 stderr. 这样,即可使用 printf() 来显示应用程序中的诊断消息. 要使用高级 I/ ...

  6. STM32中如何对printf函数重定向

    通过USART1向计算机的串口调试助手打印数据,或者接收计算机串口调试助手的数据,接下来我们现STM32工程上的printf()函数,方便用于程序开发中调试信息的打印. 方法一:使用MicroLIB库 ...

  7. STM32 printf()函数和scanf()函数重定向到串口

    STM32 printf()函数和scanf()函数重定向到串口 printf()函数和scanf()函数重定向 在学习STM32的时候,常常需要用串口来测试代码的正确与否,这时候就要要用到print ...

  8. 通过串口利用printf函数输出数据

    一.printf函数格式 printf函数具有强大的输出功能 %表示格式化字符串输出 目前printf支持以下格式的输出,例如: printf("%c",a);输出单个字符. pr ...

  9. php中sprintf与printf函数用法区别

    下面是一个示例:四舍五入保留小数点后两位  代码如下 复制代码 <?php$num1 = 21;echo sprintf("%0.2f",$num1)."<b ...

随机推荐

  1. 怎样对CODESOFT中的条形码进行黑白转换

      CODESOFT 2015标签设计软件能 够提供无与伦比的灵活性.功能和支持,其面对的用户也是极其的广泛.对于一些需要打印黑白反转条形码的特殊用户,例如使用黑色标签纸的用 户,CODESOFT 2 ...

  2. hough变换

    //c.h typedef unsigned char BYTE;typedef unsigned short WORD;typedef unsigned int DWORD;typedef long ...

  3. Drupal8开发教程:认识.info.yml文件

    YAML 文件是新引入的重要项目文件,在 Drupal 8 中,无论是模块.主题还是安装配置文件,都需要 .info.yml 文件来为其存储项目相关的基础信息. 在 Drupal 中,.info.ym ...

  4. Android开发-API指南-Android简介

    Introduction to Android 英文原文:http://developer.android.com/intl/zh-cn/guide/index.html 采集日期:2014-4-16 ...

  5. Windows 7的100M隐藏分区

    1.Windows 7的100MB的隐藏分区是Windows 7的活动分区,类似于Linux的/boot. 这其实有点类似Linux的做法,Linux在安装过程中可以专门分出一个100MB左右的分区作 ...

  6. 华为OJ平台——矩阵乘法

    题目描述: 如果A是个x行y列的矩阵,B是个y行z列的矩阵,把A和B相乘,其结果将是另一个x行z列的矩阵C. 输入: 1.第一个矩阵的行数 2.第一个矩阵的列数(也是第二个矩阵的行数) 3.第二个矩阵 ...

  7. LINQ to DataSet的DataTable操作

    1. DataTable读取列表 DataSet ds = new DataSet();// 省略ds的Fill代码DataTable products = ds.Tables["Produ ...

  8. python md5

    import hashlib import os 简单的测试一个字符串的MD5值 src = 'teststring' print (hashlib.md5(src).hexdigest().uppe ...

  9. Ax 从一个form关闭另外一个form,AX全局变量

    如果这个两个form存在调用关系,我们当然可以在调用的时候把对象传过来,然后再关闭之. 但是当2个form没有被调用的关系,我们可以利用infolog.globalCache()将FORM对象保存起来 ...

  10. AX 利用windows粘贴板功能实现批量数据快速导出EXCEL

    static void test(Args _args) { int lineNum; int titleLines; SysExcelApplication excel; SysExcelWorkb ...