承接前文,本文深入剖析 getc 與 ungetc 兩個核心字元輸入函數的內部實現,揭示 FILE 結構體的緩衝區管理機制。
unsigned char 再擴展為 intc (轉為 unsigned char) 推回輸入流fseek/rewind) 會丟棄所有推回字元核心觀念:getc = 讀字元 + 指標前移;ungetc = 指標後移 + 字元回填。
fopen 後未分配緩衝區,何時分配?_base) 呼叫 ungetc 會怎樣?ungetc 失敗是否因指標達到 _base?int ungetc(int c, FILE *stream) {
if (c == EOF) return EOF;
if (!__ioinit()) return EOF; // 確保 I/O 初始化
// 若無緩衝區,嘗試分配
if (stream->_base == NULL) _getbuf(stream);
// 指標在緩衝區起始且無推回字元時,無需後移
if (stream->_ptr == stream->_base && stream->_cnt == 0) {
stream->_flag |= _IOREAD;
*--stream->_ptr = (char)c;
stream->_cnt++;
stream->_flag &= ~_IOEOF;
stream->_flag |= _IOREAD;
return (unsigned char)c;
}
// 一般情況:指標後移、字元回填
if (stream->_ptr > stream->_base) {
*--stream->_ptr = (char)c;
stream->_cnt++;
stream->_flag &= ~_IOEOF;
stream->_flag |= _IOREAD;
return (unsigned char)c;
}
return EOF; // 無法推回
}
FILE *_getbuf(FILE *stream) {
// 嘗試分配標準大小緩衝區
stream->_base = malloc(BUFSIZ);
if (stream->_base) {
stream->_bufsiz = BUFSIZ;
stream->_flag |= _IOMYBUF; // 標記自行分配
} else {
// 分配失敗:使用 _charbuf (1 int = 2 bytes) 作為極小緩衝區
stream->_base = (char *)&stream->_charbuf;
stream->_bufsiz = 2;
}
stream->_ptr = stream->_base;
stream->_cnt = 0;
return stream;
}
_charbuf 確保緩衝區分配失敗時仍能單字元運作。
#define getc(stream) (--(stream)->_cnt >= 0 ? \
0xff & *(stream)->_ptr++ : _filbuf(stream))
邏輯:
_cnt > 0:緩衝區有資料,直接取字元、指標前移、計數遞減_cnt <= 0:緩衝區空,呼叫 _filbuf 從檔案填充int _filbuf(FILE *stream) {
// 檢查讀取權限、錯誤狀態
if (stream->_flag & (_IOERR|_IOEOF|_IOSTRG)) return EOF;
if (!(stream->_flag & (_IOREAD|_IORW))) return EOF;
// 分配緩衝區 (若無)
if (stream->_base == NULL) _getbuf(stream);
// 呼叫底層 _read 系統呼叫
stream->_ptr = stream->_base;
stream->_cnt = _read(_fileno(stream), stream->_base, stream->_bufsiz);
if (stream->_cnt <= 0) {
if (stream->_cnt == 0) stream->_flag |= _IOEOF;
else stream->_flag |= _IOERR;
stream->_cnt = 0;
return EOF;
}
// 文字模式換行符處理 (CRLF -> LF)
// ... 省略 ...
stream->_cnt--;
return 0xff & *stream->_ptr++;
}
最終呼叫 Win32 API ReadFile 將磁碟資料讀入使用者模式緩衝區。文字模式下額外處理 \r\n -> \n 轉換。
typedef struct _iobuf {
char *_ptr; // 緩衝區當前讀寫位置
int _cnt; // 緩衝區剩餘字元數
char *_base; // 緩衝區起始位址
int _flag; // 標誌位 (_IOREAD, _IOWRT, _IOMYBUF, _IOEOF, _IOERR...)
int _file; // 檔案描述符
int _charbuf; // 緩衝區分配失敗時的 1-char 後備緩衝
int _bufsiz; // 緩衝區大小
char *_tmpfname; // 暫存檔名
} FILE;
getc 與 ungetc 首次發現 _base == NULL 時透過 _getbuf 分配。ungetc 分配後不讀取資料。_ptr == _base 且 _cnt == 0 (緩衝區空),ungetc 可在 _base 處存放一字元,指標不後移。_ptr 後移至 _base 後無法再後移,因緩衝區前無空間。標準僅保證一字元推回。FILE 結構體以 _base/_ptr/_cnt 三元組管理緩衝區狀態,getc/ungetc 協同運作實現高效字元流讀取與有限回溯。理解此機制有助於正確使用標準 I/O 並避免常見陷阱。