C輸入輸出函數與緩沖區
C語言輸入輸出流和緩沖區
C語言緩沖區(緩存)詳解
緩沖區又稱為緩存,它是內存空間的一部分。也就是說,在內存空間中預留了一定的存儲空間,這些存儲空間用來緩沖輸入或輸出的數據,這部分預留的空間就叫做緩沖區。
緩沖區根據其對應的是輸入設備還是輸出設備,分為輸入緩沖區和輸出緩沖區。
為什么要引入緩沖區
比如我們從磁盤里取信息,我們先把讀出的數據放在緩沖區,計算機再直接從緩沖區中取數據,等緩沖區的數據取完后再去磁盤中讀取,這樣就可以減少磁盤的讀寫次數,再加上計算機對緩沖區的操作大大快于對磁盤的操作,故應用緩沖區可大大提高計算機的運行速度。
又比如,我們使用打印機打印文檔,由于打印機的打印速度相對較慢,我們先把文檔輸出到打印機相應的緩沖區,打印機再自行逐步打印,這時我們的CPU可以處理別的事情。
現在您基本明白了吧,緩沖區就是一塊內存區,它用在輸入輸出設備和CPU之間,用來緩存數據。它使得低速的輸入輸出設備和高速的CPU能夠協調工作,避免低速的輸入輸出設備占用CPU,解放出CPU,使其能夠高效率工作。
緩沖區的類型
緩沖區 分為三種類型:全緩沖、行緩沖和不帶緩沖。
1) 全緩沖
在這種情況下,當填滿標準I/O緩存后才進行實際I/O操作。全緩沖的典型代表是對磁盤文件的讀寫。
2) 行緩沖
在這種情況下,當在輸入和輸出中遇到換行符時,執行真正的I/O操作。這時,我們輸入的字符先存放在緩沖區,等按下回車鍵換行時才進行實際的I/O操作。典型代表是標準輸入(stdin)和標準輸出(stdout)。
3) 不帶緩沖
也就是不進行緩沖,標準出錯情況stderr是典型代表,這使得出錯信息可以直接盡快地顯示出來。
ANSI C( C89 )要求緩存具有下列特征:
1.當且僅當標準輸入和標準輸出并不涉及交互設備時,它們才是全緩存的。
2.標準出錯決不會是全緩存的。
但是,這并沒有告訴我們如果標準輸入和輸出涉及交互作用設備時,它們是不帶緩存的還是行緩存的,以及標準輸出是不帶緩存的,還是行緩存的。
大部分系統默認使用下列類型的緩存:
1.標準出錯是不帶緩存的。
2.如果是涉及終端設備的流,則它們是行緩存的;否則是全緩存的。
我們經常要用到標準輸入輸出流,而ANSI C對stdin、stdout和stderr的緩存特征沒有強行的規定,以至于不同的系統可能有不同的stdin、stdout和stderr的緩存特征。目前主要的緩存特征是:stdin和stdout是行緩存;而stderr是無緩存的。?
緩沖區的大小
如果我們沒有自己設置緩沖區的話,系統會默認為標準輸入輸出設置一個緩沖區,這個緩沖區的大小通常是512個字節的大小。
緩沖區大小由 stdio.h 頭文件中的宏 BUFSIZ 定義,如果希望查看它的大小,包含頭文件,直接輸出它的值即可:
- printf("%d", BUFSIZ);
緩沖區的大小是可以改變的,也可以將文件關聯到自定義的緩沖區,詳情可以查看 setvbuf()和 setbuf() 函數。
緩沖區的刷新(清空)
下列情況會引發緩沖區的刷新:
1.緩沖區滿時;
2.行緩沖區遇到回車時;
3.關閉文件;
4.使用特定函數刷新緩沖區。
結合緩沖區談談C語言getchar()、getche()、getch()的區別
本文將用到C語言緩沖區的概念,如果您不了解緩沖區,請查看:C語言緩沖(緩存)
三個函數的對比
-- 緩沖區 頭文件 回顯
getchar() 有緩沖區 stdio.h 有回顯
getch() 無緩沖區 conio.h 無回顯
getche() 無緩沖區 conio.h 有回顯
getchar()函數
先來看一下getchar(),其原型為:
int getchar(void);
當程序調用getchar()函數時,程序就等著用戶按鍵,用戶輸入的字符被存放在鍵盤緩沖區中,直到用戶按回車為止(回車字符也放在緩沖區中)。當用戶鍵入回車之后,getchar()函數才開始從鍵盤緩沖區中每次讀入一個字符。也就是說,后續的getchar()函數調用不會等待用戶按鍵,而直接讀取緩沖區中的字符,直到緩沖區中的字符讀完后,才重新等待用戶按鍵。
通俗一點說,當程序調用getchar()函數時,程序就等著用戶按鍵,并等用戶按下回車鍵返回。期間按下的字符存放在緩沖區,第一個字符作為函數返回值。繼續調用getchar()函數,將不再等用戶按鍵,而是返回您剛才輸入的第2個字符;繼續調用,返回第3個字符,直到緩沖區中的字符讀完后,才等待用戶按鍵。
下邊的一個實例,會讓你有深刻的體會:
#include <stdio.h> int main() { char c; //第一次調用getchar()函數 //程序執行時,您可以輸入一串字符并按下回車鍵,按下回車鍵后該函數才返回 c=getchar(); //顯示getchar()函數的返回值 printf("%c\n",c); //暫停 system("PAUSE"); while((c=getchar())!='\n') { printf("%c",c); } //暫停 system("PAUSE"); return 0; }
這段小代碼很簡單,并且在代碼內部都有注釋。
getchar()函數的執行就是采用了行緩沖。第一次調用getchar()函數,會讓程序使用者(用戶)輸入一行字符并直至按下回車鍵 函數才返回。此時用戶輸入的字符和回車符都存放在行緩沖區。
再次調用getchar()函數,會逐步輸出行緩沖區的內容。
請再看下面一個例子:
#include <stdio.h> int main() { char ch1; char ch2; ch1 = getchar(); ch2 = getchar(); printf("%d %d", ch1, ch2); return 0; }
程序的本意很簡單,就是從鍵盤讀入兩個字符,然后打印出這兩個字符的ASCII碼值。可是執行程序后會發現出了問題:當從鍵盤輸入一個字符后,就打印出了結果,根本就沒有輸入第二個字符程序就結束了。例如用戶輸入字符’a', 打印結果是97,10。這是為什么呢?
getchar()函數是從輸入流緩沖區中讀取數據的,而不是從鍵盤(終端)緩沖區讀取。當讀取遇到回車(\n)結束時,這個'\n'會一起讀入到輸入流緩沖區的,所以第一次接收輸入時取走字符后會留下字符\n,這樣第二次getchar()直接從緩沖區中把\n取走了,顯然讀取成功了,所以不會再從終端讀取!其實這里的10恰好是回車符!這就是為什么這個程序只執行了一次輸入操作就結束的原因!
getch()和getche()函數
在TC2.0時代,C程序員總是喜歡在程序末尾加上getch(),來實現程序運行完了暫停不退出的效果。如果不這樣做,在TC2.0的環境中Ctrl+F9編譯并運行后會立即退出程序,根本來不及看到結果。這時如果要看結果,就要按Alt+F5回到DOS環境中去,很麻煩。而如果在程序的結尾加上一行getch();語句,就可以省掉回DOS看結果這個步驟,因為程序運行完了并不退出,而是在程序最后把屏幕停住了,按任意鍵才退出程序。
實際上,getch()的作用是從鍵盤接收一個字符,且不帶回顯。就是說,你按了一個鍵后它并不在屏幕上顯示你按的什么,而繼續運行后面的代碼,所以在C語言中可以用它來實現“按任意鍵繼續”的效果,即程序中遇到getch();語句,就會停下來,等你按任意鍵,它接收了這個字符鍵后再繼續執行后面的代碼。
getche()和getch()很相似,它也需要引入頭文件conio.h,它們之間的區別就在于:getch()無回顯,getche()有回顯。請看下面的例子:
#include<stdio.h> #include<conio.h> void main() { char ch; int i; for(i=0;i<5;i++) { ch=getch(); printf("%c",ch); } }
首先這是個連續5次的循環來實現5次停頓,等待你輸入。編譯并運行這個程序,假設輸入的是abcde,那么屏幕上顯示的結果也是abcde,這個abcde并不是在ch=getch();中輸出的。把printf("%c",ch);這行語句去掉,就會發現按5次任意鍵程序就結束了,但屏幕上什么都沒有顯示。
你可以把代碼中的getch()換成getche()看看有什么不同。如果還是輸入abcde,那么屏幕上顯示的結果是aabbccddee,我們把printf("%c",ch);這行語句再去掉,顯示的結果就是abcde了,說明程序在執行ch=getche();這條語句的時候就把我們輸入的鍵返回顯示在屏幕上,有無回顯就是它們的唯一區別。
請大家再看下面一個例子:
#include<stdio.h> #include<conio.h> void main() { char ch='*'; while(ch=='*') { printf("\n按 * 繼續循環,按其他鍵退出!"); ch=getch(); } printf("\n退出程序!"); }
你可以在這個循環體中添加你想要的功能,程序中按*繼續循環,其他任意鍵退出,而且利用getch()無回顯的特性,不管你按什么鍵,都不會在屏幕上留下痕跡,使你的界面達到美觀效果。
還有getchar是很值得研究的:getchar()是stdio.h中的庫函數,它的作用是從stdin流(標準輸入流)中讀入一個字符,也就是說,如果stdin有數據的話不用輸入它就可以直接讀取了。而getch()和getche()是conio.h中的庫函數,它的作用是從鍵盤接收字符。
與前面兩個函數的區別在于: getchar()函數等待輸入直到按回車才結束(前提是緩沖區沒有數據),回車前的所有輸入字符都會逐個顯示在屏幕上。但只有第一個字符作為函數的返回值。
#include<stdio.h> #include<conio.h> void main() { char c; // 從鍵盤讀入字符直到回車結束 //getchar()在這里它只返回你輸入字符串的第一個字符,并把返回值賦值給c c=getchar(); // 顯示輸入的第一個字符 putchar(c); }
看到這個程序,相信你肯定會有疑問。這個就是從緩沖區中讀取字符的例子。第一次getchar()時,確實需要人工的輸入,但是如果你輸了多個字符,以后的getchar()再執行時就會直接從緩沖區中讀取了。
#include<stdio.h> #include<conio.h> void main() { char c; // 每個getchar()依次讀入一個字符 while ((c=getchar())!='\n') printf("%c",c); // 按照原樣輸出 }
程序運行后,首先停下來,等待輸入一個字符串,輸入完畢后,它會把你輸入的整個字符串都輸出來了。
這是為什么?getchar()不是只返回第一個字符么,這里為什么全部輸出了?
因為我們輸入的字符串并不是取了第一個字符就把剩下的字符串丟掉了,它還在我們的緩沖區中,就好像開閘放水,你把水放到閘里去以后,開一次閘就放掉一點,開一次就放掉一點,直到放光了為止,這里開閘動作就相當于調用一次getchar()。我們輸入的字符串也是這么一回事,首先我們輸入的字符串是放在內存的緩沖區中的,我們調用一次getchar()就把緩沖區中里出口最近的一個字符輸出,也就是最前面的一個字符輸出,輸出后,就把它釋放掉了,但后面還有字符串,所以我們就用循環把最前面的一個字符一個個的在內存中釋放掉,直到不滿足循環條件退出為止。
例子中循環條件里的'\n'實際上就是你輸入字符串后的回車符,所以意思就是說,直到遇到回車符才結束循環,而getchar()函數就是等待輸入(或緩沖區中的數據)直到按回車才結束,所以實現了整個字符串的輸出。當然,我們也可以把循環條件改一下,比如while ((c=getchar())!='a'),就是遇到字符'a'就停止循環,當然意思是如果你輸入“12345a213123/n”那么只會輸出到a,結果是12345a。
請注意:用getchar()是到標準輸入流中讀取數據,所以第一個getchar()接受的是剛剛中斷的流隊列中即將出列的第一個字符(不限于回車符,上面舉過例子了),如果流隊列不為空,執行getchar()就繼續放水,直到把回車符也放空為止,空了之后再在執行getchar()就停下等待你的輸入了。
那么為什么getch()每次都是等待用戶的輸入呢?因為getch()是從鍵盤接收,即時的接收,并不是從stdin流(標準輸入流)中去讀取數據。
C語言FILE結構體以及緩沖區深入探討
在C語言中,用一個指針變量指向一個文件,這個指針稱為文件指針。通過文件指針就可對它所指的文件進行各種操作。
定義文件指針的一般形式為:
FILE *fp;
這里的FILE,實際上是在stdio.h中定義的一個結構體,該結構體中含有文件名、文件狀態和文件當前位置等信息。我們通過fopen返回一個文件指針(指向FILE結構體的指針)來進行文件操作。
注意:FILE是文件緩沖區的結構,fp也是指向文件緩沖區的指針。
不同編譯器 stdio.h 頭文件中對 FILE 的定義略有差異,這里以標準C舉例說明:
#define NULL 0 #define EOF (-1) #define BUFSIZ 1024 #define OPEN_MAX 20 // 一次打開的最大文件數 // 定義FILE結構體 typedef struct _iobuf { int cnt; // 剩余的字符,如果是輸入緩沖區,那么就表示緩沖區中還有多少個字符未被讀取 char *ptr; // 下一個要被讀取的字符的地址 char *base; // 緩沖區基地址 int flag; // 讀寫狀態標志位 int fd; // 文件描述符 } FILE; extern FILE _iob[OPEN_MAX]; #define stdin (&_iob[0]) // stdin 的文件描述符為0 #define stdout (&_iob[1]) // stdout 的文件描述符為1 #define stderr (&_iob[2]) // stdout 的文件描述符為2 enum _flags { _READ =01, // 讀文件 _WRITE =02, // 寫文件 _UNBUF =04, // 無緩沖 _EOF = 010, // 文件結尾EOF _ERR = 020 // 出錯 }; int _fillbuf(FILE *); // 函數聲明,填充緩沖區 int _flushbuf(int, FILE *); // 函數聲明,刷新緩沖區 #define feof(p) ((p)->flag & _EOF) != 0) #define ferror(p) ((p)->flag & _ERR) != 0) #define fileno(p) ((p)->fd) #define getc(p) (--(p)->cnt >= 0 \ ? (unsigned char) *(p)->ptr++ : _fillbuf(p)) #define putc(x,p) (--(p)->cnt >= 0 \ ? *(p)->ptr++ = (x) : _flushbuf((x),p)) #define getchar() getc(stdin) #define putcher(x) putc ((x), stdout)
看吧,我們經常使用的 NULL、EOF、feof()、getc() 等都是 stdio.h 中定義的宏。
注意:一個長的 #define 語句可以用反斜杠(\)分成多行。
下面說一下如果控制緩沖區。
我們知道,當我們從鍵盤輸入數據的時候,數據并不是直接被我們得到,而是放在了緩沖區中,然后我們從緩沖區中得到我們想要的數據 。如果我們通過setbuf()或setvbuf()函數將緩沖區設置10個字節的大小,而我們從鍵盤輸入了20個字節大小的數據,這樣我們輸入的前10個數據會放在緩沖區中,因為我們設置的緩沖區的大小只能夠裝下10個字節大小的數據,裝不下20個字節大小的數據。那么剩下的那10個字節大小的數據怎么辦呢?暫時放在了輸入流中。請看下圖:
上面的箭頭表示的區域就相當是一個輸入流,紅色的地方相當于一個開關,這個開關可以控制往深綠色區域(標注的是緩沖區)里放進去的數據,輸入20個字節的數據只往緩沖區中放進去了10個字節,剩下的10個字節的數據就被停留在了輸入流里!等待下去往緩沖區中放入!接下來系統是如何來控制這個緩沖區呢?
再說一下 FILE 結構體中幾個相關成員的含義:
cnt //剩余的字符,如果是輸入緩沖區,那么就表示緩沖區中還有多少個字符未被讀取
ptr//下一個要被讀取的字符的地址
base//緩沖區基地址
在上面我們向緩沖區中放入了10個字節大小的數據,FILE結構體中的 cnt 變為了10 ,說明此時緩沖區中有10個字節大小的數據可以讀,同時我們假設緩沖區的基地址也就是 base 是0x00428e60 ,它是不變的 ,而此時 ptr 的值也為0x00428e60 ,表示從0x00428e60這個位置開始讀取數據,當我們從緩沖區中讀取5個數據的時候,cnt 變為了5 ,表示緩沖區還有5個數據可以讀,ptr 則變為了0x0042e865表示下次應該從這個位置開始讀取緩沖區中的數據 ,如果接下來我們再讀取5個數據的時候,cnt 則變為了0 ,表示緩沖區中已經沒有任何數據了,ptr 變為了0x0042869表示下次應該從這個位置開始從緩沖區中讀取數據,但是此時緩沖區中已經沒有任何數據了,所以要將輸入流中的剩下的那10個數據放進來,這樣緩沖區中又有了10個數據,此時 cnt 變為了10 ,注意了剛才我們講到 ptr 的值是0x00428e69 ,而當緩沖區中重新放進來數據的時候這個 ptr 的值變為了0x00428e60 ,這是因為當緩沖區中沒有任何數據的時候要將 ptr 這個值進行一下刷新,使其指向緩沖區的基地址也就是0x0042e860這個值!因為下次要從這個位置開始讀取數據!
在這里有點需要說明:當我們從鍵盤輸入字符串的時候需要敲一下回車鍵才能夠將這個字符串送入到緩沖區中,那么敲入的這個回車鍵(\r)會被轉換為一個換行符\n,這個換行符\n也會被存儲在緩沖區中并且被當成一個字符來計算!比如我們在鍵盤上敲下了123456這個字符串,然后敲一下回車鍵(\r)將這個字符串送入了緩沖區中,那么此時緩沖區中的字節個數是7 ,而不是6。
緩沖區的刷新就是將指針 ptr 變為緩沖區的基地址 ,同時 cnt 的值變為0 ,因為緩沖區刷新后里面是沒有數據的!
C語言為指針動態分配內存
C語言程序員要嚴防內存泄漏,這個“內存泄漏”就是由動態內存分配引起的。指針是C語言和其它語言的最大區別,也是很多人不能跨入C語言的一道門檻。既然指針是這么一個“危險”的壞東西,干嗎不取消它呢?
其實指針本身并沒有好壞,它只是一種操作地址的方法,學會了便可以發揮其它語言難以匹敵的功能,沒學會的話,只能做其它語言的程序員,也同樣發揮你的光和熱。
變量和數組可以通過指針來轉換
“int *x”中的x究竟是不是數組?光看這一句無法確定,因為它既可表示單個變量內容,也可表示數組。請理解下面的例子:
#include <stdio.h> int main(void){ int *num = NULL; int *x, y[] = {12, 22,32}, z = 100; //下面演示,指針既可充當變量、也可充當數組 x=&z; //整型變量的地址賦給x printf("*x=%d, x[0]=%d\n", *x, x[0]); x = y; //數組的地址賦給x printf("*x=%d, x[ 0]=%d, x[ 1]=%d, x[2]=%d\n", *x, x[0], x[1], x[2]); x = y + 1; //數組的第二位地址賦給x printf("*x=%d, x[-1]=%d, x[ 0]=%d, x[1]=%d\n", *x, x[-1], x[0], x[1]); x = y + 2; //數組的第三位地址賦給x printf("*x=%d, x[-2]=%d, x[-1]=%d, x[0]=%d\n", *x, x[-2], x[-1], x[0]); return 0; }
運行結果:
*x=100, x[0]=100
*x=12, x[ 0]=12, x[ 1]=22, x[2]=32
*x=22, x[-1]=12, x[ 0]=22, x[1]=32
*x=32, x[-2]=12, x[-1]=22, x[0]=32
動態分配內存
前面講到的指針,基本上將已經定義好的變量的地址賦給指針變量,現在要學的是向操作系統申請一塊新的內存。申請到的內存,必須在某個地方手動釋放,因此下面2個函數必須配對使用。malloc()和free(),都是標準函數,在stdlib.h中定義。
根據不同的電腦使用狀況,申請內存有可能失敗,失敗時返回NULL,因此,動態申請內存時一定要判斷結果是否為空。malloc()的返回值類型是“void *”,因此,不要忘記類型轉換。(許多人都省略了。)
#include <stdio.h> #include <stdlib.h> #include <string.h> int main(void){ char *p ; p = (char *)malloc(60 * sizeof(char)) ; if (p == NULL) { //這個判斷是必須的 printf("內存分配出錯!"); exit(1); } strcpy(p, "http://see.xidian.edu.cn/cpp/u/jiaocheng/\n"); //不要忘記給新內存賦值 printf("%s", p); free(p); //過河一定要拆橋 p = NULL ; //釋放后的指針置空,這是非常好的習慣,防止野指針。 return 0; }
運行結果:
http://see.xidian.edu.cn/cpp/u/jiaocheng/
隱蔽的內存泄漏
內存泄漏主要有以下幾種情況:
1.內存分配未成功,卻使用了它。
2.內存分配雖然成功,但是尚未初始化就引用它。
3.內存分配成功并且已經初始化,但操作越過了內存的邊界。
4.忘記了釋放內存,造成內存泄露。
5.釋放了內存卻繼續使用它。
下面的程序造成內存泄漏,想想錯在何處?如何修改?
#include <stdio.h> #include <stdlib.h> int main(void){ int *p, i; p = (int *)malloc(6 * sizeof(int)) ; if (p == NULL) { //判斷是否為空 printf("內存分配出錯!"); exit(1); } for (i=0; i<6; i++) { p++; *p = i; printf("%2d", *p); } printf("\n"); free(p); //這句運行時出錯 return 0; }
對動態內存的錯誤觀念
有人對某一只在函數內使用的指針動態分配了內存,用完后不釋放。其理由是:函數運行結束后,函數內的所有變量全部消亡。這是錯誤的。動態分配的內存是在“堆”里定義,并不隨函數結束而消亡。
有人對某動態分配了內存的指針,用完后直接設置為NULL。其理由是:已經為NULL了,這就釋放了。這也是錯誤的。指針可以任意賦值,而內存并沒有釋放;相反,內存釋放后,指針也并不為NULL。

浙公網安備 33010602011771號