檔案輸入/輸出
C++ 提供了以下類,用於執行字元與檔案之間的輸入/輸出操作:
- ofstream: 用於寫入檔案的流類
- ifstream: 用於從檔案中讀取的流類
- fstream: 用於從/向檔案讀取和寫入的流類。
這些類直接或間接地派生自以下類:
istream和
ostream我們已經使用過型別為這些類的物件,例如
cin是一個類的物件
istream和
、cout是一個類的物件
ostream. 因此,我們已經在使用與檔案流相關的類。 事實上,我們可以像已經習慣的那樣使用檔案流
cin和
、cout,唯一的區別是我們需要將這些流與物理檔案關聯起來。 讓我們看一個例子:
|
// 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是一個以 null 結尾的字元序列,型別為
const char *(與字串文字的型別相同),表示要開啟的檔案的名稱,而
mode是一個可選引數,包含以下標誌的組合:
ios::in | 開啟以進行輸入操作。 |
ios::out | 開啟以進行輸出操作。 |
ios::binary | 以二進位制模式開啟。 |
ios::ate | 將初始位置設定在檔案末尾。 如果此標誌未設定為任何值,則初始位置是檔案開頭。 |
ios::app | 所有輸出操作都在檔案末尾執行,將內容附加到檔案的當前內容。 此標誌只能用於為僅輸出操作開啟的流。 |
ios::trunc | 如果為輸出操作開啟的檔案之前已存在,則其先前的內容將被刪除並替換為新內容。 |
所有這些標誌都可以使用按位 OR 運算子(
||)組合。例如,如果我們想以二進位制模式開啟檔案
example.bin以新增資料,我們可以透過以下呼叫成員函式來實現:
open():
1 2
|
ofstream myfile;
myfile.open ("example.bin", ios::out | ios::app | ios::binary);
|
每個
open()類的成員函式
、ofstream,
ifstream和
fstream都有一個預設模式,如果在沒有第二個引數的情況下開啟檔案,則使用該預設模式。
類 | 預設模式引數 |
、ofstream | ios::out |
ifstream | ios::in |
fstream | ios::in | ios::out |
不寫入任何字元。對於
ifstream和
、ofstream這些類,即使傳遞給
ios::in和
ios::out成員函式的第二個引數中沒有包含它們的模式,也會自動分別被假定。
open()成員函式。
僅當在未指定模式引數的任何值的情況下呼叫該函式時,才會應用預設值。 如果使用該引數中的任何值呼叫該函式,則預設模式將被覆蓋,而不是組合。
以二進位制模式開啟的檔案流執行獨立於任何格式考慮因素的輸入和輸出操作。 非二進位制檔案被稱為
文字檔案,並且由於某些特殊字元(如換行符和回車符)的格式設定,可能會發生一些轉換。
由於通常在檔案流物件上執行的第一個任務是開啟檔案,因此這三個類都包含一個建構函式,該建構函式會自動呼叫
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()。此成員函式不帶任何引數,它的作用是重新整理關聯的緩衝區並關閉檔案。
呼叫此成員函式後,流物件可用於開啟另一個檔案,並且該檔案可以再次被其他程序開啟。
如果在物件仍然與開啟的檔案關聯時被銷燬,則解構函式會自動呼叫成員函式
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 << endl;
}
myfile.close();
}
else cout << "Unable to open file";
return 0;
}
|
This is a line.
This is another line. |
最後一個示例讀取一個文字檔案並在螢幕上列印其內容。 我們建立了一個 while 迴圈,使用
getline 逐行讀取檔案。
getline 返回的值是對流物件本身的引用,當將其作為布林表示式進行評估時(如此 while 迴圈中所示),如果流已準備好進行更多操作,則為 true;如果已到達檔案末尾或發生其他錯誤,則為 false。
檢查狀態標誌
除了
good()(檢查流是否已準備好進行輸入/輸出操作)之外,還存在其他成員函式來檢查流的特定狀態(所有這些函式都返回一個 bool 值):
- bad()
- 如果讀取或寫入操作失敗,則返回 true。 例如,如果我們嘗試寫入未開啟以進行寫入的檔案,或者我們嘗試寫入的裝置沒有剩餘空間。
- fail()
- 在與 bad() 相同的情況下返回 true,但在發生格式錯誤的情況下也返回 true,例如,當我們嘗試讀取一個整數時提取一個字母字元。
- eof()
- 如果開啟以進行讀取的檔案已到達末尾,則返回 true。
- good()
- 它是最通用的狀態標誌:在呼叫任何先前函式將返回 true 的相同情況下,它返回 false。
為了重置由我們剛剛看到的任何這些成員函式檢查的狀態標誌,我們可以使用成員函式
clear(),它不帶任何引數。
get 和 put 流指標
所有 i/o 流物件至少有一個內部流指標
ifstream,例如
istream,有一個稱為
get pointer 的指標,該指標指向下一次輸入操作中要讀取的元素。
、ofstream,例如
ostream,有一個稱為
put pointer 的指標,該指標指向要寫入下一個元素的位置。
最後,
fstream從
iostream繼承了 get 和 put 指標(它本身是從
istream和
ostream).
派生的)。可以使用以下成員函式來操作這些指向流中讀取或寫入位置的內部流指標:
tellg() 和 tellp()
這兩個成員函式都沒有引數,並返回成員型別的值
pos_type,它是一個整數資料型別,表示 get 流指標的當前位置(在
tellg的情況下)或 put 流指標的當前位置(在
tellp).
seekg() 和 seekp()
這些函式允許我們更改 get 和 put 流指標的位置。 兩個函式都使用兩種不同的原型進行過載。 第一個原型是
seekg ( position );
seekp ( position );
使用此原型,流指標將更改為絕對位置
position(從檔案開頭開始計數)。 此引數的型別與函式返回的型別相同
tellg和
tellp:成員型別
pos_typepos_type
,它是一個整數值。
這些函式的另一個原型是
seekp ( offset, direction );
使用此原型,get 或 put 指標的位置將設定為相對於引數
direction.
確定的某個特定點的偏移值。offset
的成員型別為,這也是一個整數型別。 並且
directiondirection
的型別為seekdir
,它是一個列舉型別(),用於確定從哪個點開始計數偏移量,並且可以採用以下任何值:
ios::beg | 從流的開頭計算的偏移量 |
ios::cur | 從流指標的當前位置計算的偏移量 |
ios::end | 從流的末尾計算的偏移量 |
以下示例使用我們剛剛看到的成員函式來獲取檔案的大小:
|
// obtaining file size
#include <iostream>
#include <fstream>
using namespace std;
int main () {
long begin,end;
ifstream myfile ("example.txt");
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. |
二進位制檔案
在二進位制檔案中,使用提取和插入運算子(
<<和
>>>> 和 <<)以及像
getline這樣的函式輸入和輸出資料效率不高,因為我們不需要格式化任何資料,並且資料可能不使用文字檔案用於分隔元素的分隔符(如空格、換行符等...)。
檔案流包含兩個專門設計用於按順序輸入和輸出二進位制資料的成員函式:
write和
read。第一個(
writewrite
ostream繼承的
、ofstream)。並且
readread
istream繼承的
ifstream。類
fstream的物件同時具有這兩個成員。他們的原型是
write ( memory_block, size );
read ( memory_block, size );
其中
memory_block的型別為“指向 char 的指標”(
char*),表示儲存讀取的資料元素的位元組陣列的地址,或者從中獲取要寫入的資料元素的位元組陣列的地址。
size引數是一個整數值,指定要從/向記憶體塊讀取或寫入的字元數。
|
// reading a complete binary file
#include <iostream>
#include <fstream>
using namespace std;
ifstream::pos_type size;
char * memblock;
int main () {
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 complete file content is in memory";
delete[] memblock;
}
else cout << "Unable to open file";
return 0;
}
|
the complete file content is in memory |
在此示例中,將讀取整個檔案並將其儲存在記憶體塊中。 讓我們檢查一下這是如何完成的:
首先,使用
ios::ate標誌開啟檔案,這意味著 get 指標將位於檔案末尾。 這樣,當我們呼叫成員
tellg()時,我們將直接獲得檔案的大小。 請注意我們用於宣告變數的型別
size:
1
|
ifstream::pos_type size;
|
ifstream::pos_type是用於緩衝區和檔案定位的特定型別,並且是
file.tellg()返回的型別。 此型別定義為整數型別,因此我們可以對其進行與對任何其他整數值進行的操作相同的操作,並且可以安全地將其轉換為足夠大的另一種整數型別以包含檔案的大小。 對於大小小於 2GB 的檔案,我們可以使用
int:
1 2
|
int size;
size = (int) file.tellg();
|
獲得檔案的大小後,我們請求分配一個足夠大的記憶體塊以容納整個檔案。
1
|
memblock = new char[size];
|
之後,我們繼續將 get 指標設定在檔案的開頭(記住我們開啟檔案時此指標在末尾),然後讀取整個檔案,最後將其關閉。
1 2 3
|
file.seekg (0, ios::beg);
file.read (memblock, size);
file.close();
|
此時,我們可以使用從檔案中獲得的資料進行操作。 我們的程式只是宣告檔案的內容在記憶體中,然後終止。
緩衝區和同步
當我們使用檔案流進行操作時,這些流與型別為
streambuf的內部緩衝區相關聯。 此緩衝區是一個記憶體塊,它充當流和物理檔案之間的中介。 例如,對於
、ofstream,每次呼叫成員函式
put(它寫入單個字元)時,該字元不會直接寫入與該流關聯的物理檔案。 相反,該字元將插入到該流的中間緩衝區中。
重新整理緩衝區時,其中包含的所有資料都將寫入物理介質(如果是輸出流)或僅釋放(如果是輸入流)。 此過程稱為
同步,並在以下任何情況下發生:
- 檔案關閉時:在關閉檔案之前,所有尚未重新整理的緩衝區都會被同步,並且所有待處理的資料都會被寫入或讀取到物理介質。
- 緩衝區滿時:緩衝區具有一定的大小。當緩衝區滿時,它會自動同步。
- 顯式地,使用操縱符:當在流上使用某些操縱符時,會發生顯式同步。這些操縱符是:flush和endl.
- 顯式地,使用成員函式 sync():呼叫流的成員函式sync(),它不接受任何引數,會立即進行同步。 此函式返回一個int等於-1的值,如果流沒有關聯的緩衝區或發生故障。 否則(如果流緩衝區已成功同步),它將返回0.