日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

从C++Primer某习题出发,谈谈C语言标准I/O的缓存问题

發(fā)布時間:2025/3/21 c/c++ 16 豆豆
生活随笔 收集整理的這篇文章主要介紹了 从C++Primer某习题出发,谈谈C语言标准I/O的缓存问题 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

剛看完信號那章,覺得處理信號時的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)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。