C++ 檔案操作詳解

likebeta發表於2016-10-24

C++ 通過以下幾個類支援檔案的輸入輸出:

  • ofstream: 寫操作(輸出)的檔案類 (由ostream引申而來)
  • ifstream: 讀操作(輸入)的檔案類(由istream引申而來)
  • fstream: 可同時讀寫操作的檔案類 (由iostream引申而來)

開啟檔案(Open a file)

對這些類的一個物件所做的第一個操作通常就是將它和一個真正的檔案聯絡起來,也就是說開啟一個檔案。被開啟的檔案在程式中由一個流物件(stream object)來表示 (這些類的一個例項) ,而對這個流物件所做的任何輸入輸出操作實際就是對該檔案所做的操作。

要通過一個流物件開啟一個檔案,我們使用它的成員函式open():void open (const char * filename, openmode mode);

這裡filename 是一個字串,代表要開啟的檔名,mode 是以下標誌符的一個組合: ios::in 為輸入(讀)而開啟檔案

  • ios::out 為輸出(寫)而開啟檔案
  • ios::ate 初始位置:檔案尾
  • ios::app 所有輸出附加在檔案末尾
  • ios::trunc 如果檔案已存在則先刪除該檔案
  • ios::binary 二進位制方式

這些識別符號可以被組合使用,中間以”或”操作符(|)間隔。例如,如果我們想要以二進位制方式開啟檔案”example.bin” 來寫入一些資料,我們可以通過以下方式呼叫成員函式open()來實現:

ofstream file;
file.open ("example.bin", ios::out | ios::app | ios::binary);

ofstream, ifstream 和 fstream所有這些類的成員函式open 都包含了一個預設開啟檔案的方式,這三個類的預設方式各不相同: 類 引數的預設方式

  • ofstream ios::out | ios::trunc
  • ifstream ios::in
  • fstream ios::in | ios::out

只有當函式被呼叫時沒有宣告方式引數的情況下,預設值才會被採用。如果函式被呼叫時宣告瞭任何引數,預設值將被完全改寫,而不會與呼叫引數組合。

由 於對類ofstream, ifstream 和 fstream 的物件所進行的第一個操作通常都是開啟檔案,這些類都有一個建構函式可以直接呼叫open 函式,並擁有同樣的引數。這樣,我們就可以通過以下方式進行與上面同樣的定義物件和開啟檔案的操作:

ofstream file ("example.bin", ios::out | ios::app | ios::binary);

兩種開啟檔案的方式都是正確的。

你可以通過呼叫成員函式is_open()來檢查一個檔案是否已經被順利的開啟了:bool is_open();

它返回一個布林(bool)值,為真(true)代表檔案已經被順利開啟,假( false )則相反。

關閉檔案(Closing a file)

當檔案讀寫操作完成之後,我們必須將檔案關閉以使檔案重新變為可訪問的。關閉檔案需要呼叫成員函式close(),它負責將快取中的資料排放出來並關閉檔案。它的格式很簡單:

void close ();

這個函式一旦被呼叫,原先的流物件(stream object)就可以被用來開啟其它的檔案了,這個檔案也就可以重新被其它的程式(process)所有訪問了。

為防止流物件被銷燬時還聯絡著開啟的檔案,解構函式(destructor)將會自動呼叫關閉函式close。

文字檔案(Text mode files)

類ofstream, ifstream 和fstream 是分別從ostream, istream 和iostream 中引申而來的。這就是為什麼 fstream 的物件可以使用其父類的成員來訪問資料。

一般來說,我們將使用這些類與同控制檯(console)互動同樣的成員函式(cin 和 cout)來進行輸入輸出。如下面的例題所示,我們使用過載的插入操作符

// writing on a text file
#include <fstream>
using namespace std;

int main()
{
    ofstream examplefile("example.txt");
    if (examplefile.is_open())
    {
        examplefile << "This is a line.\n";
        examplefile << "This is another line.\n";
        examplefile.close();
    }
    return 0;
}

從檔案中讀入資料也可以用與 cin的使用同樣的方法:

// reading a text file
#include <iostream>
#include <fstream>
#include <cstdlib>
using namespace std;
int main ()
{
    char buffer[256];
    ifstream examplefile("example.txt");
    if (! examplefile.is_open())
    {
        cout << "Error opening file"; exit (1);
    }
    while (!examplefile.eof())
    {
        examplefile.getline(buffer,100);
        cout<<buffer<< endl;
    }
    return 0;
}
//This is a line.
//This is another line.

上面的例子讀入一個文字檔案的內容,然後將它列印到螢幕上。注意我們使用了一個新的成員函式叫做eof ,它是ifstream 從類 ios 中繼承過來的,當到達檔案末尾時返回true 。

狀態標誌符的驗證(Verification of state flags)

除了eof()以外,還有一些驗證流的狀態的成員函式(所有都返回bool型返回值):

bad()

如果在讀寫過程中出錯,返回 true 。例如:當我們要對一個不是開啟為寫狀態的檔案進行寫入時,或者我們要寫入的裝置沒有剩餘空間的時候。

fail()

除了與bad() 同樣的情況下會返回 true 以外,加上格式錯誤時也返回true ,例如當想要讀入一個整數,而獲得了一個字母的時候。

eof()

如果讀檔案到達檔案末尾,返回true。

good()

這是最通用的:如果呼叫以上任何一個函式返回true 的話,此函式返回 false 。

要想重置以上成員函式所檢查的狀態標誌,你可以使用成員函式clear(),沒有引數。

獲得和設定流指標(get and put stream pointers)

所有輸入/輸出流物件(i/o streams objects)都有至少一個流指標:

  • ifstream, 類似istream, 有一個被稱為get pointer的指標,指向下一個將被讀取的元素。
  • ofstream, 類似 ostream, 有一個指標 put pointer ,指向寫入下一個元素的位置。
  • fstream, 類似 iostream, 同時繼承了get 和 put

我們可以通過使用以下成員函式來讀出或配置這些指向流中讀寫位置的流指標:

tellg() 和 tellp()

這兩個成員函式不用傳入引數,返回pos_type 型別的值(根據ANSI-C++ 標準) ,就是一個整數,代表當前get 流指標的位置 (用tellg) 或 put 流指標的位置(用tellp).

seekg() 和seekp()

這對函式分別用來改變流指標get 和put的位置。兩個函式都被過載為兩種不同的原型:

seekg ( pos_type position );
seekp ( pos_type position );

使用這個原型,流指標被改變為指向從檔案開始計算的一個絕對位置。要求傳入的引數型別與函式 tellg 和tellp 的返回值型別相同。

seekg ( off_type offset, seekdir direction );
seekp ( off_type offset, seekdir direction );

使用這個原型可以指定由引數direction決定的一個具體的指標開始計算的一個位移(offset)。它可以是:

  • ios::beg 從流開始位置計算的位移
  • ios::cur 從流指標當前位置開始計算的位移
  • ios::end 從流末尾處開始計算的位移

流指標 get 和 put 的值對文字檔案(text file)和二進位制檔案(binary file)的計算方法都是不同的,因為文字模式的檔案中某些特殊字元可能被修改。由於這個原因,建議對以文字檔案模式開啟的檔案總是使用seekg 和 seekp的第一種原型,而且不要對tellg 或 tellp 的返回值進行修改。對二進位制檔案,你可以任意使用這些函式,應該不會有任何意外的行為產生。

以下例子使用這些函式來獲得一個二進位制檔案的大小:

// obtaining file size
#include <iostream>
#include <fstream>
using namespace std;

int main ()
{
    const char * filename = "example.txt";
    long l,m;
    ifstream file(filename, ios::in|ios::binary);
    l = file.tellg();
    file.seekg(0, ios::end);
    m = file.tellg();
    file.close();
    cout <<"size of "<< filename;
    cout <<" is "<< (m-l)<<" bytes.\n";
    return 0;
}
//size of example.txt is 40 bytes.

二進位制檔案(Binary files)

在二進位制檔案中,使用>,以及函式(如getline)來操作符輸入和輸出資料,沒有什麼實際意義,雖然它們是符合語法的。

文 件流包括兩個為順序讀寫資料特殊設計的成員函式:write 和 read。第一個函式 (write) 是ostream 的一個成員函式,都是被ofstream所繼承。而read 是istream 的一個成員函式,被ifstream 所繼承。類 fstream 的物件同時擁有這兩個函式。它們的原型是:

write ( char * buffer, streamsize size );
read ( char * buffer, streamsize size );

這裡 buffer 是一塊記憶體的地址,用來儲存或讀出資料。引數size 是一個整數值,表示要從快取(buffer)中讀出或寫入的字元數。

// reading binary file
#include <iostream>
#include <fstream>
using namespace std;
int main ()
{
    const char * filename = "example.txt";
    char * buffer;
    long size;
    ifstream file(filename, ios::in|ios::binary|ios::ate);
    size = file.tellg();
    file.seekg(0, ios::beg);
    buffer = new char [size];
    file.read(buffer, size);
    file.close();
    cout <<"the complete file is in a buffer";
    delete[] buffer;
    return 0;
}
//The complete file is in a buffer

快取和同步(Buffers and Synchronization)

當我們對檔案流進行操作的時候,它們與一個streambuf 型別的快取(buffer)聯絡在一起。這個快取(buffer)實際是一塊記憶體空間,作為流(stream)和物理檔案的媒介。例如,對於一個輸出流, 每次成員函式put (寫一個單個字元)被呼叫,這個字元不是直接被寫入該輸出流所對應的物理檔案中的,而是首先被插入到該流的快取(buffer)中。

當快取被排放出來(flush)時,它裡面的所有資料或者被寫入物理媒質中(如果是一個輸出流的話),或者簡單的被抹掉(如果是一個輸入流的話)。這個過程稱為同步(synchronization),它會在以下任一情況下發生:

  • 當檔案被關閉時: 在檔案被關閉之前,所有還沒有被完全寫出或讀取的快取都將被同步。
  • 當快取buffer 滿時:快取Buffers 有一定的空間限制。當快取滿時,它會被自動同步。
  • 控制符明確指明:當遇到流中某些特定的控制符時,同步會發生。這些控制符包括:flush 和endl。

明確呼叫函式sync(): 呼叫成員函式sync() (無引數)可以引發立即同步。這個函式返回一個int 值,等於-1 表示流沒有聯絡的快取或操作失敗

在C++中,有一個stream這個類,所有的I/O都以這個“流”類為基礎的,包括我們要認識的檔案I/O,stream這個類有兩個重要的運算子:

1、插入器(<<) 

向流輸出資料。比如說系統有一個預設的標準輸出流(cout),一般情況下就是指的顯示器,所以,cout

2、析取器(>>)

從流中輸入資料。比如說系統有一個預設的標準輸入流(cin),一般情況下就是指的鍵盤,所以,cin>>x;就表示從標準輸入流中讀取一個指定型別(即變數x的型別)的資料。

在C++中,對檔案的操作是通過stream的子類fstream(file stream)來實現的,所以,要用這種方式操作檔案,就必須加入標頭檔案fstream.h。下面就把此類的檔案操作過程一一道來。

一、開啟檔案

在fstream類中,有一個成員函式open(),就是用來開啟檔案的,其原型是:

void open(const char* filename,int mode,int access);

引數:

  • filename: 要開啟的檔名
  • mode: 要開啟檔案的方式
  • access: 開啟檔案的屬性

開啟檔案的方式在類ios(是所有流式I/O類的基類)中定義,常用的值如下:

  • ios::app: 以追加的方式開啟檔案
  • ios::ate: 檔案開啟後定位到檔案尾,ios:app就包含有此屬性
  • ios::binary: 以二進位制方式開啟檔案,預設的方式是文字方式。兩種方式的區別見前文
  • ios::in: 檔案以輸入方式開啟
  • ios::out: 檔案以輸出方式開啟
  • ios::nocreate: 不建立檔案,所以檔案不存在時開啟失敗
  • ios::noreplace:不覆蓋檔案,所以開啟檔案時如果檔案存在失敗
  • ios::trunc: 如果檔案存在,把檔案長度設為0

可以用“或”把以上屬性連線起來,如ios::out|ios::binary

開啟檔案的屬性取值是:

0:普通檔案,開啟訪問
1:只讀檔案
2:隱含檔案
4:系統檔案

可以用“或”或者“+”把以上屬性連線起來 ,如3或1|2就是以只讀和隱含屬性開啟檔案。

例如:以二進位制輸入方式開啟檔案c:config.sys

fstream file1;
file1.open("c:config.sys",ios::binary|ios::in,0);

如果open函式只有檔名一個引數,則是以讀/寫普通檔案開啟,即:

file1.open("c:config.sys");<=>file1.open("c:config.sys",ios::in|ios::out,0);

另外,fstream還有和open()一樣的建構函式,對於上例,在定義的時侯就可以開啟檔案了:

fstream file1("c:config.sys");

特別提出的是,fstream有兩個子類:ifstream(input file stream)和ofstream(outpu file stream),ifstream預設以輸入方式開啟檔案,而ofstream預設以輸出方式開啟檔案。

ifstream file2("c:pdos.def");//以輸入方式開啟檔案
ofstream file3("c:x.123");//以輸出方式開啟檔案

所以,在實際應用中,根據需要的不同,選擇不同的類來定義:如果想以輸入方式開啟,就用ifstream來定義;如果想以輸出方式開啟,就用ofstream來定義;如果想以輸入/輸出方式來開啟,就用fstream來定義。

二、關閉檔案

開啟的檔案使用完成後一定要關閉,fstream提供了成員函式close()來完成此操作,如:file1.close();就把file1相連的檔案關閉。

三、讀寫檔案

讀寫檔案分為文字檔案和二進位制檔案的讀取,對於文字檔案的讀取比較簡單,用插入器和析取器就可以了;而對於二進位制的讀取就要複雜些,下要就詳細的介紹這兩種方式

1、文字檔案的讀寫

文字檔案的讀寫很簡單:用插入器(>)從檔案輸入。假設file1是以輸入方式開啟,file2以輸出開啟。示例如下:

file2"I Love You";//向檔案寫入字串"I Love You"
int i;
file1>>i;//從檔案輸入一個整數值。

這種方式還有一種簡單的格式化能力,比如可以指定輸出為16進位制等等,具體的格式有以下一些

操縱符 功能 輸入/輸出
dec 格式化為十進位制數值資料 輸入和輸出
endl 輸出一個換行符並重新整理此流 輸出
ends 輸出一個空字元 輸出
hex 格式化為十六進位制數值資料 輸入和輸出
oct 格式化為八進位制數值資料 輸入和輸出
setpxecision(int p) 設定浮點數的精度位數 輸出

比如要把123當作十六進位制輸出:file1<<hex<<123;要把3.1415926以5位精度輸出:file1<<setpxecision(5)<<3.1415926。

2、二進位制檔案的讀寫

①put()

put()函式向流寫入一個字元,其原型是ofstream &put(char ch),使用也比較簡單,如file1.put(‘c’);就是向流寫一個字元’c’。

②get()

get()函式比較靈活,有3種常用的過載形式:

一種就是和put()對應的形式:ifstream &get(char &ch);功能是從流中讀取一個字元,結果儲存在引用ch中,如果到檔案尾,返回空字元。如file2.get(x);表示從檔案中讀取一個字元,並把讀取的字元儲存在x中。

另一種過載形式的原型是: int get();這種形式是從流中返回一個字元,如果到達檔案尾,返回EOF,如x=file2.get();和上例功能是一樣的。

還 有一種形式的原型是:ifstream &get(char *buf,int num,char delim=’n’);這種形式把字元讀入由 buf 指向的陣列,直到讀入了 num 個字元或遇到了由 delim 指定的字元,如果沒使用 delim 這個引數,將使用預設值換行符’n’。例如:

file2.get(str1,127,’A’);//從檔案中讀取字元到字串str1,當遇到字元’A’或讀取了127個字元時終止。

③讀寫資料塊

要讀寫二進位制資料塊,使用成員函式read()和write()成員函式,它們原型如下:

read(unsigned char *buf,int num);
write(const unsigned char *buf,int num);

read() 從檔案中讀取 num 個字元到 buf 指向的快取中,如果在還未讀入 num 個字元時就到了檔案尾,可以用成員函式 int gcount();來取得實際讀取的字元數;而 write() 從buf 指向的快取寫 num 個字元到檔案中,值得注意的是快取的型別是 unsigned char *,有時可能需要型別轉換。

例:

unsigned char str1[]="I Love You";
int n[5];
ifstream in("xxx.xxx");
ofstream out("yyy.yyy");
out.write(str1,strlen(str1));//把字串str1全部寫到yyy.yyy中
in.read((unsigned char*)n,sizeof(n));//從xxx.xxx中讀取指定個整數,注意型別轉換
in.close();out.close();

四、檢測 EOF

成員函式eof()用來檢測是否到達檔案尾,如果到達檔案尾返回非0值,否則返回0。原型是int eof();

例:

if(in.eof())ShowMessage("已經到達檔案尾!");

五、檔案定位

和 C的檔案操作方式不同的是,C++ I/O系統管理兩個與一個檔案相聯絡的指標。一個是讀指標,它說明輸入操作在檔案中的位置;另一個是寫指標,它下次寫操作的位置。每次執行輸入或輸出時, 相應的指標自動變化。所以,C++的檔案定位分為讀位置和寫位置的定位,對應的成員函式是 seekg()和 seekp(),seekg()是設定讀位置,seekp是設定寫位置。它們最通用的形式如下:

istream &seekg(streamoff offset,seek_dir origin);
ostream &seekp(streamoff offset,seek_dir origin);

streamoff定義於 iostream.h 中,定義有偏移量 offset 所能取得的最大值,seek_dir 表示移動的基準位置,是一個有以下值的列舉:

  • ios::beg: 檔案開頭
  • ios::cur: 檔案當前位置
  • ios::end: 檔案結尾

這兩個函式一般用於二進位制檔案,因為文字檔案會因為系統對字元的解釋而可能與預想的值不同。

例:

file1.seekg(1234,ios::cur);//把檔案的讀指標從當前位置向後移1234個位元組
file2.seekp(1234,ios::beg);//把檔案的寫指標從檔案開頭向後移1234個位元組

相關文章