題外話
這篇博文主要圍繞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
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 建一個 空的控制台.如下
第三步 在控制台中添加 一些文件
需要添加的文件如下:
需要添加到 剛才項目 (右擊在文件夾下打開那個位置) 如下圖
最後是這樣的
這裡配置的是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 多線程程序設計