笨辦法學C 練習24:輸入輸出和檔案
練習24:輸入輸出和檔案
譯者:飛龍
你已經學會了使用printf
來列印變數,這非常不錯,但是還需要學習更多。這個練習中你會用到fscanf
和fgets
在結構體重構建關於一個人的資訊。在這個關於讀取輸入的簡介之後,你會得到C語言IO函式的完整列表。其中一些你已經見過並且使用過了,所以這個練習也是一個記憶練習。
#include <stdio.h>
#include "dbg.h"
#define MAX_DATA 100
typedef enum EyeColor {
BLUE_EYES, GREEN_EYES, BROWN_EYES,
BLACK_EYES, OTHER_EYES
} EyeColor;
const char *EYE_COLOR_NAMES[] = {
"Blue", "Green", "Brown", "Black", "Other"
};
typedef struct Person {
int age;
char first_name[MAX_DATA];
char last_name[MAX_DATA];
EyeColor eyes;
float income;
} Person;
int main(int argc, char *argv[])
{
Person you = {.age = 0};
int i = 0;
char *in = NULL;
printf("What's your First Name? ");
in = fgets(you.first_name, MAX_DATA-1, stdin);
check(in != NULL, "Failed to read first name.");
printf("What's your Last Name? ");
in = fgets(you.last_name, MAX_DATA-1, stdin);
check(in != NULL, "Failed to read last name.");
printf("How old are you? ");
int rc = fscanf(stdin, "%d", &you.age);
check(rc > 0, "You have to enter a number.");
printf("What color are your eyes:\n");
for(i = 0; i <= OTHER_EYES; i++) {
printf("%d) %s\n", i+1, EYE_COLOR_NAMES[i]);
}
printf("> ");
int eyes = -1;
rc = fscanf(stdin, "%d", &eyes);
check(rc > 0, "You have to enter a number.");
you.eyes = eyes - 1;
check(you.eyes <= OTHER_EYES && you.eyes >= 0, "Do it right, that's not an option.");
printf("How much do you make an hour? ");
rc = fscanf(stdin, "%f", &you.income);
check(rc > 0, "Enter a floating point number.");
printf("----- RESULTS -----\n");
printf("First Name: %s", you.first_name);
printf("Last Name: %s", you.last_name);
printf("Age: %d\n", you.age);
printf("Eyes: %s\n", EYE_COLOR_NAMES[you.eyes]);
printf("Income: %f\n", you.income);
return 0;
error:
return -1;
}
這個程式非常簡單,並且引入了叫做fscanf
的函式,意思是“檔案的格式化輸入”。scanf
家族的函式是printf
的反轉版本。printf
用於以某種格式列印資料,然而scanf
以某種格式讀取(或者掃描)輸入。
檔案開頭沒有什麼新的東西,所以下面只列出main
所做的事情:
ex24.c:24-28
建立所需的變數。
ex24.c:30-32
使用fgets
函式獲取名字,它從輸入讀取字串(這個例子中是stdin
),但是確保它不會造成緩衝區溢位。
ex24.c:34-36
對you.last_name
執行相同操作,同樣使用了fgets
。
ex24.c:38-39
使用fscanf
來從stdin
讀取整數,並且將其放到you.age
中。你可以看到,其中使用了和printf
相同格式的格式化字串。你也應該看到傳入了you.age
的地址,便於fsnaf
獲得它的指標來修改它。這是一個很好的例子,解釋了使用指向資料的指標作為“輸出引數”。
ex24.c:41-45
列印出用於眼睛顏色的所有可選項,並且帶有EyeColor
列舉所匹配的數值。
ex24.c:47-50
再次使用了fscanf
,從you.eyes
中獲取數值,但是保證了輸入是有效的。這非常重要,因為使用者可以輸入一個超出EYE_COLOR_NAMES
陣列範圍的值,並且會導致段錯誤。
ex24.c:52-53
獲取you.income
的值。
ex24.c:55-61
將所有資料列印出來,便於你看到它們是否正確。要注意EYE_COLOR_NAMES
用於列印EyeColor
列舉值實際上的名字。
你會看到什麼
當你執行這個程式時,你應該看到你的輸入被適當地轉換。你應該嘗試給它非預期的輸入,看看程式是怎麼預防它的。
$ make ex24
cc -Wall -g -DNDEBUG ex24.c -o ex24
$ ./ex24
What's your First Name? Zed
What's your Last Name? Shaw
How old are you? 37
What color are your eyes:
1) Blue
2) Green
3) Brown
4) Black
5) Other
> 1
How much do you make an hour? 1.2345
----- RESULTS -----
First Name: Zed
Last Name: Shaw
Age: 37
Eyes: Blue
Income: 1.234500
如何使它崩潰
這個程式非常不錯,但是這個練習中真正重要的部分是,scanf
如何發生錯誤。對於簡單的數值轉換沒有問題,但是對於字串會出現問題,因為scanf
在你讀取之前並不知道緩衝區有多大。類似於gets
的函式(並不是fgets
,不帶f
的版本)也有一個我們已經避免的問題。它並不是道輸入緩衝區有多大,並且可能會使你的程式崩潰。
要演示fscanf
和字串的這一問題,需要修改使用fgets
的那一行,使它變成fscanf(stdin, "%50s", you.first_name)
,並且城市再次執行。你會注意到,它讀取了過多的內容,並且吃掉了你的Enter鍵。這並不是你期望它所做的,你應該使用fgets
而不是去解決古怪的scanf
問題。
接下來,將fgets
改為gets
,接著使用valgrind
來執行valgrind ./ex24 < /dev/urandom
,往你的程式中輸入一些垃圾字串。這叫做對你的程式進行“模糊測試”,它是一種不錯的方法來發現輸入錯誤。這個例子中,你需要從/dev/urandom
檔案來輸入一些垃圾,並且觀察它如何崩潰。在一些平臺上你需要執行數次,或者修改MAX_DATA
來使其變小。
gets
函式非常糟糕,以至於一些平臺在程式執行時會警告你使用了gets
。你應該永遠避免使用這個函式。
最後,找到you.eyes
輸入的地方,並移除對其是否在正確範圍內的檢查。然後,為它輸入一個錯誤的數值,比如-1或者1000。在Valgrind
執行這些操作,來觀察會發生什麼。
譯者注:根據最新的C11標準,對於輸入函式,你應該總是使用
_s
字尾的安全版本。對於向字串的輸出函式,應該總是使用C99中新增的帶n
的版本,例如snprintf
。如果你的編譯器支援新版本,就不應該使用舊版本的不安全函式。
IO函式
這是一個各種IO函式的簡單列表。你應該查詢每個函式併為其建立速記卡,包含函式名稱,功能和它的任何變體。
fscanf
fgets
fopen
freopen
fdopen
fclose
fcloseall
fgetpos
fseek
ftell
rewind
fprintf
fwrite
fread
過一遍這些函式,並且記住它們的不同變體和它們的功能。例如,對於fscanf
的卡片,上面應該有scanf
、sscanf
、vscanf
,以及其它。並且在背面寫下每個函式所做的事情。
最後,為了獲得這些卡片所需的資訊,使用man
來閱讀它的幫助。例如,fscanf
幫助頁由man fscanf
得到。
附加題
- 將這個程式重寫為不需要
fscanf
的版本。你需要使用類似於atoi
的函式來將輸入的字串轉換為數值。 - 修改這個程式,使用
scanf
來代替fscanf
,並觀察有什麼不同。 - 修改程式,是輸入的名字不包含任何換行符和空白字元。
- 使用
scanf
編寫函式,按照檔名讀取檔案內容,每次讀取單個字元,但是不要越過(檔案和緩衝區的)末尾。使這個函式接受字串大小來更加通用,並且確保無論什麼情況下字串都以'\0'
結尾。
相關文章
- 笨辦法學C 練習42:棧和佇列佇列
- 笨辦法學C 練習29:庫和連結
- 笨辦法學C 練習8:大小和陣列陣列
- C輸入輸出與檔案
- 笨辦法學C 練習28:Makefile 進階
- 笨辦法學C 練習13:Switch語句
- 笨辦法學C 練習25:變參函式函式
- 笨辦法學C 練習34:動態陣列陣列
- 笨辦法學C 練習18:函式指標函式指標
- 笨辦法學C 練習36:更安全的字串字串
- C++ 學習筆記之——輸入和輸出C++筆記
- C語言檔案輸入和輸出操作的學習心得(一)C語言
- C++中的檔案輸入/輸出(3):掌握輸入/輸出流 (轉)C++
- 笨辦法學C 練習38:雜湊演算法演算法
- 笨辦法學C 練習1:啟用編譯器編譯
- 笨辦法學C 練習17:堆和棧的記憶體分配記憶體
- 【C++】標準檔案的輸入輸出!!!C++
- 分治法求眾數和重數(含檔案輸入輸出)
- 排序,檔案輸入輸出排序
- 檔案操作-輸入輸出
- 笨辦法學C 練習2:用Make來代替PythonPython
- 笨辦法學C 練習7:更多變數和一些算術變數
- 瞭解下C# 檔案的輸入與輸出C#
- Solidity語言學習筆記————24、輸入輸出引數Solid筆記
- C++中的檔案輸入/輸出(2):讀取檔案 (轉)C++
- 輸入和輸出基礎語法
- C++中的檔案輸入/輸出(4):檢測輸入/輸出的狀態標誌 (轉)C++
- 笨辦法學 Python · 續 練習 39:SQL 建立PythonSQL
- java_檔案輸入與輸出Java
- 01_Numpy學習筆記(下):輸入和輸出筆記
- 【C++】輸入輸出C++
- python:檔案的輸入與輸出Python
- C++ 中輸入輸出流及檔案流操作筆記C++筆記
- Java I/O系統學習系列二:輸入和輸出Java
- 笨辦法學C 練習45:一個簡單的TCP/IP客戶端TCP客戶端
- 第10章 對檔案的輸入輸出
- C語言輸入輸出C語言
- C語言之輸入輸出C語言