一文看懂 C 語言 I/O

Only(AR)發表於2022-03-25

再會吧,這寶貴的片刻和短暫的時機限制了我在情義上的真摯表示,也不能容我們暢敘衷曲,這本來是親友重逢所應有的機緣;願上帝賜給我們美好的未來,好讓我們開懷暢談!再一次告別;勇敢作戰吧,祝你勝利!——威廉•莎士比亞《查理三世》

0 說在前面

當你看到這篇文章時,不妨回想一下你當初第一次用 C 語言“Hello World!”時是什麼樣的心情。那是你第一次成功使用神祕程式碼完成了和計算機的交流。儘管展示資訊的黑框框讓你可能不大習慣這樣一種溝通方式,但這難道不也有點電影裡黑客那感覺了~?

不知不覺半年過去了,你為了資料結構作業絞盡腦汁,敲下最後一個分號,滑鼠輕點”編譯執行“。黑色高階框框跳出來,尷尬而不失禮貌地對你說:


--------------------------------
Process exited after 4.511 seconds with return value 3221225477
請按任意鍵繼續. . .

你質問:

“你怎麼了,為什麼要這樣對我……嗚嗚~”

是啊,你認識了這個框框那麼久,它早已熟悉你寫 bug 的習慣,而你卻摸不清它的性情。你是時候應該瞭解一下它了。

1 標準輸入輸出

“你好,我叫終端,也叫控制檯,英文名是 Terminal,也叫 Console,很高興成為你的朋友。”

“你不記得我啦?我就是你每次執行程式的時候跳出來跟你聊天的那位。請看——”

一文看懂 C 語言 I/O

“其實我並不是你的程式本體,你的程式躲在電腦裡面,是它派我來跟你說話的,”

“當你在鍵盤上敲敲的時候,我會幫你把你輸入的字元顯示出來,這樣你就知道你輸入的對不對了,”

“然後你一行輸完,按下回車,我幫你把整行字串都傳給你的程式,你的程式就會對一行字串進行解析,如果有 scanf 函式的話還會逐個解析出裡面的數字、字元等等,”

“當你的程式算完之後,會把輸出的資訊告訴我,我來顯示到螢幕上。”


所謂 I/O,就是 Input/Output,即輸入輸出。通過終端讀入和顯示的就是“標準輸入輸出”,由於終端也是從鍵盤獲取資訊,並把資訊顯示在螢幕上。所以:

  • 標準輸入也叫鍵盤輸入
  • 標準輸出也叫螢幕輸出

標準輸入輸出的英文是 Standard Input and Output,縮寫就是“stdio”,覺不覺得眼熟hhh~


——“你用的 scanf、gets、getchar 函式都是解析標準輸入的,printf、puts、putchar 都是標準輸出。現在知道我是幹什麼的了吧”

——“哦,原來是這樣。但是你好醜。”

——“???那我走”

2 輸出輸出重定向

終端走了——你萬念俱灰,把你的程式碼提交給希冀的評測姬。她說:

得分0.00   最後一次提交時間:2022-03-25 19:29:50

共有測試資料:5
平均佔用記憶體:1.401K    平均CPU時間:0.00578S    平均牆鍾時間:0.00576S

測試資料	評判結果
測試資料1	執行錯誤
測試資料2	執行錯誤
測試資料3	執行錯誤
測試資料4	執行錯誤
測試資料5	執行錯誤

——“求求你在本地測好再交給我 OK?我每天判那麼多程式碼很累了啦!”

——“emm……我好奇你怎麼知道我們的程式碼對不對的,也是用終端嗎?”

——“終端?那不是低階的 PC 才會用的東西?我們伺服器不需要這個。I/O 重定向一下就行了”


現在你可以試試這樣一個操作,寫好一份 C 語言程式碼,裡面有標準輸入輸出函式,然後新增兩行這樣的語句:

#include <stdio.h>
// 一些額外的標頭檔案和巨集定義
int main() {
    freopen("a.in", "r", stdin);
    freopen("a.out", "w", stdout);   // 額外新增這兩句 :)
    ...
    return 0;
}

接下來在你的 C 程式的同一資料夾下新建文字文件,命名為 “a.in”(注意這一步之前要確保你的電腦顯示了檔案字尾[1])。然後在 “a.in” 裡面寫上你要輸入的資料,Ctrl+S 儲存。

編譯執行你的程式碼,你會發現程式直接結束,黑框框沒有其它輸出了。

然後你在程式碼所在的資料夾裡發現了一個名為 “a.out” 的文字文件,裡面正是你要的答案。

當然 “a.in” 和 “a.out” 可以改成你喜歡的任何名字,文字文件對字尾不敏感,跟 “.txt” 是一樣的。

你也可以像我一樣玩(用編輯器開啟輸入檔案分屏出去,除錯的時候不用每次在控制檯輸入,多是一件美事):

一文看懂 C 語言 I/O

回過頭來我們看看這兩句是什麼意思:

freopen("a.in", "r", stdin);
freopen("a.out", "w", stdout);
/*
 * freopen: f    表示 “file (檔案)”
 *          re   表示 “重新”
 *          open 表示 “開啟”
 *
 * "a.in" / "a.out" 表示重定向的檔名
 * "r" / "w" 表示檔案的開啟模式:"r" 意味著“讀”,"w" 意味著寫
 * stdin / stdout 表示被替換的 I/O 方式
 *                分別是標準輸入(standard input)和標準輸出(standart output)
 */

翻譯成人話就是:

  • 我要用“只讀方式”開啟檔案 “a.in”,並用其替換標準輸入
  • 我要以“只寫方式”開啟檔案 “a.out”,並用其替換標準輸出
    • 順便說一句,只寫模式 “w” 下,如果找不到檔案,程式會幫你建立一個~

評測姬說:

“現在你懂了?在我拿到你的程式時,會自動幫你加上 freopen 將標準 I/O 重定向為檔案 I/O,再在我的 CPU 裡跑程式,跑完再對比一下你的輸出和標準答案一不一樣就行了。”

3 檔案 I/O

此時晏老師:“多出點檔案 I/O 的題,難死這幫小崽子~”

理論上來說,你會 I/O 重定向之後就可以做所有檔案 I/O 的題了,大不了都用 scanf 和 gets 唄。

但是有時候讓你既從標準輸入讀入又從檔案讀入~檔案 I/O 也不能不會是吧。

廢話了這麼多,終於可以講講你們不大清楚的 I/O 函式的用法了:

3.1 檔案指標

要熟練使用檔案 I/O,要過的第一關就是檔案指標,它相當於給你的檔案貼一個標籤,讓後當你需要呼叫函式的時候要把檔案指標作為輸入變數傳進去,這樣才能對你的檔案進行操作:

FILE * file_in = fopen("in.txt", "r");
FILE * file_out = fopen("out.txt", "w");
/*
 * FILE * 是一個變數型別,代表檔案的指標
 *
 * 後面的 file_in 和 file_out 是你自己起的變數名
 *
 * fopen("...", "r/w"); 是開啟檔案的函式,前面的檔名,後面是開啟模式讀或寫
 *                      表示將一個檔案以某種方式開啟,返回該檔案的指標
 *
 * 以後你就可以把 file_in 或 file_out 傳進其它函式裡了
 */

3.2 I/O 函式

3.2.1 輸入函式:

scanf("...", ...);
fscanf(file, "...", ...); // file 是前面用 "r" 模式開啟的檔案指標

在以下兩個條件下,這兩個函式是我最推薦大家使用的。

  • 需要從輸入中獲取數字(直接 %d 或 %lf)
  • 需要逐詞對字串處理(不含空格)

如果題不是要求類似於“讀入若干行,行內有空格,對每行輸出一個balabala……”這種,真心不建議用 gets 和fgets。因為 gets 很可能會產生莫名其妙的 bug(我曾解釋過),fgets 不好記也不好用。

所以比如“單詞統計”等等這類題,只要不怕空格,還請選擇 scanf/fscanf


printf("...", ...);
fprintf(file, "...", ...);

這倆大家應該挺熟了,後面那個 fprintf 就是把輸出目標換成 “w” 模式的檔案指標就行了。

介紹兩個新朋友:

len = fread(str, sizeof(str[0]), MAX, file);
/*
 * 這個函式的作用是從 "r" 模式的 file 檔案裡把整個檔案一股腦讀到 str 裡
 * 
 * str  是要接受的字串,儘量開大點,一定要初始化為全 0,這個函式不保證在字串末尾補 '\0'
 * sizeof(str[0]) 實際上就是一個字元的大小,表示讀的單位大小
 * MAX  讀的最大長度,儘量跟 str[] 的容量一樣大,要大於所給資料範圍
 *      如果讀到檔案末尾還不到 MAX 則返回 str 的長度
 *      如果讀到 MAX 則返回 MAX
 * file 檔案指標
 */

fwrite(str, sizeof(str[0]), len, file);
/*
 * 這個函式的作用是把 str 一股腦寫進 "w" 模式的 file 裡
 * 
 * str  是要寫的字串
 * sizeof(str[0]) 解釋同上
 * len  是想寫的長度,也就是 str 的長度
 * file 想寫的檔案指標
 */

檔案加密一題中,需要讀取一整段文字,這種情況下,用這兩個函式是最好的選擇。


剩下的不太常用我大概說一下。

gets(str);				// 讀到換行就停止,讀進來的字串不含換行,可能引起神祕 bug
fgets(str, MAX, file);	/* 讀到換行 / 檔案末尾 / 超過 MAX - 1 時停止讀入
						 * 特性:str 中保留讀到的換行並自動在末尾新增 '\0' */

上面這倆函式如果想用的話還是建議好好研究一下特性小心一點使用,挺容易出 bug 的。

ch = getchar();			// 從標準輸入讀入單個字元,毒瘤,別用
						// 建議想用的時候用 scanf("%s", str); 讀字串來避免 bug

ch = fgetc(file)		// 從檔案中讀單個字元,注意的一點是:
    					// 請把 ch 定義成 int 型別,因為它讀到檔案末尾會返回 EOF
    					// 而 char 型別不能儲存 -1 導致無法識別檔案末尾
putchar(ch);			// 向標準輸出寫一個字元,等同於 printf("%c", ch);
fputc(ch, file);		// 向檔案寫一個 ch,等同於 fprintf(file, "%c", ch);


  1. 開啟“此電腦”,在上面一欄找到“檢視”按鈕,點進去,找到“檔案字尾名”,看是否打勾,如果沒有請務必打上。 ↩︎

相關文章