檔案輸入/輸出

C++ 提供以下類來執行從檔案輸入或向檔案輸出字元的操作:

  • ofstream: 用於寫入檔案的流類
  • ifstream: 用於從檔案讀取的流類
  • fstream: 用於對檔案進行讀寫操作的流類。

這些類直接或間接地派生自 istreamostream 類。我們已經使用過這些類的物件:cinistream 類的物件,coutostream 類的物件。因此,我們實際上已經在使用與檔案流相關的類了。事實上,我們可以像使用 cincout 一樣使用檔案流,唯一的區別是我們必須將這些流與物理檔案關聯起來。讓我們來看一個例子:

// basic file operations
#include <iostream>
#include <fstream>
using namespace std;

int main () {
  ofstream myfile;
  myfile.open ("example.txt");
  myfile << "Writing this to a file.\n";
  myfile.close();
  return 0;
}
[file example.txt]
Writing this to a file.

這段程式碼建立了一個名為 example.txt 的檔案,並像我們習慣於使用 cout 那樣,向其中插入一個句子,但這裡使用的是檔案流 myfile

但是,讓我們一步一步來。

開啟檔案

通常對這些類的物件執行的第一個操作是將其與一個真實的檔案關聯起來。這個過程被稱為開啟檔案。一個開啟的檔案在程式中由一個(即這些類中的一個物件;在前一個例子中是 myfile)來表示,對此流物件執行的任何輸入或輸出操作都將應用於與之關聯的物理檔案。

為了用流物件開啟一個檔案,我們使用它的成員函式 open

open (filename, mode);

其中 filename 是一個表示要開啟的檔名的字串,mode 是一個可選引數,它是以下標誌的組合:

ios::in為輸入操作(讀取)而開啟。
ios::out為輸出操作(寫入)而開啟。
ios::binary以二進位制模式開啟。
ios::ate將初始位置設定在檔案末尾。
如果不設定此標誌,初始位置是檔案的開頭。
ios::app所有輸出操作都在檔案末尾進行,將內容追加到檔案當前內容的後面。
ios::trunc如果檔案是為輸出操作而開啟的,並且它已經存在,那麼它之前的內容將被刪除,並被新內容替換。

所有這些標誌都可以使用按位或運算子(|)進行組合。例如,如果我們想以二進位制模式開啟檔案 example.bin 來新增資料,我們可以透過以下呼叫成員函式 open 來實現:

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

ofstreamifstreamfstream 類的每個 open 成員函式都有一個預設模式,如果在開啟檔案時沒有提供第二個引數,就會使用這個預設模式:

預設模式引數
、ofstreamios::out
ifstreamios::in
fstreamios::in | ios::out

對於 ifstreamofstream 類,即使傳遞給 open 成員函式的第二個引數不包含 ios::inios::out,它們也會被自動分別假定(這些標誌會被組合)。

對於 fstream,預設值僅在呼叫函式時未指定任何模式引數值的情況下應用。如果呼叫該函式時為該引數指定了任何值,則預設模式將被覆蓋,而不是組合。

二進位制模式開啟的檔案流執行輸入和輸出操作時,不考慮任何格式問題。非二進位制檔案被稱為文字檔案,由於某些特殊字元(如換行符和回車符)的格式化,可能會發生一些轉換。

由於對檔案流執行的第一個任務通常是開啟檔案,這三個類都包含一個建構函式,它會自動呼叫 open 成員函式,並且引數與該成員完全相同。因此,在我們之前的例子中,我們也可以透過以下方式宣告 myfile 物件並執行相同的開啟操作:

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

將物件構造和流開啟合併為一條語句。這兩種開啟檔案的方式都是有效且等價的。

要檢查檔案流是否成功開啟檔案,你可以透過呼叫成員函式 is_open 來實現。如果流物件確實與一個開啟的檔案相關聯,該成員函式返回一個 bool 值為 true,否則返回 false

1
if (myfile.is_open()) { /* ok, proceed with output */ }

關閉檔案

當我們完成對檔案的輸入和輸出操作後,我們應該關閉它,以便通知作業系統,使其資源再次可用。為此,我們呼叫流的成員函式 close。這個成員函式會重新整理相關的緩衝區並關閉檔案。

1
myfile.close();

一旦呼叫了這個成員函式,流物件就可以被重新用來開啟另一個檔案,並且該檔案也再次可被其他程序開啟。

如果一個物件在仍與一個開啟的檔案關聯時被銷燬,其解構函式會自動呼叫成員函式 close

文字檔案

文字檔案流是指在其開啟模式中不包含 ios::binary 標誌的流。這些檔案旨在儲存文字,因此所有輸入或輸出它們的值都可能經歷一些格式轉換,這些轉換不一定與其字面二進位制值相對應。

對文字檔案的寫入操作與我們操作 cout 的方式相同:

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

int main () {
  ofstream myfile ("example.txt");
  if (myfile.is_open())
  {
    myfile << "This is a line.\n";
    myfile << "This is another line.\n";
    myfile.close();
  }
  else cout << "Unable to open file";
  return 0;
}
[file example.txt]
This is a line.
This is another line.

從檔案中讀取也可以用我們操作 cin 的相同方式進行:

// reading a text file
#include <iostream>
#include <fstream>
#include <string>
using namespace std;

int main () {
  string line;
  ifstream myfile ("example.txt");
  if (myfile.is_open())
  {
    while ( getline (myfile,line) )
    {
      cout << line << '\n';
    }
    myfile.close();
  }

  else cout << "Unable to open file"; 

  return 0;
}
This is a line.
This is another line.  

最後一個例子讀取一個文字檔案並將其內容列印到螢幕上。我們建立了一個 while 迴圈,使用 getline 逐行讀取檔案。getline 返回的值是對流物件本身的引用,當它被作為布林表示式求值時(如在這個 while 迴圈中),如果流已準備好進行更多操作,則為 true;如果已到達檔案末尾或發生其他錯誤,則為 false

檢查狀態標誌

存在以下成員函式來檢查流的特定狀態(它們都返回一個 bool 值):

bad()
如果讀寫操作失敗,則返回 true。例如,當我們試圖向一個未以寫入模式開啟的檔案寫入,或者我們試圖寫入的裝置沒有剩餘空間時。
fail()
在與 bad() 相同的情況下返回 true,但當發生格式錯誤時也返回 true,例如當我們試圖讀取一個整數時卻提取了一個字母字元。
eof()
如果為讀取而開啟的檔案已到達末尾,則返回 true
good()
這是最通用的狀態標誌:在呼叫任何前面函式會返回 true 的情況下,它都返回 false。注意,goodbad 並非完全相反(good 一次性檢查更多的狀態標誌)。

成員函式 clear() 可以用來重置狀態標誌。

get 和 put 流定位

所有 I/O 流物件內部都至少保持一個內部位置:

ifstream,像 istream 一樣,保持一個內部的獲取位置 (get position),即下一次輸入操作要讀取的元素的位置。

ofstream,像 ostream 一樣,保持一個內部的放置位置 (put position),即下一個元素必須被寫入的位置。

最後,fstream,像 iostream 一樣,同時保持獲取位置放置位置

這些內部流位置指向流中下一次讀取或寫入操作要執行的位置。這些位置可以使用以下成員函式來觀察和修改:

tellg() 和 tellp()

這兩個沒有引數的成員函式返回一個成員型別為 streampos 的值,該型別表示當前的獲取位置(對於 tellg)或放置位置(對於 tellp)。

seekg() 和 seekp()

這些函式允許改變獲取位置放置位置。這兩個函式都有兩個不同原型的過載。第一種形式是:

seekg ( position );
seekp ( position );

使用這個原型,流指標被更改為絕對位置 position(從檔案開頭算起)。此引數的型別是 streampos,與 tellgtellp 函式返回的型別相同。

這些函式的另一種形式是:

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

使用這個原型,獲取放置位置被設定為一個相對於由引數 direction 確定的特定點的偏移值。offset 的型別是 streamoff。而 direction 的型別是 seekdir,它是一個列舉型別,用於確定偏移量從哪個點開始計算,並且可以取以下任何值:

ios::beg從流的開頭計算偏移量
ios::cur從當前位置計算偏移量
ios::end從流的末尾計算偏移量

下面的例子使用我們剛剛看到的成員函式來獲取檔案的大小:

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

int main () {
  streampos begin,end;
  ifstream myfile ("example.bin", ios::binary);
  begin = myfile.tellg();
  myfile.seekg (0, ios::end);
  end = myfile.tellg();
  myfile.close();
  cout << "size is: " << (end-begin) << " bytes.\n";
  return 0;
}
size is: 40 bytes.

注意我們為變數 beginend 使用的型別:

1
streampos size;

streampos 是用於緩衝區和檔案定位的特定型別,也是 file.tellg() 返回的型別。這種型別的值可以安全地與同類型的其他值相減,也可以轉換為足以容納檔案大小的整數型別。

這些流定位函式使用兩種特殊的型別:streamposstreamoff。這些型別也被定義為流類的成員型別:

型別成員型別描述
streamposios::pos_type定義為 fpos<mbstate_t>
它可以與 streamoff相互轉換,並且可以與這些型別的值進行加減運算。
streamoffios::off_type它是基本整數型別之一(如 intlong long)的別名。

上述每個成員型別都是其非成員等價型別的別名(它們是完全相同的型別)。使用哪一個都無所謂。成員型別更通用,因為它們在所有流物件上都是相同的(即使是使用特殊字元型別的流),但由於歷史原因,非成員型別在現有程式碼中被廣泛使用。

二進位制檔案

對於二進位制檔案,使用提取和插入運算子(<<>>)以及像 getline 這樣的函式來讀寫資料是低效的,因為我們不需要格式化任何資料,而且資料很可能不是按行格式化的。

檔案流包含兩個專門設計用於順序讀寫二進位制資料的成員函式:writeread。第一個(write)是 ostream 的成員函式(由 ofstream 繼承)。而 readistream 的成員函式(由 ifstream 繼承)。fstream 類的物件兩者都有。它們的原型是:

write ( memory_block, size );
read ( memory_block, size );

其中 memory_block 的型別是 char*(指向 char 的指標),表示一個位元組陣列的地址,讀取的資料元素儲存在這裡,或者要寫入的資料元素從這裡獲取。size 引數是一個整數值,指定要從記憶體塊中讀取或寫入的字元數。

// reading an entire binary file
#include <iostream>
#include <fstream>
using namespace std;

int main () {
  streampos size;
  char * memblock;

  ifstream file ("example.bin", ios::in|ios::binary|ios::ate);
  if (file.is_open())
  {
    size = file.tellg();
    memblock = new char [size];
    file.seekg (0, ios::beg);
    file.read (memblock, size);
    file.close();

    cout << "the entire file content is in memory";

    delete[] memblock;
  }
  else cout << "Unable to open file";
  return 0;
}
the entire file content is in memory

在這個例子中,整個檔案被讀取並存儲在一個記憶體塊中。讓我們來看看這是如何完成的:

首先,檔案以 ios::ate 標誌開啟,這意味著獲取指標將被定位在檔案的末尾。這樣,當我們呼叫成員函式 tellg() 時,我們將直接獲得檔案的大小。

一旦我們獲得了檔案的大小,我們就請求分配一個足夠大的記憶體塊來容納整個檔案:

1
memblock = new char[size];

緊接著,我們著手將獲取位置設定在檔案的開頭(記住我們開啟檔案時這個指標在末尾),然後我們讀取整個檔案,最後關閉它:

1
2
3
file.seekg (0, ios::beg);
file.read (memblock, size);
file.close();

此時,我們可以對從檔案中獲取的資料進行操作。但我們的程式只是宣佈檔案內容已在記憶體中,然後就結束了。

緩衝區和同步

當我們操作檔案流時,它們與一個型別為 streambuf 的內部緩衝區物件相關聯。這個緩衝區物件可以代表一個記憶體塊,作為流和物理檔案之間的中介。例如,對於一個 ofstream,每次呼叫成員函式 put(寫入單個字元)時,該字元可能會被插入到這個中間緩衝區,而不是直接寫入到與流關聯的物理檔案中。

作業系統也可能為檔案的讀寫定義了其他層次的緩衝。

當緩衝區被重新整理時,其中包含的所有資料都會被寫入到物理介質中(如果它是一個輸出流)。這個過程被稱為同步,並在以下任何一種情況下發生:

  • 當檔案關閉時: 在關閉檔案之前,所有尚未被重新整理的緩衝區都會被同步,所有待處理的資料都會被寫入或讀取到物理介質。
  • 當緩衝區滿時: 緩衝區有一定的大小。當緩衝區滿時,它會自動同步。
  • 顯式地,使用操縱符: 當在流上使用某些操縱符時,會發生顯式同步。這些操縱符是:flushendl
  • 顯式地,使用成員函式 sync(): 呼叫流的成員函式 sync() 會導致立即同步。這個函式返回一個 int 值,如果流沒有關聯的緩衝區或發生故障,則為-1。否則(如果流緩衝區成功同步),它返回 0
Index
目錄