C 標準庫 IO 使用詳解
其實輸入與輸出對於不管什麼系統的設計都是異常重要的,比如設計 C 介面函式,首先要設計好輸入引數、輸出引數和返回值,接下來才能開始設計具體的實現過程。C 語言標準庫提供的介面功能很有限,不像Python 庫。不過想把它用好也不容易,本文總結 C 標準庫基礎 IO 的常見操作和一些特別需要注意的問題,如果你覺著自己還不是大神,那麼請相信我,讀完全文後你肯定會有不少收穫。
一、操作控制代碼
開啟檔案其實就是在作業系統中分配一些資源用於儲存該檔案的狀態資訊及檔案的標識,以後使用者程式可以用這個標識做各種讀寫操作,關閉檔案則釋放佔用的資源。
開啟檔案的函式:
#include <stdio.h> FILE *fopen(const char *path, const char *mode);
FILE 是 C 標準庫定義的結構體型別,其包含檔案在核心中的標識(檔案描述符)、I/O 緩衝區和當前讀寫位置資訊,呼叫者不需知道 FILE 的具體成員,由庫函式內部維護,呼叫者不應該直接訪問這些成員。像 FILE* 這樣的檔案指標稱為控制代碼(Handle)。
開啟檔案操作是對檔案資源進行操作的,所以有可能開啟檔案失敗,所以在開啟函式時一定要判斷返回值,如果失敗則返回錯誤資訊,以方便快速定位錯誤。
開啟檔案應該與關閉檔案成對存在,雖然程式在退出時會釋放相應的資源,但是對於一個長時間執行服務程式來說,經常開啟而不關閉檔案是會造成程序資源耗盡的,因為程序的檔案描述符個數是有限的,及時關閉檔案是個好習慣。
關閉檔案的函式:
#include <stdio.h> int fclose(FILE *fp);
fopen 函式引數 mode 總結:
- "r":只讀,檔案必須存在。
- "w":只寫,如果不存在則建立,存在則覆蓋。
- "a":追加,如果不存在則建立。
- "r+":允許讀和寫,檔案必須存在。
- "w+":允許讀和寫,檔案不存在則建立,存在則覆蓋。
- "a+":允許讀和追加,檔案不存在則建立。
二、關於stdin/stdout/stderr
在使用者程式啟動時,main 函式還沒開始執行之前,會自動開啟三個 FILE* 指標分別是:stdin、stdout、stderr,這三個檔案指標是 libc 中定義的全域性變數,在 stdio.h 中宣告,printf 向 stdout 寫,而 scanf 從 stdin 讀,使用者程式也可以直接使用這三個檔案指標。
- stdin 只用於讀操作,稱為標準輸入
- stdout 只用於寫操作,稱為標準輸出
- stderr 也用於寫操作,稱為標準錯誤輸出
通常程式的執行結果列印到標準輸出,而錯誤提示列印到標準錯誤輸出,一般標準輸出和標準錯誤都是螢幕。通常可以標準輸出重定向到一個常規檔案,而標準錯誤輸出仍然對應終端裝置,這樣就可以將執行結果與錯誤資訊分開。
三、以位元組為單位的IO函式
fgetc 函式從指定的檔案中讀一個位元組,getchar從標準輸入讀一個位元組,呼叫 getchar() 相當於 fgetc(stdin)
#include <stdio.h> int fgetc(FILE *stream); int getchar(void);
fputc 函式向指定的檔案寫入一個位元組,putchar 向標準輸出寫一個位元組,呼叫 putchar() 相當於呼叫 fputc(c, stdout)。
#include <stdio.h> int fputc(int c, FILE *stream); int putchar(int c);
引數和返回值型別為什麼使用 int 型別?可以看到這幾個函式的引數和返回值型別都是 int,而非 unsigned char 型。因為錯誤或讀到檔案末尾時將返回 EOF,即 -1,如果返回值是 unsigned char(0xff),與實際讀到位元組 0xff 無法區分,如果使用 int 就可以避免這個問題。
四、操作讀寫位置函式
當我們在操作檔案時,有一個叫「檔案指標」的傢伙來記錄當前操作的檔案位置,比如剛開啟檔案,呼叫了 1 次 fgetc 後,此時檔案指標指向了第 1 個位元組後邊,注意是以位元組為單位記錄的。
改變檔案指標位置的函式:
#include <stdio.h> int fseek(FILE *stream, long offset, int whence); whence:從何處開始移動,取值:SEEK_SET | SEEK_CUR | SEEK_END offset:移動偏移量,取值:可取正 | 負 void rewind(FILE *stream);
舉幾個簡單例子:
fseek(fp, 5, SEEK_SET);// 從檔案頭向後移動5個位元組 fseek(fp, 6, SEEK_CUR);// 從當前位置向後移動6個位元組 fseek(fp, -3, SEEK_END);// 從檔案尾向前移動3個位元組
offset 可正可負,負值表示向檔案開頭的方向移動,正值表示向檔案尾方向移動,如果向前移動的位元組數超過檔案開頭則出錯返回,如果向後移動的位元組數超過了檔案末尾,再次寫入會增加檔案尺寸,檔案空洞位元組都是 0
$ echo "5678" > file.txt fp = fopen("file.txt", "r+"); fseek(fp, 10, SEEK_SET); fputc('K', fp) fclose(fp) // 通過結果可以看出字母K是從第10個位置開始寫的 liwei:/tmp$ od -tx1 -tc -Ax file.txt 0000000353637380a00000000004b 5678\n\0\0\0\0\0K
rewind(fp) 等價於 fseek(fp, 0, SEEK_SET)
ftell(fp) 函式比較簡單,直接返回當前檔案指標在檔案中的位置
// 實現計算檔案位元組數的功能 fseek(fp, 0, SEEK_END); ftell(fp);
五、以字串為單位的IO函式
fgets 從指定的檔案中讀一行字元到呼叫者提供的緩衝區,讀入內容不超過 size 。
char *fgets(char *s, int size, FILE *stream); char *gets(char *s);
首先要說明 gets() 函式強烈不推薦使用,類似 strcpy 函式,使用者不可以指定緩衝區大小,很容易造成緩衝區溢位錯誤。不過 strcpy 程式設計師還是可以避免,而 gets 的輸入使用者可以提供任意長的字串,唯一避免方法就是不使用 gets,而使用 fgets(buf, size, stdin)
fgets 函式從 stream 所指檔案讀取以 '\n' 結尾的一行,包括 '\n' 在內,存到緩衝區中,並在該行結尾新增一個 '\0' 組成完整的字串。如果檔案一行太長,fgets 從檔案中讀了 size-1 個字元還沒有讀到 '\n',就把已經讀到的 size-1 個字元和一個 '\0' 字元存入緩衝區,檔案行剩餘的內容可以在下次呼叫 fgets 時繼續讀。
若一次 fgets 呼叫在讀入若干字元後到達檔案末尾,則將已讀到的字元加上 '\0' 存入緩衝區並返回,如果再次呼叫則返回 NULL,可以據此判斷是否讀到檔案末尾。
fputs 向指定檔案寫入一個字串,緩衝區儲存的是以 '\0' 結尾的字串,與 fgets 不同的是,fputs 不關心字串中的 '\n' 字元。
int fputs(const char *s, FILE *stream); int puts(const char *s);
六、以記錄為單位的IO函式
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
fread 和 fwrite 用於讀寫記錄,這裡的記錄是指一串固定長度的位元組,比如一個 int、一個結構體貨或一個定長陣列。
引數 size 指出一條記錄的長度,nmemb 指出要讀或寫多少條記錄,這些記錄在 ptr 所指記憶體空間連續存放,共佔 size * nmemb 個位元組。
fread 和 fwrite 返回的記錄數有可能小於 nmemb 指定的記錄數。例如當讀寫位置距檔案末尾只有一條記錄長度,呼叫 fread 指定 nmemb 為 2,則返回值為 1。如果寫檔案時出錯,則 fwrite 的返回值小於 nmemb 指定的值。
struct t{ inta; short b; }; struct t val = {1, 2}; FILE *fp = fopen("file.txt", "w"); fwrite(&val, sizeof(val), 1, fp); fclose(fp); liwei:/tmp$ od -tx1 -tc -Ax file.txt 00000000100000002000000 001\0\0\0 002\0\0\0
從結果可以看出,寫入的是 8 個位元組,有興趣的同學可以就此分析下系統的「大小端」和結構體的「對齊補齊」問題。
七、格式化IO函式
(1). printf / scanf
int printf(const char *format, ...); int scanf(const char *format, ...);
這兩個函式是我們學習 C 語言最早接觸,可能也是接觸比較多的了,沒什麼特別要說的。printf 就是格式化列印到標準輸出。下面總結下 printf 常用的方式。
printf("%d\n", 5);// 列印整數 5 printf("-%10s-\n", "hello")// 設定顯示寬度並左對齊:-hello- printf("-%-10s-\n", "hello")// 設定顯示寬度並右對齊:-hello- printf("%#x\n", 0xff);// 0xff 不加#則顯示ff printf("%p\n", main);// 列印 main 函式首地址 printf("%%\n");// 列印一個 %
scanf 就是從標準輸入中讀取格式化資料,簡單舉個例子:
int year, month, day; scanf("%d/%d/%d", &year, &month, &day); printf("year = %d, month = %d, day = %d\n", year, month, day);
(2). sprintf / sscanf / snprintf
sprintf 並不列印到檔案,而是列印到使用者提供的緩衝區中並在末尾加 '\0',由於格式化後的字串長度很難預計,所以很可能造成緩衝區溢位,強烈推薦 snprintf 更好一些,引數 size 指定了緩衝區長度,如果格式化後的字串超過緩衝區長度,snprintf 就把字串截斷到 size - 1 位元組,再加上一個 '\0',保證字串以 '\0' 結尾。如果發生截斷,返回值是截斷之前的長度,通過對比返回值與緩衝區實際長度對比就知道是否發生截斷。
int sscanf(const char *str, const char *format, ...); int sprintf(char *str, const char *format, ...); int snprintf(char *str, size_t size, const char *format, ...);
sscanf 是從輸入字串中按照指定的格式去讀取相應的資料,函式功能非常的強大,支援類似正則表示式匹配的功能。具體的使用格式請自行查詢官方手冊,這裡總結出最常用、最重要的幾種使用場景和方式。
- 最基本的用法
char buf[1024] = 0; sscanf("123456", "%s", buf); printf("%s\n", buf); // 結果為:123456
- 取指定長度的字串
sscanf("123456", "%4s", buf); printf("%s\n", buf); // 結果為:1234
- 取第1個字串
sscanf("hello world", "%s", buf); printf("%s\n", buf); // 結果為:hello因為預設是以空格來分割字串的,%s讀取第一個字串hello
- 讀取到指定字元為止的字串
sscanf("123456#abcdef", "%[^#]", buf); // 結果為:123456 // %[^#]表示讀取到#符號停止,不包括#
- 讀取僅包含指定字符集的字串
sscanf("123456abcdefBCDEF", "%[1-9a-z]", buf); // 結果為:123456abcdef // 表示式是要匹配數字和小寫字母,匹配到大寫字母就停止匹配了。
- 讀取指定字符集為止的字串
sscanf("123456abcdefBCDEF", "%[^A-Z]", buf); // 結果為:123456abcdef
- 讀取兩個符號之間的內容(@和.之間的內容)
sscanf("[email protected]", "%*[^@]@%[^.]", buf); // 結果為:linuxblogs // 先讀取@符號前邊內容並丟棄,然後讀@,接著讀取.符號之前的內容linuxblogs,不包含字元.
- 給一個字串
sscanf("hello, world", "%*s%s", buf); // 結果為:world // 先忽略一個字串"hello,",遇到空格直接跳過,匹配%s,儲存 world 到 buf // %*s 表示第 1 個匹配到的被過濾掉,即跳過"hello,",如果沒有空格,則結果為 NULL
- 稍微複雜點的
sscanf("ABCabcAB=", "%*[A-Z]%*[a-z]%[^a-z=]", buf); // 結果為:AB自己嘗試分析哈
- 包含特殊字元處理
sscanf("201*1b_-cdZA&", "%[0-9|_|--|a-z|A-Z|&|*]", buf); // 結果為:201*1b_-cdZA&
如果能將上述幾個例子搞明白,相信基本上已經掌握了 sscanf 的用法,實踐才是檢驗真理的唯一標準,只有多使用,多思考才能真正理解它的用法。
(3). fprintf / fscanf
fprintf 列印到指定的檔案 stream 中,fscanf 從檔案中格式化讀取資料,類似 scanf 函式。相關函式的宣告如下:
int fprintf(FILE *stream, const char *format, ...); int fscanf(FILE *stream, const char *format, ...);
還是通過簡單例項來說明基本用法。
FILE *fp = fopen("file.txt", "w"); fprintf(fp, "%d-%s-%f\n", 32, "hello", 0.12); fclose(fp); liwei:/tmp$ cat file.txt 32-hello-0.120000
而 fscanf 函式的使用基本上與 sscanf 函式使用方式相同。
八、IO緩衝區
還有個關於 IO 非常重要的概念,就是 IO 緩衝區。
C 標準庫為每個開啟的檔案分配一個 I/O 緩衝區,使用者呼叫讀寫函式大多數都在 I/O 緩衝區中讀寫,只有少數請求傳遞給核心。
以 fgetc/fputc 為例,當第一次呼叫 fgetc 讀一個位元組時,fgetc 函式可能通過系統呼叫進入核心讀 1k 位元組到緩衝區,然後返回緩衝區中第一個位元組給使用者,以後使用者再呼叫 fgetc,就直接從緩衝區讀取。
另一方面,fputc 通常只是寫到緩衝區中,如果緩衝區滿了,fputc 就通過系統呼叫把緩衝區資料傳遞給核心,核心將資料寫回磁碟。如果希望把緩衝區資料立即寫入磁碟,可以呼叫 fflush 函式。
C 標準庫 IO 緩衝區有三種類型:全緩衝、行緩衝和無緩衝區,不同型別的緩衝區具有不同的特性。
全緩衝 行緩衝 無緩衝
printf("hello world"); while(1); // 執行程式會發現螢幕並沒有列印hello world // 因為緩衝區沒滿,且沒有\n符號
除了寫滿緩衝區、寫入換行符之外,行緩衝還有一種情況會自動做 flush 操作,如果:
- 使用者程式呼叫庫函式從無緩衝的檔案中讀取
- 或從行緩衝的檔案中讀取,且這次讀操作會引發系統呼叫從核心讀取資料,那麼會讀之前自動 flush 所有行緩衝
- 程式退出時通常也會自動 flush 緩衝區
如果不想完全依賴自動的 flush 操作,可以呼叫 fflush 函式手動操作。若呼叫 fflush(NULL) 可以對所有開啟檔案的 IO 緩衝區做 flush 操作。緩衝區大小也可以自定義設定,一般情況無需設定,預設即可。
Linux公社的RSS地址 :https://www.linuxidc.com/rssFeed.aspx
本文永久更新連結地址:https://www.linuxidc.com/Linux/2019-02/157127.htm