C語言之輸入輸出

libq8發表於2024-10-24

標準庫 IO

輸入輸出功能並非C語言的組成部分,ANSI標準定義了相關的庫函式

輸入輸出 <stdio.h>

流stream是與裝置關聯的資料的源或者目的地。

  • 文字流:由文字行組成的序列
    不同系統的特性可能不一樣,比如行最大長度和行結束符
  • 二進位制流:未經處理的位元組序列

程式執行時,預設開啟 stdin, stdout, stderr

標準 IO 常量

  • EOF:檔案尾,實際值是幾個字元避免混淆
  • FOPEN_MAX:一個程式同時最多能夠開啟的檔案數量,與編譯器有關,值至少為 8
  • FILENAM_MAX:編譯器支援的最長檔名

檔案流操作

檔案指標指向包含檔案資訊的結構,包括緩衝區,讀寫狀態等

開啟與關閉

FILE *fopen(const char *filename, const char *mode);                    // 開啟檔案流
/*
mode:
* r, 開啟讀
* w, 開啟或建立寫,刪除原有內容
* a, 開啟或者建立,檔案尾追加
* +, 更新,讀或寫,交叉操作前需要執行fflush或者定位操作
  * r+, 開啟檔案更新(讀和寫)
  * w+, 建立檔案更新,刪除原有內容
  * a+, 開啟或建立檔案,檔案尾更新
* b, 二進位制流模式

應始終檢查返回值
*/

FILE *freopen(const char *filename, const char *mode, FILE *stream);    // 先關閉再開啟檔案流

FILE *fdopen(int fd, const char *type);   // POSIX標準,常用於由建立管道和網路通訊通道函式返回的描述符,不能直接用 fopen 開啟
// type 引數: r, w, a, (+)
// 注意讀寫模式下讀寫操作之間需要進行呼叫定位函式


int fclose(FILE *stream);                                               // 重新整理輸出緩衝,釋放系統緩衝區,關閉流


int fileno(FILE *fp);   // POSIX 標準

流的定向

標準IO可用於單位元組和多位元組字符集,具體由流的定向決定。

  • freopen函式清除一個流的定向
  • fwide函式設定流的定向、
#include <stdio.h>
#include <wchar.h>

int fiwde(FILE *fp, int mode);
/*
mode<0, 試圖設定為單位元組
mode>0,試圖設定為多位元組
mode=0,不設定定向,返回流的定向

不會改變已經設定的流定向
*/

檔案操縱函式

重新命名

int remove(FILE *stream);                               // 刪除檔案

int rename(const char *oldname, const char *newname); // 重新命名檔案

臨時檔案

FILE *tmpfile(void);     // 以`wb+`模式建立臨時檔案,在關閉或者程式結束時自動刪除
// 實現上tmpnam,建立檔案,unlink(不會刪除內容),檔案的關閉會在程式結束中自動進行

char *tmpnam(char s[L_tmpnam]); // 建立不同現有檔名的字串,NULL返回指向靜態陣列的指標

// UNIX 優勢是不存在時間間隙,避免其他程序建立同名檔案
char *mkdtemp(char *temple);  // 建立目錄
int mkstmp(char *temple);     // 建立檔案,不會自動刪除

緩衝操作

標準IO提供緩衝的目的是儘可能減少read/write的呼叫次數

  • 全緩衝:緩衝區滿了才進行實際IO操作

  • 行緩衝:遇到換行符才進行IO操作

    • 行緩衝的長度是固定的,行緩衝滿了即使沒有換行符也會進行IO操作
    • 標準IO要求從不帶緩衝或者行緩衝(需要從核心請求資料)獲取資料,會立即重新整理所有行緩衝的輸出流
  • 不帶緩衝

ISO C標準

  • 當且僅當標準輸入和標準輸出不指向互動式裝置才是全緩衝的
  • 標準錯誤不會是全緩衝的

預設情況

  • 標準錯誤不帶緩衝
  • 若是指向終端裝置的流,則是行緩衝的,否則是全緩衝的

更換緩衝的型別:流被開啟之後,且在執行任何操作之前

int fflush(FILE *stream);                                               // 重新整理緩衝區
/*
- 對於輸出流,重新整理寫入緩衝區的內容至目標檔案
- 對於輸入流,其結果是未定義的
- NULL,重新整理所有的緩衝區

實際使用技巧:每個除錯 printf 之後立馬呼叫 fflush
*/

int setvbuf(FILE *stream, char *buf, int mode, size_t size);            // 必須在執行讀寫操作之前設定緩衝
/*
mode:
- _IOFBF 完全緩衝
- _IOLBF 行緩衝
- _IONBF 不設定緩衝
*/

void setbuf(FILE *stream, char *buf);     // char buf[BUFSIZ]
/*
- buff為 NULL, 關閉緩衝
- 否則,等價於 `_IOFBF`

// 注意:buf 不要使用自動變數型別,儘量使用系統緩衝區或者動態分配記憶體
*/

​#TODO#​檢視流緩衝狀態 《UNIX高階環境程式設計》

讀寫流操作

字元 IO

​#TODO#​輸入輸出函式家族 《C和指標》P301

int fgetc(FILE *stream);  // unsigned char 轉 int, 相容EOF
int getc(FILE *stream);   // 等價於 fgetc,注意實現為宏
int getchar(void);        // 等價於 getc(stdin)

int fputc(int c, FILE *stream);
int putc(int c, FILE *stream);  // 等價於 fputc, 注意實現為宏
int putchar(int c);             // 等價於 fputc(stdout)

// 只有fgetc和fputc是函式,其他都是宏

int ungetc(int c, FILE *stream);  // 將字元退回流中,依賴於當前位置
// 不同於寫操作,僅涉及流本身而無關裝置儲存

未格式化的行IO

char *fgets(char *s, int n, FILE *stream);  // 自動包含換行符,\n 換為 \0,最多n-1
char *gets(char *s);                        // 不自動包含換行符。沒有緩衝區長度引數,可能導致越界;不推薦使用,已廢棄


int fputs(const char *s, FILE *stream);     // 不自動包含換行符\n,逐字元輸入任意個數換行符
int puts(const char *s);                    // 自動新增換行符\n

ssize_t getline(char **lineptr, size_t *n, FILE *stream);
// 根據輸入動態分配記憶體,無需預先確定輸入字串的最大長度
// 在儲存的字串中將換行符替換為字串結束符'\0'

格式化 IO

int fprintf(FILE *stream, const char *format, ...);
int printf(const char *format, ...);                  // 等價於 `fprintf(stdout, fotmat, ...)`
int sprintf(char *s, const char *format, ...);        // 包含結束符 NUL,無長度引數,可能越界溢位
int snprintf(char *buf, szie_t n, const char *format, ...); // 超出部分截斷,出錯返回負值

int fdprintf(int fd, const char *format, ...);

// 變長引數列表變體
int vprintf(const char *format, va_list arg);
int vfprintf(FILE *stream, const char *format, va_list arg);
int vsprintf(char *s, const char *format, va_list arg);

轉換格式:

  • 普通字元:複製到輸出流

  • 轉換說明:控制引數的格式轉換

    • 開頭 %

    • 標誌[可選]

      • -​左對齊,預設為右對齊
      • +​顯示正負號
      • 空格 有符號值轉換
      • 0​ 寬度不足時填充 0
      • #​指定另一種輸出形式
    • 寬度數值[可選]:指定最小欄位寬度

    • 精度數值[可選]:點號開始,後接十進位制數值

    • 長度修飾符[可選]:指定引數的長度

      • h 按照short/unsigned short 輸出
      • l 按照long/unsigned long 輸出
      • L 按照long double 輸出
    • 結尾: 轉換字元, d, c, s, f, x等

int fscanf(FILE *stream, const char *format, ...);
int scanf(const char *format, ...);
int sscanf(const char *s, char *format, ...);

int fdscanf(int fd, const char *format,...);

// 變長引數列表變體 ...
  • 引數必須是指標
  • 到檔案尾或者出錯返回EOF,否則返回實際輸入的字元數

轉換格式

  • 空格或者製表符

  • 普通字元:匹配下一個輸入

  • 轉換說明

    • 開始標誌%​[可選]
    • 賦值遮蔽符號*​[可選]
    • 最大欄位寬度數值[可選]
    • 限定符 h\l\L​,指定引數的長度[可選]
    • 結束標誌:轉換字元 d,f,c, s, x等等

​#TODO#​4種使用場景P309

二進位制 IO

直接IO/二進位制IO,通常一次處理一個結構,能夠處理null位元組和換行符。

注意事項:只能用於同一系統,不同系統的偏移對齊以及儲存格式可能不同。因此網路通訊需要指定規範。

size_t fread(void *buffer, size_t size, size_t nobj, FILE *stream);

size_t fwrite(const void *buffer, size_t size, size_t nobj, FILE *stream);

/* 返回值是實際讀寫的元素的個數而非位元組數
fread:少於nobj, 出錯或者EOF,需要進一步分辨
fwrite:少於nobj,錯誤
*/

記憶體流

無關檔案,直接在緩衝區和主存之間進行位元組IO。非常適用於字串。

Linux支援。

FILE *fmemopen(void *buf, size_t size, const char *type); // buf=null, 讀寫無意義;對於null位元組的處理十分特殊

FILE *open_memstream(char **bufp, size_t sizep);   // 面向位元組的流

FILE *open_wmemstream(wchar_t **bufp, size_t *sizep);  // 面向寬位元組的流

檔案定位函式

  • 二進位制檔案:使用位元組偏移量,不一定支援 SEEK_END
  • 文字檔案:格式不同不能使用位元組,orgin=SEEK_SET, offset=0/ftell
int fseek(FILE *stream, long offset, int origin);
/*
- 二進位制檔案
  - origin
    - `SEEK_SET` 檔案開始處
    - `SEEK_CUR` 當前位置
    - `SEEK_END` 檔案結束處,可能不支援
- 文字檔案
  - `SEEK_SET`;offset是 0 或者 `ftell`返回值
  - `SEEK_CUR`/`SEEK_END`:offset只能是 0

注意事項
- 行末指示符將被清除
- 退回的字元將被丟棄
- 更新模式中的讀寫操作切換
*/

long ftell(FILE *stream);

void rewind(FILE *stream);    // 重置為起始位置
//等價於 `fseek(stream, 0L, SEEK_SET); clearerr(stream);`
// off_t, 大於32位 UNIX 標準
off_t ftello(FILE *fp);
int seeko(FILE *fp, off_t offset, int origin);
// fpos_t ISO C標準,更加通用
int fgetpos(FILE *stream, fpos_t *ptr);

int fsetpos(FILE *stream, const fpos_t *ptr);

錯誤處理函式

發生錯誤或者到達檔案尾時會設定狀態指示符

整型表示式 errno 包含錯誤編號,定義在 <errno.h>

  • 只有庫函式失敗時才會設定 errno, 成功執行並不會修改 errno
  • 任何函式都不會將常量置為0
/* ----- 流錯誤 -----*/
int feof(FILE *stream);     // 流設定了檔案結束指示符,返回非0值

int ferror(FILE *stream);   // 流設定了錯誤指示符,返回非0值

int clearerr(FILE *stream); // 清除流的所有指示符
#include <string.h>
char *strerror(int crrno);  // 對映錯誤資訊

#include  <stdio.h>
int perror(const char *s);  // 列印字串和 errno 錯誤資訊
// 類似於 fprintf(stderr, "%s: %s\n", s, "error essage");

標準IO的替代

標準IO 的效率不高,呼叫行IO需要進行兩次資料的複製

  • 快速IO fio: 使用指標而不是複製整行
  • sfio: 提高速度,同時推廣IO流
  • mmap

適用於嵌入式系統的更低記憶體要求的實現

  • uClibc C庫
  • Newlib C庫

參考

  • 《C程式設計語言》
  • 《UNIX 環境高階程式設計》

相關文章