从C++Primer某习题出发,谈谈C语言标准I/O的缓存问题
剛看完信號那章,覺得處理信號時的sigsetjmp/siglongjmp似乎跟異常的跳出很像,于是想去復(fù)習(xí)C++異常,然后發(fā)現(xiàn)了對I/O沒有充分理解的問題。
題目是C++ Primer 5.6.3節(jié)的練習(xí)5.25,描述如下:
1、從標(biāo)準(zhǔn)輸入讀取2個整數(shù), 輸出第1個整數(shù)除以第2個整數(shù)的結(jié)果。
2、如果第2個整數(shù)為0,拋出異常;
3、用try語句塊捕捉異常,catch語句中為用戶輸出一條提示信息,詢問是否輸入新數(shù)并重新執(zhí)行try語句塊的內(nèi)容。
于是我隨手一寫,就寫出了這樣的代碼
#include <stdio.h> #include <stdexcept>int main() {int x, y;while (1) {try {fputs("input two numbers: ", stdout);scanf("%d %d", &x, &y);if (y == 0)throw std::runtime_error("除數(shù)為0!");printf("%d / %d = %d\n", x, y, x / y);}catch (std::exception& e) {fputs(e.what(), stderr);fputs("是否重新輸入?[Y/n] ", stdout);char ch = getchar();if (ch == 'Y' || ch == 'y')continue;}break;}return 0; }調(diào)試看看,在getchar()下面加一句printf("%d\n", ch);后重新運行,會發(fā)現(xiàn)打印的是10(ACSII碼中換行符'\n'對應(yīng)的是10)
也就是說getchar()不需要等待我們輸入就獲取了字符。那么這個換行符是怎么來的呢?
哦,剛才輸入了"1 0"后是按了回車,然后scanf才執(zhí)行。scanf讀到第2個int對應(yīng)字符串部分('0')終止就不再讀了,也就是'\n'并沒有讀進去。而標(biāo)準(zhǔn)I/O庫采取了緩存策略,標(biāo)準(zhǔn)輸入的字符都放在一個字符串?dāng)?shù)組內(nèi),比如我剛才輸入1、空格、0、Enter時,在標(biāo)準(zhǔn)輸入(stdin)對應(yīng)的FILE結(jié)構(gòu)中,它的緩存(可以看做一個字符數(shù)組)是這樣的
'1', ' ', '0', '\n', '\0', '\0', ...
FILE結(jié)構(gòu)有個指向當(dāng)前位置的指針(注:下文中的指針均默認指代這個指針),最初是指向'1'的,然后進行scanf,讀第2個int時,指針指向'0',然后讀取'0',指針右移,此時指向'\n',不是一個數(shù)字,開始分析scanf讀到的2個int對應(yīng)字符串"1"和"0"并且轉(zhuǎn)換成int存入x和y的地址(&x和&y)中。
結(jié)果就是,指針指向的是'\n',調(diào)用getchar()時,標(biāo)準(zhǔn)輸入的緩存中已經(jīng)有字符,那么直接取出即可。只有在標(biāo)準(zhǔn)輸入的指針已經(jīng)到達緩存非'\0'字符的末尾(即所謂字符數(shù)組風(fēng)格字符串的末尾),才會阻塞進程并且等待用戶輸入,用戶的輸入會填入緩存,然后getchar()取得指針指向的字符。
回到這里,指針指向'\n',那么getchar()就會把它取出來并返回,然后指針右移。因此我們需要接收到用戶新輸入的字符,需要像這樣
getchar(); // 取出剛才的換行符 char ch = getchar();如果熟悉庫函數(shù)fflush(),很可能會采用fflush(stdin);的方式來取代getchar(),意思就是沖刷標(biāo)準(zhǔn)輸入的緩存。
看似可行,但是,標(biāo)準(zhǔn)輸入不同于標(biāo)準(zhǔn)輸出(stdout)和標(biāo)準(zhǔn)錯誤(stderr),后兩者被沖刷的話,指針右移直到字符串末尾,然后右移過程中的字符被輸出到屏幕上(雖然這么說,但實際上是一次系統(tǒng)調(diào)用打印出來)。也不同于打開普通文件(txt等等)的FILE*,沖刷它們會把字符串輸出到文本中。
那么,標(biāo)準(zhǔn)輸入又能輸出到哪呢?
POSIX.1-2001 did not specify the behavior for flushing of input streams, but the behavior is specified in POSIX.1-2008.
在POSIX.1-2001標(biāo)準(zhǔn)中,沖刷輸入流的行為是未定義的。雖然POSIX新標(biāo)準(zhǔn)定義了其行為,我沒有具體查看,但是在Ubuntu 16.04 gcc 5.4.0下,用-std=gnu++11編譯得到的結(jié)果并不是我們期望的那樣。盡管網(wǎng)上能搜到很多C語言考題會考fflush(stdin),還是VC6.0環(huán)境(我就不多說了,點到即止)
?
本來像上面那樣更改代碼后就OK了,但是健壯性較好的做法是只判斷第1個字符即可,后面的字符隨便輸入,比如卸載軟件的命令
我輸入了yabcd wufq ue這一段瞎按的字符串,只有首字母為y,但是卸載程序仍然執(zhí)行了。
那么我的程序是否也能如此呢?
僅僅是輸入了2個字符,結(jié)果不僅重新輸入了一些信息,還直接返回了。
來分析一下程序的執(zhí)行流程:
1、我輸入了yy,此時從指針指向的位置起,緩存字符是'\n', 'y', 'y';
2、getchar()讀取'\n',第2個getchar()讀取'y'返回并賦值給字符ch,然后if語句判斷ch是否為'Y'或'y'
3、if語句為真,執(zhí)行continue;跳過while循環(huán)中剩余代碼(即break;),重新進入while循環(huán)。
就此打住,注意,現(xiàn)在stdin的緩存是'y',而scanf會根據(jù)格式化字符串"%d %d"讀取,也就是首先要讀1個int,如果碰到正負號和數(shù)字之外的字符會怎樣呢?
把代碼的scanf那句改成下面這樣,檢查返回值(scanf的返回值為成功格式化寫入的變量個數(shù))
int n = scanf("%d %d", &x, &y); if (n != 2) {fprintf(stderr, "scanf實際讀取int的數(shù)量: %d\n", n);return 1; }運行結(jié)果如下
實際上碰到數(shù)字、正負號(還有空白字符)之外的字符就會返回,因為格式化輸入已經(jīng)不合法了。
關(guān)于printf和scanf的具體實現(xiàn),主要是利用了C語言的可變參數(shù)類型va_list,具體可以參考C語言的經(jīng)典教材《C程序設(shè)計語言》作者是丹尼斯·里奇(Dennis Ritchie),C語言之父&UNIX之父。7.3節(jié) 變長參數(shù)表里面提供了一份簡化版printf的實現(xiàn)。
如果自己動手試著實現(xiàn)下,對printf/scanf的理解會更深刻。
?
于是回到問題,那我們該怎么解決呢?一個自然而然想到的方法是像剛才getchar()一樣,把stdin的緩存全部讀完,即在if語句之前加上
while (getchar() != '\n') { }但是這會有調(diào)用函數(shù)的開銷,比如我輸入了10000個字符,那么就要調(diào)用getchar() 10000次。函數(shù)調(diào)用次數(shù)過多的話,開銷就不能忽視了,因為每次函數(shù)調(diào)用都伴隨著參數(shù)的入棧、出棧,函數(shù)棧幀的建立和銷毀。
但是從性能的角度,可以采取更好的方法
char buf[BUFSIZ]; while (!fgets(buf, sizeof(buf), stdin)) { }那就是減少函數(shù)調(diào)用的次數(shù),每次獲取BUFSIZ個字符,這樣輸入10000個字符的話只需要調(diào)用函數(shù)10000 / BUFSIZ次。
?
從實踐的角度看,這種優(yōu)化在這里其實沒有必要,首先,沒有誰那么無聊輸入這么多字符,頂多不小心多按了幾個字母。比如手滑按Enter鍵時把旁邊的鍵給按下了。其次,這個程序本身就非常簡單,甚至都不用考慮效率。
但是了解這些是有意義的??丛创a不是為了重復(fù)造輪子,重復(fù)造輪子也不是僅僅為了重復(fù)造輪子,而是加深對底層實現(xiàn)的理解。既然選擇了C/C++,就不得不去面對名為“效率”的怪物,不得不去了解底層實現(xiàn)。
?
最后再補充一點,C語言標(biāo)準(zhǔn)I/O庫在終端I/O上默認是行緩沖,標(biāo)準(zhǔn)I/O庫其實也要從應(yīng)用態(tài)切換到內(nèi)核態(tài)去調(diào)用內(nèi)核的read/write等函數(shù),10000次用戶函數(shù)調(diào)用的開銷也許不大,但是10000次上下文切換的開銷就不小了。內(nèi)核的I/O也有自己的一套緩存。所謂行緩沖,就是輸入換行符時,一次性把目前為止輸入/輸出的所有字符進行I/O,也就是每讀取一行(只要這一行不是特別特別長)只進行1次系統(tǒng)調(diào)用(system call)。(參考《Unix環(huán)境高級編程》)
因此每次輸入換行符時,才把鍵盤輸入的字符串一次性給搬運到內(nèi)存中,然后scanf從頭開始分析字符串。
轉(zhuǎn)載于:https://www.cnblogs.com/Harley-Quinn/p/6741677.html
總結(jié)
以上是生活随笔為你收集整理的从C++Primer某习题出发,谈谈C语言标准I/O的缓存问题的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 字符串在内存中的存储——C语言进阶
- 下一篇: C++源码的调用图生成