背景
文章貼出 Microsoft C Runtime (CRT) 中 output.c 完整源碼(版權屬微軟),這是 printf 族函數(printf、fprintf、sprintf、vprintf 等)的核心實現,採用**有限狀態機 (FSM)** 解析格式字串,逐字符處理,支援 ANSI C、Unicode、多執行緒、多種平台(Win32、POSIX、Mac、Alpha)。
核心架構:狀態機解析格式字串
狀態定義
enum STATE {
ST_NORMAL, // 普通字符,直接輸出
ST_PERCENT, // 讀到 '%'
ST_FLAG, // 旗標:- + 空格 # 0
ST_WIDTH, // 欄位寬度
ST_DOT, // 讀到 '.'
ST_PRECIS, // 精度
ST_SIZE, // 大小修飾:h l L w 等
ST_TYPE // 類型指示符:d i u x X o c s p n e f g 等
};
字符分類查表法
- 預先生成
__lookuptable[](由 maketabc.c 產出),每個字節高 4 位存**下一狀態**,低 4 位存**字符類別**。
- O(1) 查表決定狀態轉移,無需複雜
if-else 鏈。
關鍵數據結構
union {
char *sz; // 多位元組字串指標
wchar_t *wz; // 寬字符字串指標
} text;
union {
char sz[BUFFERSIZE]; // 轉換緩衝區 (512 bytes)
wchar_t wz[BUFFERSIZE]; // 寬字符版本
} buffer;
BUFFERSIZE = 512 滿足 ANSI C 最小 509 字元要求,並容納最大浮點數轉換結果。
bufferiswide 標記緩衝區當前內容類型。
核心流程:_output() / _woutput()
- 初始化旗標、寬度、精度、前綴、基數等變數。
- 主迴圈:逐字符讀取格式字串 → 查表得類別 → 查表得下一狀態 →
switch(state) 執行對應動作。
- 狀態
ST_TYPE 時,進入巨大 switch(ch) 分派具體類型轉換:
- 整數:COMMON_INT 統一處理 d/i/u/o/x/X/p,支援基數 8/10/16、旗標、精度、前綴。
- 浮點數:呼叫 _cfltcvt / _cldcvt,處理 e/f/g/E/G,支援大小寫指數、# 旗標強制小數點、g 格式裁剪尾隨零。
- 字符/字串:c/C/s/S/Z,處理多位元組⇄寬字符轉換、NULL 指標顯示 (null)、精度截斷。
- %n:將已輸出字符數寫回對應指標,不產生輸出。
- 轉換完成後,統一處理對齊、填充、前綴、實際輸出(
WRITE_CHAR/WRITE_STRING/WRITE_MULTI_CHAR)。
- 返回總輸出字符數,或 -1 表示 I/O 錯誤。
多平台/字符集適配細節
| 宏定義 | 用途 |
| WPRFLAG | 寬字符版本 (wprintf 族) |
| CPRFLAG | 控制台直接輸出 (_cprintf) |
| _UNICODE / _MBCS | 字符集模式 |
| _WIN32_ / _POSIX_ / _CRUISER_ / _MAC_ | 目標平台,決定查表內容 |
| _ALPHA_ | DEC Alpha 64-bit 整數支援 (__int64 / FL_LONGDOUBLE) |
輸出輔助函數
LOCAL(void) write_char(int ch, FILE *f, int *pnumwritten); // 單字符
LOCAL(void) write_multi_char(int ch, int num, FILE *f, int *pnw); // 重複字符
LOCAL(void) write_string(char *s, int len, FILE *f, int *pnw); // 字串
// 以上均有 WPRFLAG/CPRFLAG 變體
- 全部為
static _CALLTYPE4(LOCAL 宏),內聯、無鎖(註明 **** DOESN'T CURRENTLY DO MTHREAD LOCKING ****)。
- 錯誤時將
*pnumwritten = -1,主迴圈檢測到 < 0 即中斷。
參數提取宏
__inline int get_int_arg(va_list *pargptr) { return va_arg(*pargptr, int); }
#if !LONG_IS_INT
__inline long get_long_arg(va_list *pargptr) { return va_arg(*pargptr, long); }
#endif
#if !SHORT_IS_INT
__inline short get_short_arg(va_list *pargptr) { return va_arg(*pargptr, short); }
#endif
#ifdef _ALPHA_
__inline __int64 get_quad_arg(va_list *pargptr) { return va_arg(*pargptr, __int64); }
#endif
根據目標平台類型大小(LONG_IS_INT、PTR_IS_INT 等宏)自動選擇正確提取方式。
歷史修訂紀錄亮點
- 1989-1992 年持續維護,支援從 16-bit DOS 到 Win32、Alpha、Unicode、MBCS、POSIX。
- 早期處理
va_arg 對 double 不一致問題(1991-12-05)。
- 逐步加入
%Z (counted string)、%wc/%ws、%tc/%ts 等擴展格式。
學習價值
- **工業級 FSM 解析器** 範本:查表驅動、狀態清晰、擴展性強。
- **可移植性設計**:平台差異集中在宏與查表,核心邏輯不變。
- **邊界處理**:精度預設、旗標衝突、NULL 指標、緩衝區溢出防護、多位元組/寬字符轉換錯誤處理。
- **性能優化**:整數轉換就地反向寫入緩衝區末尾,避免額外反轉;浮點數委託專用轉換器。
註:源碼版權屬 Microsoft,僅供學習研究參考。
- 上一篇文章: 如何成為優秀的 C 程式設計師:經典書籍推薦與學習路徑
- 下一篇文章: C 語言筆試經典題:指針運算與函數參數求值順序陷阱
Copyright ©
程式碼之家 All Rights Reserved