2017年4月8日 星期六

[C語言] I/O 常見錯誤 : scanf() 篇

scanf() 常見問題


結論: 別再使用 scanf( ),請改用或是fgets( ) 或是 getline( ),再去作字串分割。麻煩,但是你清楚輸入字串裡面有什麼

上述說法可以參考: Scanf and loops

奇怪現象


在寫演算法作業優化的時候

發現一件事情

就是我在使用scanf() 來決定我要輸入幾筆測資

while loop 它竟然會 直接跳過 下面要求我輸入字串的指令

read = getline(&line, &len, stdin);

而直接輸出結果

然後輸出的字串是沒有顯示在螢幕上,但是字串長度是1,結果如下兩行所表示


line =
line length = 1

以下是範例程式  (2017/4/10 已修正亂碼錯誤)

//------------------- example code --------------------
#include <stdio.h>
#include <string.h>

int main(){ 
    int i = 0;
    int p;
    int dummy = 0;

    scanf("%d",&dummy);
    while( read != -1){
        //readline
        puts("enter a line");
        read = getline(&line, &len, stdin); 
        printf("line = %s", line);
        printf("line length = %zu\n", read); //%zd z coresponding to size_t type variable
        puts(line);

        if(i == dummy-1){ 
            printf("dummy = %d\n",dummy);
            break;
        }
        i++;
    }
    return 0;
}
//------------------- example code --------------------

後來在paslab的大家給予專業建議 外加 茫茫的stackoverflow的問題海中找到了答案

背後原因


Scanf skips every other while loop in C
When you read keyboard input with scanf(), the input is read after enter is pressed but the newline generated by the enter key which is not consumed by the call to scanf(). 
That means the next time you read from standard input there will be a newline waiting for you (which will make the next scanf() call return instantly with no data).

上面提到一件重要的事情,雖然 scanf("%d",&var1) 這行

會抓取標準輸入(standard input, 縮寫為stdin)的正整數來指定給變數 var1

如下面所說明的:
The standard I/O library provides a simple and efficient buffered stream I/O interface.
At program start-up, three streams shall be predefined and need not be opened explicitly: standard input (for reading conventional input), ...
--- STDIO(3) Linux Programmer's Manual

但是按下 Enter 鍵所產生的換行符號卻會留在緩衝區內(buffer)

所以 getline() 中負責抓取字元的 pointer

在緩衝區抓到的殘留字元就是按下 Enter鍵所輸入進的換行字元 

因此就產生換行字元直接餵給 getline(),執行時直接跳過的現象

這邊有鄉民驗證 ASCII code 的值為10,對應的按鍵不意外的就是換行的Enter鍵


還沒有概念? 可以看下面的圖解說明

圖解說明


如果用圖片來說明的話,以下圖片引用自 linux C标准库IO缓冲区--行缓冲实现 图解

如下圖一所示,當兩個指標 _IO_read_ptr  還沒與 _IO_read_end 重合時

表示緩衝區內還有內容可以讀取


圖一

後續的輸入會直接從緩存區裡面抓


圖二

之後兩者指向同一個位置的時候,才會停止自動抓取

解決方案


在不使用 getline()、fgets() 的情況下

要解決這個問題有兩個方法,但是概念一樣都是清空緩衝區

1. 在scanf() 後面加入一行 getchar() 把換行字元抓出來到其他地方

(注意,方法2 非正式規範!)

2.  用 fflush 清空輸入 input 的緩衝區( 即stdin的緩衝區)
 
一行可以處理  fflush(stdin);

補充知識


fflush 錯誤觀念澄清  : 正式規範是定義給  stdout 使用,不是 stdin 

fflush: flush a stream
In cases where a large amount of computation is done after printing part of a line on an output terminal, it is necessary to fflush(3) the standard output before going off and computing so that the output will appear.

觀念澄清1

 Q: How can I flush pending input so that a user's typeahead isn't read at the next prompt? Will fflush(stdin) work?

A: fflush is defined only for output streams

觀念澄清2

关于fflush(stdin)清空输入缓存流(C/C++) 新手必看!!
也许有人会说:“居然这样,那么在 scanf 函数后面加上‘fflush(stdin);’,把输入缓冲清空掉不就行了?” 然而这是错的! C和C++的标准里从来没有定义过 fflush(stdin)。 也许有人会说:“可是我用 fflush(stdin) 解决了这个问题,你怎么能说是错的呢?” 的确,某些编译器(如VC6)支持用 fflush(stdin) 来清空输入缓冲, 但是并非所有编译器都要支持这个功能(linux 下的 gcc 就不支持), 因为标准中根本没有定义 fflush(stdin)。

關於fflush的使用

Q: If fflush won't work, what can I use to flush input?


關於記憶體 - 緩衝區(buffer),資料是存在電腦的RAM (Random Access Memory) 中


關於換行符號

What is the newline character in the C language: \r or \n?

心得


從上次的演算法作業一學到一件事情,就是程式永遠都會按照目前編譯規範執行

如果發生預期以外的輸出結果,通常是因為疏忽使用規則而造成意外的結果

所以建議往後撰寫完時,務必一段一段的測試,並且有意外情況的時候,先看C使用手冊

The GNU C Reference Manual

再發問

---------------------------------------------
ps. 以上說明文件屬於 C89 規範

沒有留言:

張貼留言

/* 載入prettify的autoloader */ /* 載入JQuery */