C 中 关于printf 函数中度剖析

题外话 

      这篇博文主要围绕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关闭停留一下,输出一段话等待一下. 效益图如下

图片 1

斯作用在 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

 

 

 

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("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 建一个 空的主宰台.如下

图片 2

其三步  在控制台中补充加 一些文书

  需要添加的文件如下:

图片 3

 

图片 4

图片 5

需添加到 刚才路 (右击在文书夹下开拓那个位置) 如下图

图片 6

说到底是如此的

图片 7

此地配置的是x86 开发环境文件多,配置x64文件就挺少了. 这个学会了 以后 就专门简单了.

季步:修改头文件 去丢冲突

 先添加那些头文件 shift + alt
+ A,将 三个头文件上加至品种里来,如下:

图片 8

将 pthread.h 下面 299实行 改成为 下面这样,直接当当前目录下找头文件

#include "sched.h"

以315行 回车一下 添加脚宏声明,去丢重结构定义

#define HAVE_STRUCT_TIMESPEC

第五步 添加有文书包含

首先 添加 VS取消安全监测宏 _CRT_SECURE_NO_WARNINGS

图片 9

在路右击选择属性,或者 键盘右击键 + R

背后加加静态库

图片 10

末尾其它静态库,当找不展现了祥和上加. 当然要 你想在 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;
}

这里运行的结果如下:

图片 11

自然还有变化的 log.txt 文件,

图片 12

反省结果是不曾出现乱序现象, 后面看 完<<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 多线程程序设计