c++ 文件讀寫總結(streambuf)


在C++ 中引入了流的概念,我們很方便的通過流來讀寫文本數據和二進制數據,那么流對象的數據究竟是怎么存儲的呢,為了搞清這個問題,先來看一看c++ 的 io 體系:

 由圖可以看出,在stream 的實現中,除了虛基類IOS_BASE之外,所有的類內部都有一個streambuf, streambuf 是一個虛基類(不能被實例化,因此所內部包含streambuf(這個虛基類而非其子類)的類也是虛基類),代表流對象內部的緩沖區,就是我們流操作中輸入輸出的內容在內存中的緩沖區。

Streambuf有兩個子類,分別是stringbuf 和 filebuf,這兩個子類可以被實例化,我們常用的文件流和字符串流,內部的緩沖區就是這兩個類。

我們平常使用到的流基本是標准輸入輸出流,文件流和字符串流。在每個流初始化的時候都會初始化相應的streambuf(其實是它的子類)用來緩沖數據。

當我們用文件或者字符串初始化流的時候,流內部會保存該文件和字符串的信息,而在內部實例化一個streambuf用來緩沖數據,些數據時,當緩沖區滿的時候再將數據寫到文件或者字符串,讀數據時當緩沖區沒有數據時從文件或字符串讀數據到緩沖區。

在文件流這種情況下,streambuf 是為了避免大量的IO 操作

在字符串流的情況下,streambuf (其實是套在上面的流對象)是為了提供字符串的格式化讀取和輸出操作(想象字符串是你從鍵盤輸入的數據)

 

所以streambuf 可以看作一塊緩沖區,用來存儲數據,在這種情況下,我們常常在程序中用的 char數組緩沖區是不是可以被替代呢?答案是of course

而且,有了streambuf ,緩沖區的管理和寫入寫出都非常方便,最好的是流對象有復制拷貝等構造函數可以方便參數傳遞等需要拷貝的情景。

但是streambuf 本身是個虛基類,不能實例化,所以要用streambuf 就需要自己繼承streambuf 寫一個新的類出來才能用,這個實現方法最后介紹,好在c++ 標准類庫實現了兩個子類stringbuf 和 filebuf ,所以我們可以選stringbuf 來作為我們的數據緩沖對象(不選filebuf 是因為它的實現和文件緊耦合的,只適合文件流)

流對象有一個構造函數是通過streambuf 來構造:

stringbuf sb;
istream is(&sb);

有了流對象我們就可以在流上進行各種輸入輸出操作,輸入會從緩沖區讀數據,輸出會將數據寫到緩沖區

注意對緩沖區的讀寫一定要注意方法,流符號是格式話輸入輸出,get,put,read,write等是二進制讀寫。

格式化輸入的內容應當格式化讀取,二進制寫入應當二進制讀取否則會出現寫入和讀出數據不一致的問題

格式化寫入一個int 數據時,會將該數據每位分離出來,按照字符編碼寫到緩沖區,例如 int x= 123, 格式化寫入以后緩沖區存以后,緩沖區有三個字節分別存放1,2,3的字符編碼。格式化讀出是相反的過程,將讀到的字符轉成相應的類型的數據

二進制寫入時進行直接的內存拷貝不做任何動作,例如 int x = 123 二進制寫入后(二進制寫時需要取地址,轉成char* 並指出要寫入的字節數,如f.write((char*)&x,sizeof(int))

寫完后緩沖區的數據是0x0000007b,是計算機內存中對123 的內存的完全拷貝

streambuf是C++流(iostream)與流實體(或者叫原始流,文件、標准輸入輸出等)交互的橋梁

# 文件流 fstream <--> filebuf <--> file # 字符串流 stringstream <--> stringbuf <--> string

streambuf內部實現

術語說明:

  • get 相當於 從流中讀取數據
  • put 相當於 寫入數據到流中
  • 字符,C/C++中的char,也可以理解為字節

streambuf內部持有三個用於get的指針gfirst,gnext,glast和三個用於put的指針pfirst,pnext,plast,這些指針分別可以使用eback(),gptr(),egptr()pbase(),pptr(),epptr()函數獲得,在代碼中需要使用這些函數獲取指針,為了方便描述,我直接使用這些指針變量名

下面是其他幾個受保護的成員函數的作用

  • gbump(n) : gnext+=n
  • setg : setg(gfirst, gnext, glast)
  • pbump(n) : pnext+=n
  • setp : setp(pfirst, pnext, plast)

小結:

  • get緩沖區通過setg()設置,setg的三個參數分別對應gfirst,gnext,glast
  • put緩沖區通過setp()設置,setp的兩個參數分別對應pfirst,plast
  • 如果繼承自streambuf的子類不通過setg和setp設置緩沖區,也就是讀寫緩沖區為空,那么這個流可以說是不帶讀緩沖和寫緩沖的流,這時gfirst = gnext = glast = pfirst = pnext = plast = NULL

子類需要override(覆寫)幾個虛函數來封裝具體的流的實現

虛函數(protected)

這些函數有些需要子類實現,來屏蔽不同的流的具體實現,向上提供統一的接口

緩沖區管理

  • setbuf ---------- 設置緩沖區
  • seekoff --------- 根據相對位置移動內部指針
  • seekpos --------- 根據絕對位置移動內部指針
  • sync ------------ 同步緩沖區數據(flush),默認什么都不做
  • showmanyc ------- 流中可獲取的字符數,默認返回0

輸入函數(get)

  • underflow(c) ---- 當get緩沖區不可用時調用,用於獲取流中當前的字符,注意獲取和讀入的區別,獲取並不使gnext指針前移,默認返回EOF
  • uflow() --------- 默認返回underflow(),並使gnext++
  • xsgetn(s, n) ---- 從流中讀取n個字符到緩沖區s中並返回讀到的字符數:默認從當前緩沖區中讀取n個字符,若當前緩沖區不可用,則調用一次uflow()
  • pbackfail ------- 回寫失敗時調用

輸出函數(put)

  • overflow(c) ----- 當put緩沖區不可用時調用,向流中寫入一個字符;當c==EOF時,流寫入結束;與輸入函數的uflow()相對
  • xsputn(s, n) ---- 將緩沖區s的n個字符寫入到流中並返回寫入的字符數;與輸入函數的xsputn相對

緩沖區不可用是指gnext(pnext) == NULL或者gnext(pnext) >= glast(plast)

public函數

緩沖區管理

  • pubsetbuf : setbuf()
  • pubseekoff : seekoff()
  • pubseekpos : seekpos()
  • pubsync : sync()

輸入函數(get)

  • in_avail : (用於get的)緩沖區內還有多少個字符可獲取,緩沖區可用時返回glast-gnext,否則返回showmanyc()
  • snextc : return sbumpc() == EOF ? EOF : sgetc()
  • sbumpc : 緩沖區不可用時返回uflow();否則返回(++gnext)[-1]
  • sgetc : 緩沖區不可用時返回underflow();否則返回*gnext
  • sgetn : xsgetn()
  • sputbackc : 緩沖區不可用時返回pbackfail(c);否則返回*(--gnext)
  • sungetc : 類似於sputbackc,不過默認調用pbackfail(EOF)

輸出函數(put)

  • sputc : (用於put操作的)緩沖區不可用時,返回overflow(c);否則*pnext++ = c,返回pnext
  • sputn : xsputn()

iostream與streambuf的調用關系

下面就iostream常用的幾個函數說明他們的調用關系

  • read(char *s, int n) -> buf.sgetn(s, n)
  • getline() -> buf.sgetc(), buf.snextc(); 首先調用一次sgetc()來判斷當前字符是否為EOF,然后不斷地調用snextc()讀取下一個字符,直到讀到\n
  • peek() -> buf.sgetc()
  • sync() -> buf.pubsync()

總結

  • 在istream對象中,除了read這種一次讀入多個字符的函數外,一般的讀取流的函數(operator>>())、get、getline都是調用snextc()一次讀入一個字符
  • istream的readsome(buf, size)函數本質還是調用了read,大致相當於read(buf, min(in_avail(), size))
  • snextc函數,當緩沖區不可用時會觸發uflow(),uflow()會調用underflow()觸發一次讀取原始流的操作,如果讀到了流的末尾,可以返回EOF;緩沖區可用時直接從緩沖區中讀取一個字符return *gnext++
  • underflow函數的作用是:當讀取緩沖區不足時,從原始流中讀取一段數據並調用setg重新設置gfirst gnext glast三個指針,將讀到的數據緩存起來,並返回當下的字符return *gnext;原始流中沒有數據時(或者說讀到了流的末尾時)返回EOF
  • 只要原始流還可訪問(讀取或寫入),xsgetn與xsputn就需要盡可能的從原始流中讀取(寫入)n個字符。因為有些流比如tcp socket一次可能接收不完所需要的字符數,這就需要循環接收直到收到n個字符為止。
  • [gfirst, glast)永遠是已經從流實體里讀到的數據如果他們不為空的話 

有兩種情況會使一個istream對象的bool轉型為false:讀到EOF(文件結束標志)或遇到一個無效的值(輸入流進入fail狀態)。istream對象的bool轉型為false的情況下,此后的所有讀入動作都是無操作。直到調用istream對象的成員函數clear()來清除該對象的內部狀態。

缺省情況下,輸入操作符丟棄空白符、空格符、制表符、換行符以及回車。如果希望讀入上述字符,或讀入原始的輸入數據,一種方法是使用istream的get()成員函數來讀取一個字符,另一種方法是使用istream的getline()成員函數來讀取多個字符。istream的read(char* addr, streamsize size)函數從輸入流中提取size個連續的字節,並將其放在地址從addr開始的內存中。istream成員函數gcount()返回由最后的get()、getline()、read()調用實際提取的字符數。read()一般多用在讀取二進制文件,讀取塊數據。

輸入流有三個函數來測試流狀態:即bad(),fail()和eof()。ignore()用來拋掉指定個數的緩沖區中的字節。如果bad()為真,代表是遇到了系統級的故障。如果fail()為真,則表示輸入了非法的字符。

 

 

 

 

下面是緩沖區使用的情景:

考慮一個生產者,消費者的問題,線程A 生成的數據,線程B讀取,可以解決的方案如下:

1. 設立全局變量,緩沖數據,A,B都可以訪問(在這種情況下,A 生產的時候要考慮緩沖區是否夠用,B讀取的時候要判斷當前是否有有效數據可讀,而且很難設計一個合理分配內存的緩沖區(想象A生產的數據有時很大,有時很小))

2.網絡通信(TCP,UDP)

3. streambuf 登場,有了streambuf配合stream,  A就像正常操作流一樣往流對象里塞數據,而B 就像正常操作流一樣從流里面讀數據,不用關心其他問題,只要這兩個流的sterambuf 是同一個對象。

上一段代碼:

#include <iostream>
#include <streambuf>
#include <sstream>
#include <fstream>
#include <string>
#include <cstring>
#include <memory>
#include <thread>
using namespace std;
stringbuf buf;
istream in(&buf);
ostream out(&buf);
bool flag = false;
void threadb() 
{
    char data;
    while (true) 
    {
        if (flag) 
        {
            in >> data;
            cout << "thread B recv:" << data << endl;
            flag = false;
        }
    }
}
int main() 
{
    thread consumer(threadb);
    char data;
    while (true) 
    {
        cin >> data;
        out << data;
        flag = true;
    }
    return 0;
}

在特殊的情景下可以實現自己的streambuf類,自己實現的類必須繼承streambuf 類,自定義的streambuf 必須實現overflow,underflow,uflow 等方法,其中overflow在輸出緩沖區不夠用時調用,underflow和uflow在輸入緩沖區無數據時調用,區別是uflow 會讓讀取位置前進一位,而underflow不會。sreambuf 內部維護着六個指針 eback,gptr,egptr,pbase,pptr,epptr.分別指向讀取緩沖區的頭,當前讀取位置,尾,寫緩沖區的頭,當前寫位置,尾(實際上這幾個指針指向同一段緩沖區)

自定義實現方式要注意要在該返回EOF的時候,返回EOF,underflow和uflow都有可能返回EOF,一旦返回了EOF則標志着流結束,之后對流的操作無效。

如下代碼實現了一個自定義的streambuf:

#include <iostream>
#include <streambuf>
#include <sstream>
#include <fstream>
#include <string>
#include <cstring>
#include <memory>
using namespace std;
class mybuf : public streambuf 
{
public:
    enum{ SIZE = 10};
    mybuf() 
    {
        memset(buffer, 'j', 10);
        //buffer[3] = ' ';
        setbuf(buffer, SIZE);
    }
    void log() 
    {
        cout <<hex<<gptr() << endl;
    }
protected:
    int_type overflow( int_type c) 
    {
        cout << "overflow" << endl;
        return c;
    }
    streambuf* setbuf(char* s, streamsize n) 
    {
        setp(s, s + n);
        setg(s, s, s + n);
        return this;
    }
    int_type underflow() override
    {
        cout << "here"<<endl;
        memset(buffer, 'w', 10);
        setg(buffer, buffer, buffer+10);
        return ' ';
    }
    int_type uflow() override
    {
        cout << "uflow" << endl;
        memset(buffer, 'x', 10);
        setg(buffer, buffer, buffer + 10);
        return EOF;
    }
private:
    char buffer[SIZE];
};
int main() 
{
    mybuf buf;
    char test[2000];
    memset(test, 'a', 2000);
    //buf.pubsetbuf(test, 1000);
    string hh;
    string xx;
    istream in(&buf);
    ostream tt(&buf);
    in>>hh;
    cout << hh << endl;
    //tt.write(test, 9);
    in >> xx;
    in.read(test, 11);
    cout<< xx << endl;
    cout << "end" << endl;
    return 0;
}

rdbuf函數有兩種調用方法

basic_streambuf<Elem, Traits> *rdbuf( ) const;

basic_streambuf<Elem, Traits> *rdbuf( basic_streambuf<E, T> *_Sb);

1)無參數。返回調用者的流緩沖指針。

2)參數為流緩沖指針。它使調用者與參數(流緩沖指針)關聯,返回自己當前關聯的流緩沖區指針。

假如我們用C語言寫一個文件復制程序,比如一個mp3文件,我們首先考慮的是C語言的文件輸入輸出功能,其思路是建一個指定大小緩沖區,我們從源文件中循環讀取緩沖區大小的數據,然后寫進目的文件。而在C++中,我們拋棄了這種用字符緩沖區的按字節復制的方法,因為這種方法看起來很繁瑣,而且效率一點也不高。

下面可以對比這兩種方法(程序可以直接執行):

C:

int main()
{
  char buf[256];
  FILE *pf1, *pf2;
  if((pf1 = fopen("1.mp3", "rb")) == NULL)
  {
    printf("源文件打開失敗\n");
    return 0;
  }
  if((pf2 = fopen("2.mp3","wb")) == NULL)
  {
    printf("目標文件打開失敗\n");
    return 0;
  }
  while(fread(buf,1,256,pf1), !feof(pf1))
  {
     fwrite(buf,1,256,pf2);
  }
  fclose(pf1);
  fclose(pf2);
  return 0;
}

在C++中:

using namespace std;
int main()
{
  fstream fin("1.mp3",ios::in|ios::binary);
  if(!fin.is_open())
  {
    cout << "源文件打開失敗" << endl;
    return 0;
  }
  fstream fout("2.mp3",ios::out|ios::binary);
  if(! fin.is_open())
  {
    cout << "目標文件打開失敗!" << endl;
    return 0;
  }
  fout<<fin.rdbuf();
  fin.close();
  fout.close();
  return 0;
}  

看起來是不是清晰多了呢,這就是C++中的流緩沖的威力了,程序通過把源文件的流重定向到關聯到目的文件的流對象,通過 fout<<fin.rdbuf();一句代碼就完成了在C語言中的循環讀寫緩沖區的功能,而且C++中使用的是底層的流緩沖,效率更高


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM