關於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慢的原因很多,其中很重要的一點是爲了使cin
與scanf
可以兼容混合使用,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
,敲回車,結果如下:
這下就正常了。
下面是一個典型的輸入場景:
矩陣類字符輸入
描述: 第一行輸入兩個整數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的第一步。