關於C/C++ stdin緩衝區以及對字符輸入的一些經驗和心得

關於C/C++ stdin緩衝區以及對字符輸入的一些經驗和心得

  在使用C/C++編寫控制檯應用或acm競賽的時候,I/O方式無非是標準輸入輸出,特別是acm競賽,就本人來說,由C語言入門,輸入方式還只會scanf,自從學了C++,便深深地被 cin/cout輸入輸出流的簡潔用法所吸引,相信有這種感覺的不止我一個人。
  所以很長一段時間,日常的訓練和各種線上比賽,再也沒有使用過scanf,反手一個cin感覺很炫酷。然而好景不長,一次bestcoder的常規線上賽,前期發揮穩定,手感相當好,1001和1002快而準地ac,1003也很快來了思路(兩年前的事情了,細節什麼的早忘了),咔咔咔敲完代碼提交ac,最後剩下充足的時間攻克1004,雖然到結束也沒做出來,但是3題鐵定漲分啊……
  然而終判的結果讓我大喫一鯨,T!L!E!,居然超時,百思不得其解之時(其實之前知道cin效率低,但是用着太順手了就沒在意),想到了會不會是數據太多?然後等着終判結束題目開放,抱着試一試的心態,把cin改成了scanf,然後……居然……秒過……
  然後網上查閱資料(只說輸入,輸出大同小異),cin慢的原因很多,其中很重要的一點是爲了使cinscanf可以兼容混合使用,cin在內部實現的時候會同步輸入緩衝區,也就是說,輸入流會時刻與輸入緩衝保持同步,這是一個很耗時的操作,所以就導致了在大量輸入數據的時候,cin會比scanf慢很多,可以說,這個慢,是數量級上的差異。
  如果你可以保證程序中不會出現標準輸入與流輸入混用的情況,可以在程序開始時使用ios::sync_with_stdio(false);關閉同步來提高速度,但是在大量數據面前輸入速度仍顯得乏力,相比scanf還是慢了一些(上面說的1003題我用cin關同步還是超時,只有scanf能過),個人認爲原因在於對輸入流對象的封裝和>>這個符號的運算符重載導致執行時間變慢。
  所以從那以後,在acm生涯裏再也沒有使用過cin……硬生生地改回了scanf的習慣。簡潔和效率總要捨棄一個,對於算法競賽來說,效率纔是關鍵吧……


  說了這麼多cin與scanf的速度比較,接下來重點說一下scanf,用法不必多說,結合多年來競賽經驗,介紹一下格式符%c與其他格式符的區別和特定使用場景的注意事項。

  %c是一個很奇葩的設定,單獨讀入一個字符,包括不可見的控制字符(換行等),而其他格式化符號(如%d %lld %f %lf %s等)會在讀入未完成時將換行符、空格、製表符等空白字符統統捨棄忽略,直到讀到了足夠的數據或遇到文件結尾才結束。我們平時控制檯輸入時通常按行輸入,也就是輸入數據後要敲擊回車才能被讀取,這樣就導致了換行符在%c與其他格式符號並存的程序中出現各種問題,例如無法獲得理想輸入數據,字符串錯誤錯誤導致程序崩潰等。

例如下面這段程序片段:

int a,b;
char c;
scanf("%d %d", &a, &b);
printf("a=%d b=%d\n", a, b);
scanf("%c", &c);
printf("char=%c\n", c);

理想狀態下,我們輸入以下數據:

11 66
a

  輸出結果應該與輸入一致,也就是說輸入11 66後敲下回車緊接着就會出現a=11 b=66,然後再輸入a敲回車會出現char=a。但事實上輸入11 66敲回車得到a=11 b=66以後,並不會再等待輸入字符,而是在出現char=一個空行之後,直接結束,如下圖:

運行結果

  爲什麼會出現這種情況?我們先來想兩個問題,爲什麼程序在遇到scanf等輸入操作的時候,會停在那裏發生阻塞?爲什麼我們輸入完成後,還要敲一下回車纔能有反應?

爲什麼發生阻塞?
簡單點說,程序在scanf處發生了I/O請求,需要數據,而scanf需要從輸入緩衝區讀取數據,程序剛運行的時候,這個緩衝區是空的,所以scanf得不到數據,就會阻塞程序,一直等待緩衝區內出現數據,此時我們從控制檯輸入內容,敲下回車,輸入的內容便會傳送給程序輸入緩衝區,被scanf阻塞的程序發現緩衝區裏有內容了,就會讓scanf繼續執行,讀入數據。


爲什麼要敲回車?
默認情況下,我們在控制檯的輸入內容是不會立刻同步到緩衝區的,也許是爲了防止誤輸入或效率問題,只有敲下回車的時候,輸入內容連同換行符纔會被一起傳送至緩衝區,但實際上,被傳送至緩衝區的換行符通常是我們所不需要的,它只是我們從控制檯輸入內容時所要按下的一個鍵而已,並不是我們需要的數據。

  當緩衝區內有了數據,scanf便開始按照設定的格式進行讀取,除%c格式符以外,scanf會按照格式裏的內容從左至右讀取指定格式的數據。

  我們回看之前的例子,單步解讀程序,格式內容"%d %d",我們輸入11 66敲回車後,輸入緩衝區內容變爲11 66\n(用\n代表換行符),scanf先嚐試讀取格式內容裏的第一個%d,也就是讀整數,從緩衝區裏成功讀到了11,此時緩衝區剩下66\n(注意開頭有個空格),然後嘗試讀取第二個%d,由於此時緩衝區開頭的內容是空格,%d不理睬 ,忽略開頭若干空白字符,然後遇到66,成功讀入,此時scanf沒有別的要讀的了,結束,函數返回2(讀入的數量),此時緩衝區剩餘\n,然後程序執行printf,再執行scanf("%c", &c);,由於此時緩衝區有內容\n,所以不阻塞,直接讀取,開頭說到%c會讀取一切字符,所以換行符自然而然被讀到了 ,所以變量c的內容是'\n',帶入printf("char=%c\n", c);,就可以明白爲什麼會出現圖示的現象了。

  如何解決?方案就是在每次使用scanf之後,調用fflush(stdin);來清空輸入緩衝區(也就是清掉那個惱人的換行符),然而這樣做很麻煩並且幾乎所有oj都拒絕這種危險的操作,所以一般使用getchar();來消除換行符(實際就是讀入但不賦值給變量)。

  getchar()scanf("%c")一樣,可以讀入任意字符,所以我們每次要使用scanf("%c")時,不妨先檢查在此之前是否有其他的輸入行,如果有的話,記得在這兩次輸入之間加上getchar()來抵消敲擊回車所產生的換行符。上例,修改後如下:

int a,b;
char c;
scanf("%d %d", &a, &b);
printf("a=%d b=%d\n", a, b);
getchar();
scanf("%c", &c);
printf("char=%c\n", c);

運行,我們依次輸入11 66,敲回車,輸入a,敲回車,結果如下:

運行結果2

這下就正常了。

下面是一個典型的輸入場景:


矩陣類字符輸入
描述: 第一行輸入兩個整數n,m,接着n行,每行m個字符
樣例:

2 3
abc
def

分析:

我們先不考慮怎麼一步步輸的,假設把整個輸入內容送到緩衝區,然後我們緩衝區內的字符序列是這樣的:2 3\nabc\ndef\n,觀察這個序列,a,d前面都有換行,所以讀取這兩個字符時,換行會產生干擾,當然最後一個換行程序都快結束了留着也沒什麼卵用,所以我們需要抵消三個換行,分別在輸完2 3以後和每行的字符輸完以後。

實現1:

char a[10][10];
int n,m;
scanf("%d %d", &n, &m);
getchar(); // 接下來要讀字符,而這裏產生了額外的換行,喫掉
for(int i = 0 ; i < n ; i++)
{
    for(int j = 0 ; j < m ; j++)
    {
        scanf("%c", &a[i][j]);
    }
    getchar(); // 讀完了m個字符,也就是一行,產生了一個換行符,而接下來的外層循環要讀下一行的字符,所以要喫掉它
}

實現2(輸入不包含空格):

如果題目明確表示或暗示輸入字符不包含空格,可以將每行字符作爲一個不含空格的字符串輸入,也就是使用scanf(“%s”),這樣我們就不用考慮換行符的抵消問題了,因爲前面說過,除了%c,其他格式符號都不會care換行符。
下面實現的前提是字符中不包含空格

char a[10][10];
int n,m;
scanf("%d %d", &n, &m);
for(int i = 0 ; i < n ; i++)
{
    scanf("%s", a[i]);
    //  這樣相當於直接將abc按照順序依次存入a[0][0],[0][1],[0][2],並自動在a[0][3]加入字符串結束標誌'\0',方便調試。
}

實現3(包含空格但不想用scanf):

如果覺得scanf太麻煩,而且輸入的字符的確包含空格,那麼可以用gets()函數,不過這個函數由於在設計時存在緩衝區溢出漏洞,C++標準裏並不推薦使用,但是日常訓練和競賽只要稍加註意並不會出現溢出問題(只要字符數組夠大就沒事),所以這個函數也是很好用的。
gets()函數會讀入一行字符,它會一直讀輸入緩衝區內的字符序列直到遇到了'\n'纔會停止,值得注意的是,gets()以換行符爲界,並不會把換行符作爲輸入的一部分而讀進字符串,但會消耗掉換行符,這點與scanf有所不同。
例如輸入緩衝區內容爲abc def gh 666\n233,gets函數從該緩衝區讀取到的內容爲abc def gh 666,而緩衝區剩餘233'\n'爲gets函數做“路標”但慘遭gets函數“拋棄”。當然,如果緩衝區第一個字符就是換行符,則gets會讀入一個空字符串並消耗掉緩衝區的這個換行符。上例代碼:

char a[10][10];
int n,m;
scanf("%d %d", &n, &m);
getchar(); // 此時緩衝區第一個字符是換行符,不消除的話下一個gets會讀到空字符串
for(int i = 0 ; i < n ; i++)
{
    gets(a[i]);
    // 這裏不需要爲下一次循環的gets做換行抵消,因爲gets本身就會在本行輸入結束時抵消換行符
}

  總結來說,瞭解了緩衝區的作用和各種輸入對於控制符的處理,再處理起字符類輸入問題就得心應手了,acm生涯裏不乏遇到各種姿勢的變態輸入格式,有的時候數據輸入完了再輸出來就變得不一樣了,還有的時候輸入數據複雜到整個題目的時間都用在了研究輸入上,所以掌握好基本的數據輸入纔是ac的第一步。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章