一文看懂 C 語言 I/O
- 2022 年 3 月 25 日
- 筆記
再會吧,這寶貴的片刻和短暫的時機限制了我在情義上的真摯表示,也不能容我們暢敘衷曲,這本來是親友重逢所應有的機緣;願上帝賜給我們美好的未來,好讓我們開懷暢談!再一次告別;勇敢作戰吧,祝你勝利!——威廉•莎士比亞《查理三世》
0 說在前面
當你看到這篇文章時,不妨回想一下你當初第一次用 C 語言「Hello World!」時是什麼樣的心情。那是你第一次成功使用神秘程式碼完成了和電腦的交流。儘管展示資訊的黑框框讓你可能不大習慣這樣一種溝通方式,但這難道不也有點電影里黑客那感覺了~?
不知不覺半年過去了,你為了數據結構作業絞盡腦汁,敲下最後一個分號,滑鼠輕點」編譯運行「。黑色高級框框跳出來,尷尬而不失禮貌地對你說:
--------------------------------
Process exited after 4.511 seconds with return value 3221225477
請按任意鍵繼續. . .
你質問:
」你怎麼了,為什麼要這樣對我……嗚嗚~「
是啊,你認識了這個框框那麼久,它早已熟悉你寫 bug 的習慣,而你卻摸不清它的性情。你是時候應該了解一下它了。
1 標準輸入輸出
」你好,我叫終端,也叫控制台,英文名是 Terminal,也叫 Console,很高興成為你的朋友。「
」你不記得我啦?我就是你每次運行程式的時候跳出來跟你聊天的那位。請看——「
「其實我並不是你的程式本體,你的程式躲在電腦裡面,是它派我來跟你說話的,」
「當你在鍵盤上敲敲的時候,我會幫你把你輸入的字元顯示出來,這樣你就知道你輸入的對不對了,」
「然後你一行輸完,按下回車,我幫你把整行字元串都傳給你的程式,你的程式就會對一行字元串進行解析,如果有 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」 是一樣的。
當然你可以像我一樣玩(用編輯器打開輸入文件分屏出去,調試的時候不用每次在控制台輸入,多是一件美事):
回過頭來我們看看這兩句是什麼意思:
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);
-
打開「此電腦」,在上面一欄找到「查看」按鈕,點進去,找到「文件後綴名」,看是否打勾,如果沒有請務必打上。 ↩︎