c語言緩衝區的理解
開始深入地瞭解一下c語言,發現以前對於緩衝區的理解並不清楚,在這裡對此作一些深入的瞭解,後續關於緩衝區的問題也在本篇部落格上進行更新。
備註:本篇部落格的內容建立在剛閱讀的c語言中文網相關內容,網址如下:http://c.biancheng.net/cpp/html/2413.html(需要會員資格檢視相關內容)
緩衝區的概念
緩衝區(Buffer),又稱為快取(Cache),就是一塊記憶體區,處於IO裝置與CPU之間,用來快取資料。它使得低速的IO裝置和高速的CPU能夠協調工作,避免低速的IO裝置佔用CPU,解放出CPU,使其能夠高效率工作。直白地說,在記憶體中預留了一定的儲存空間,用來暫時儲存輸入或輸出的資料,這部分預留的空間就叫做緩衝區。
緩衝區的型別
緩衝區有三種類型:全緩衝、行緩衝和不帶緩衝。
1)全緩衝
當填滿緩衝區後才進行實際IO操作。全緩衝的典型代表是對磁碟檔案的讀寫
(讀到的檔案內容高於緩衝區時,才會真正執行磁碟檔案的讀寫操作)。
2)行緩衝
遇到換行符\n時,執行真正的I/O操作。這時,我們輸入的字元先存放在緩
衝區,等按下回車鍵換行時才進行實際的I/O操作。典型代表是標準輸入(stdin)和標準輸出(stdout)。
3)不帶緩衝
也就是不進行緩衝,一旦有操作,立即進行IO操作標準錯誤檔案stderr是
典型代表,這使得出錯資訊可以直接儘快地顯示出來。
緩衝區的重新整理(清空)
下列情況會引發緩衝區的重新整理:
緩衝區滿時;
行緩衝區遇到回車時;
關閉檔案;
使用特定函式重新整理緩衝區(fflush(stdin))。
緩衝區大小
如果沒有設定緩衝區的話,系統會預設為標準輸入輸出設定一個緩衝區,這個緩衝區的大小通常是512個位元組的大小。緩衝區大小由 stdio.h標頭檔案中的巨集BUFSIZ 定義。通過如下程式碼可以檢視該值:
printf(“%d”, BUFSIZ);//macOS輸出值為1024
對於緩衝區,有一個常用的函式是需要了解的:scanf();
scanf() 是帶有緩衝區的。遇到 scanf() 函式,程式會先檢查輸入緩衝區中是否有資料:
1. 如果沒有,等待使用者輸入。使用者從鍵盤輸入的每個字元都會暫時儲存到緩衝區,直到按下回車鍵,輸入結束,scanf() 再從緩衝區中讀取資料,賦值給變數。
2. 如果有資料,scanf()會直接讀取,不會等待使用者輸入。
3. 當控制字串不是以%xxx 開頭時,回車鍵就起作用了(不再視為行的結尾符),scanf()會對它進行匹配,只是匹配失敗而已。
4. 在以%c的格式輸入時,\n不會被視為換行字元,而是會作為1個字元存在緩衝區,被之後的%c格式讀入。
上述關於scanf()函式的特性會導致一些看起來很奇怪的問題出現:
如下列程式碼:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a=0, b=0;
scanf("a=%d", &a);
scanf("b=%d", &b);
printf("a=%d, b=%d\n", a, b);
return 0;
}
//輸入a=100後換行,終端不等待輸入b=..,直接輸出a=100, b=0
//這裡就跟上述的第3條特性相關了,當控制字串不是以%xxx 開頭時,
//回車鍵就不起作用了,scanf()會對它進行匹配,只是匹配失敗而已。
//當輸入a=100換行時,先匹配a=100,之後將a=100從緩衝區刪除,然後
//匹配\n字元,發現\n並不是不=...的格式,匹配失敗,直接執行下一行程式碼
如果想讓上面的程式碼執行不出現這種邏輯錯誤,可以將其改為如下形式:
#include <stdio.h>
int main()
{
int a, b;
scanf("%d", &a);
scanf("%d", &b);
printf("a=%d, b=%d\n", a, b);
return 0;
}
//輸入:100
//200
//輸出:a=100,b=200
上述程式碼還有一種輸入方式,輸入100 200 300 400 500 600 700,即不換行,直接用空格,一次性寫入緩衝區,scanf()讀取資料時,先讀取100,賦值給a,將100從緩衝區刪除,再讀取200,賦值給b,執行下一行程式碼,在上述程式中,直到程式碼執行完成,緩衝區仍然還有300 400 500 600 700這些資料,只是沒有使用到而已。
至於scanf()不安全的緩衝區問題,與第1、2條緩衝區的特性有關:
c語言中,scanf()中的格式控制符必須與引數型別相同,不然會出現無法理解的錯誤,如下列程式碼(至於為什麼,現在不去糾結):
#include <stdio.h>
int main()
{
char a,b;
scanf("%d",&a);
scanf("%d",&b);
printf("%d %d\n", a,b);
return 0;
}
//輸入:12
//34
//輸出:0 34
清空快取區的方法
scanf() 的緩衝區有時會引發奇怪的問題,多個 scanf() 之間要注意清空緩衝區。清空快取區存在兩種解決方式:
- 將緩衝區中的資料丟棄
- 將緩衝區中的資料讀出來,但不使用。
將緩衝區中的資料丟棄,c語言提供了fflush(stdin)方法,在C語言中,為了便於操作,鍵盤和顯示器也被看作是檔案。鍵盤稱為標準輸入檔案(stdin),顯示器稱為標準輸出檔案(stdout)。如下列程式碼:
#include<stdio.h>
int main()
{
int a, b;
scanf("%d", &a);
fflush(stdin);
scanf("%d", &b);
printf("a=%d, b=%d\n", a, b);
return 0;
}
//按照c語言中文網的demo說法,該demo在windows的執行結果應該如下:
//執行結果:
//100 200↙
//300↙
//a=100, b=300
//第一個scanf() 讀取完成後,將100賦值給變數a,緩衝區中剩下200。然
//後呼叫 fflush() 函式將200從緩衝區中清除。執行到第二個 scanf()
//時由於緩衝區中沒有資料,所以會等待使用者輸入。在LinuxGCC下可能無
//效,因為C語言標準規定:對於以 stdin 為引數的 fflush() 函式,它
//的行為是不確定的,fflush() 操作輸入流是對標準C語言的擴充。
//上述程式碼在macbook gcc 執行結果如下:
//100 200↙
//a=100, b=200
//也即fflush(stdin)語句無效
將緩衝區中的資料讀出來,但不使用
1. 使用下列語句:
//如果沒有遇到換行符或者檔案結尾符,就繼續讀出資料,但是不做任何操作,直到遇到換行符或者檔案結尾符
while((c = getchar()) != '\n' && c != EOF);
2.使用下列語句:
//%*[^\n]:逐個讀取緩衝區中的'\n'字元前其它字元,%後面的*表示將讀取的這些字元丟棄,遇到'\n'字元時便停止讀取。
//建議使用該方法
scanf("%*[^\n]%*c");
上述兩種方式的例子如下:
#include <stdio.h>
int main()
{
int a, b;
char c;
scanf("%d", &a);
scanf("%*[^\n]%*c");
scanf("%d", &b);
printf("a=%d, b=%d\n", a, b);
while((c=getchar()) != '\n' && c != EOF)
;
scanf("%d",&a);
while((c=getchar()) != '\n' && c != EOF)
;
scanf("%d",&b);
printf("a=%d,b=%d\n", a,b);
return 0;
}
//執行結果:
//100 200↙
//300↙
//a=100, b=300
//9 99↙
//999↙
//a=9, b=999
關於緩衝區:還有個緩衝區溢位的問題,暫時不做深究,先列出其定義,維基百科關於緩衝區溢位的定義如下:
緩衝區溢位(buffer overflow),是針對程式設計缺陷,向程式輸入緩衝區寫入使之溢位的內容(通常是超過緩衝區能儲存的最大資料量的資料),從而破壞程式執行、趁著中斷之際並獲取程式乃至系統的控制權。目前OpenBSD、Linux、Windows、Mac OS等作業系統都具有buffer overflow protection(快取溢位保護/記憶體位置重新定向)功能[來源請求],在某種程度上可以保護作業系統,但仍還是有辦法讓溢位的程式碼到正確的位置上。其是作原理是:記憶體跟程序在memory中受到保護。內對外的access memory物件位置會被核心(排程器)隨機定向,使其無法正確溢位。