2022年3月18日 星期五

要用幾個變數來接受 c scanf 的輸入

初學者在學 c 的鍵盤輸入時, 最困擾的通常是要宣告幾個變數來接受鍵盤的資料, 以 scanf 為例, 如果宣告一個 int input, 不就只能接受一個輸入嗎? 如果輸入兩筆資料時要怎麼辦? 30 筆的時候該怎麼辦? 好像沒辦法事先知道要宣告幾個變數來接收鍵盤輸入。

這時候先學習 c++ 的好處就來了, 不需要先寫可怕的 malloc 來處理這件事情, 用 std::vector 就可以搞定, 如果你不是用 c++ 而是學 c, 那就得先刻一個類似 vector 的資料結構, 通常這時候也應該會寫 list, 剛好派上用場。

初學的時候一般會用一個折衷的辦法, 先輸入一個數字, 例如 5, 代表之後要輸入 5 筆資料, 再用 malloc 5 個 int 大小的記憶體區間, 厄, 不太好是吧, 還不如一次就把這些觀念帶上, 先用 c++ vector, 再用 c 自己實做一個 vector/list, 這本來就是本科系無法迴避的課題。scanf 可沒想像的簡單。如果是我來教學, 我會這麼做, 讓初學者知道有這樣的資料結構, 等學習一段時間之後, 再去實做那種資料結構。

list 1 s.cpp
 1 #include <cstdio>
 2 #include <vector>
 3 
 4 using namespace std;
 5 
 6 int main(int argc, char *argv[])
 7 {
 8   vector<int> vals;
 9   int input=0;
10   int ret;
11 
12   while(1)
13   {
14     ret = scanf("%d", &input);
15     printf("ret: %d\n", ret);
16     printf("input: %d\n", input);
17     if (ret)
18     {
19       vals.push_back(input);
20     }
21     if (ret == EOF || ret == 0)
22     {
23       break;
24     }
25   }
26 
27   for (auto &i : vals)
28   {
29     printf("i: %d\n", i);
30   }
31   return 0;
32 }

我用 c++ 就偷懶一下了。scanf 對 c 初學者來說, 門檻真的太高了, 它比想像中的還要難。

另外一個類似的問題, 如果你要輸入一個字串, 要宣告 char str[10]; 確定這個夠嗎? 如果輸入的字串有 20 char 怎麼辦? POSIX.1-2008 親切的提供了 m 來處理這個問題, 參考 list 2 的用法, 然而在更早之前 GNU 提供了 a, 所以有點混亂, 更麻煩的是 a 現在提供給浮點數用, 而且成為標準, 但如果你用的是夠新的 c 編譯器, 應該有 m 可以用。windows/vs, mac/clang 請自行確認有沒有 m 可以用。請記得不用時要 free 這個 str, 另外傳給 scanf 時, 是傳指標的指標 &str(char **), 相信整死 c 初學者了。

list 2 s2.cpp
25   char *str;
26
27   ret = scanf("%ms", &str);
28
29   printf("str: %s\n", str);
30
31   free(str);

還沒完, 如果格式轉換出錯時怎麼辦, %d 結果輸入了 abc 呢? 這個真的整死我, 花了不少時間才搞定, 因為轉換失敗, 這時候 abc 還存在 stdin buffer 裡頭, 如果使用 list 1. L12 用 while 去抓, 在我的平台會一直無窮迴圈, 因為 abc 一直存在, scanf 會一直格式錯誤, return 0, errno 會得到 EILSEQ。

解法很直覺, 清掉 stdin buffer 就好了吧, 出動 fflush(stdin), 結果沒用, 再補上 clearerr(stdin), 也沒用, 靠, stdin buffer 清不掉阿, fflush 名過其實。後來找到 __fpurge(stdin) 終於清掉 stdin buffer。這時候 scanf 就又會等在那邊等著使用者按下鍵盤輸入了。

linux man page 提到: For input streams associated with seekable files (e.g., disk files, but not pipes or terminals), 也許 stdin 不算是 seekable files 吧! 所以 fflush(stdin) 無效。另外「C程式語言」B-4 提到 fflush 對於 input streams, 結果未定義 (undefined behavior)。

__fpurge(stdin) 不確定其他平台有沒有, 請自行查詢。另外一個有可攜性的作法就是把 stdin buffer 讀出來丟掉。

int c;
while ((c = getchar()) != '\n' && c != EOF);

我本來只想寫第一段那個而已, 沒想到後來補了這麼多, scanf 難阿!

ref:
  1. C語言——使用scanf函式時需要注意的問題
  2. I am not able to flush stdin

沒有留言:

張貼留言

使用 google 的 reCAPTCHA 驗證碼, 總算可以輕鬆留言了。

我實在受不了 spam 了, 又不想讓大家的眼睛花掉, 只好放棄匿名留言。這是沒辦法中的辦法了。留言的朋友需要有 google 帳號。