首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

深入 C 標準庫 (3):stdio.h 中 getc 與 ungetc 的實現機制

     編輯:關於C語言

前言

承接前文,本文深入剖析 getc 與 ungetc 兩個核心字元輸入函數的內部實現,揭示 FILE 結構體的緩衝區管理機制。

函數規格回顧

int getc(FILE *stream)

  • 從輸入流讀取下一字元,轉為 unsigned char 再擴展為 int
  • 檔案位置指標前移一位
  • 返回字元值,EOF 表示檔案結束或讀取錯誤

int ungetc(int c, FILE *stream)

  • 將字元 c (轉為 unsigned char) 推回輸入流
  • 後續讀取按推回逆序返回
  • 呼叫定位函數 (fseek/rewind) 會丟棄所有推回字元
  • 最少保證一字元推回,多次推回可能失敗
  • 成功呼叫清除檔案結束標記

核心觀念:getc = 讀字元 + 指標前移;ungetc = 指標後移 + 字元回填。

關鍵問題

  1. fopen 後未分配緩衝區,何時分配?
  2. 在檔案開頭 (指標指向 _base) 呼叫 ungetc 會怎樣?
  3. 多次 ungetc 失敗是否因指標達到 _base?

原始碼深度解析

1. ungetc 實現

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;  // 無法推回
}

2. _getbuf 緩衝區分配

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 確保緩衝區分配失敗時仍能單字元運作。

3. getc 實現 (巨集展開)

#define getc(stream) (--(stream)->_cnt >= 0 ? \
    0xff & *(stream)->_ptr++ : _filbuf(stream))

邏輯:

  • _cnt > 0:緩衝區有資料,直接取字元、指標前移、計數遞減
  • _cnt <= 0:緩衝區空,呼叫 _filbuf 從檔案填充

4. _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++;
}

5. _read 系統呼叫 (Windows)

最終呼叫 Win32 API ReadFile 將磁碟資料讀入使用者模式緩衝區。文字模式下額外處理 \r\n -> \n 轉換。

FILE 結構體完整定義

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;

問題解答

  1. 緩衝區分配時機:getc 與 ungetc 首次發現 _base == NULL 時透過 _getbuf 分配。ungetc 分配後不讀取資料。
  2. 檔案開頭推回字元:若 _ptr == _base 且 _cnt == 0 (緩衝區空),ungetc 可在 _base 處存放一字元,指標不後移。
  3. 多次推回失敗原因:_ptr 後移至 _base 後無法再後移,因緩衝區前無空間。標準僅保證一字元推回。

結語

FILE 結構體以 _base/_ptr/_cnt 三元組管理緩衝區狀態,getc/ungetc 協同運作實現高效字元流讀取與有限回溯。理解此機制有助於正確使用標準 I/O 並避免常見陷阱。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved