C++ IO流_資料的旅行之路

2022-08-29 12:00:37

1. 前言

程式中的資料總是在流動著,既然是流動就會有方向。資料從程式的外部流到程式內部,稱為輸入;資料從程式內部流到外部稱為輸出。

C++提供有相應的API實現程式和外部資料之間的互動,統稱這類APIIOAPI

是一個形象概念,資料從一端傳遞到另一端時,類似於水一樣在流動,只是流動的不是水,而是資料

概括而言,流物件可連線 2 端,並在兩者之間搭建起一個通道 ,讓資料通過此通道流過來、流過去。

2. 標準輸入輸出流

初學C++時,會接觸 coutcin 兩個流物件。

2.1 簡介

cout稱為標準輸出流物件,其一端連執行緒式,一端連線標準輸出裝置(標準輸出裝置一般指顯示器),cout的作用是把程式中的資料顯示在顯示器上。

除了cout,還有cerr,其作用和 cout相似。兩者區別:

  • cout帶有資料快取功能,cerr不帶快取功能。

    快取類似於蓄水池,輸出時,先快取資料,然後再從快取中輸出到顯示器上。

  • cout輸出程式通用資料(測試,邏輯結果……),cerr輸出錯誤資訊。

另還有一個 clog物件,和 cerr類似,與cerr不同之處,帶有快取功能。

cin 稱為標準輸入流物件,一端連執行緒式,一端連線標準輸入裝置(標準輸入裝置一般指鍵盤),cin用來把標準輸入裝置上的資料輸入到程式中。

使用 coutcin時需要包含 iostream標頭檔案。

#include <iostream>

開啟 iostream 原始碼,可以看到 iostream檔案中包含了另外 2 個標頭檔案:

#include <ostream>
#include <istream>

且在 iostream標頭檔案中可以查詢到如下程式碼:

extern istream cin;		/// Linked to standard input
extern ostream cout;	/// Linked to standard output
extern ostream cerr;	/// Linked to standard error (unbuffered)
extern ostream clog;	/// Linked to standard error (buffered)

coutcerrclogostream類的範例化物件,cinistream 類的範例化物件。

2.2 使用

ostream類過載了<< 運運算元,istream類過載了>>運運算元,可以使用這 2 個運運算元方便、快速地完成輸入、輸出各種型別資料。開啟原始碼,可以檢視到 <<運運算元返回撥用者本身。意味著使用 cout<<資料時,返回 cout本身,可以以鏈式方式進行資料輸出。

#include <iostream>
using namespace std;
int main(int argc, char** argv) {
	string name="果殼"; 
	int age=12;
    //鏈式輸出格式
	cout<<"姓名:"<<name<<"年齡:"<<age; 
	return 0;
}

istream類過載了 >>運運算元,返回撥用者(即 istream 物件)本身,也可以使用鏈式方式進行輸入。

#include <iostream>
using namespace std;
int main(int argc, char** argv) {
	char sex; 
	int age;
    //鏈式輸入
	cin>>sex>>age; 
	return 0;
}

coutcin 流物件的其它函數暫不介紹,繼續本文的重點檔案流。

3. 檔案流

檔案流 API完成程式中的資料和檔案中的資料的輸入與輸出,使用時,需要包含 fstream標頭檔案。

#include <fstream>

3.1 檔案輸入流

ifstreamistream類派生,用來實現把檔案中的資料l輸入(讀)到程式中。

輸入操作對程式而言,也稱為操作。

檔案輸入流物件的使用流程:

3.1.1 建立流通道

使用 ifstream流物件的 open函數建立起程式外部儲存裝置中的檔案資源之間的流通道。

檔案型別分文字檔案和二進位制檔案。

使用之前,瞭解一下 open函數的原型說明。開啟ifstream標頭檔案,可檢視到 ifstream 類中有如下的資訊說明:

template<typename _CharT, typename _Traits>
class basic_ifstream : public basic_istream<_CharT, _Traits>
{
       /**
       *  @brief  Opens an external file.
       *  @param  __s  The name of the file.
       *  @param  __mode  The open mode flags.
       *
       *  Calls @c std::basic_filebuf::open(s,__mode|in).  If that function
       *  fails, @c failbit is set in the stream's error state.
       *
       *  Tip:  When using std::string to hold the filename, you must use
       *  .c_str() before passing it to this constructor.
       */
      void  open(const char* __s, ios_base::openmode __mode = ios_base::in)
      {
		if (!_M_filebuf.open(__s, __mode | ios_base::in))
		  this->setstate(ios_base::failbit);
		else
		  // _GLIBCXX_RESOLVE_LIB_DEFECTS
		  // 409. Closing an fstream should clear error state
		  this->clear();
      }
      #if __cplusplus >= 201103L
      /**
       *  @brief  Opens an external file.
       *  @param  __s  The name of the file.
       *  @param  __mode  The open mode flags.
       *
       *  Calls @c std::basic_filebuf::open(__s,__mode|in).  If that function
       *  fails, @c failbit is set in the stream's error state.
       */
      void  open(const std::string& __s, ios_base::openmode __mode = ios_base::in)
      {
        if (!_M_filebuf.open(__s, __mode | ios_base::in))
          this->setstate(ios_base::failbit);
        else
          // _GLIBCXX_RESOLVE_LIB_DEFECTS
          // 409. Closing an fstream should clear error state
          this->clear();
      }
	#endif
    }

ifstream過載了 open函數,2 個函數引數數量一致,但第一個引數的型別不相同。呼叫時需要傳遞 2 個引數:

  • 第一個引數,指定檔案的路徑。第一個open函數通過 const char* __s型別(字串指標)接受,第二個open函數通過const std::string& __s型別(字串物件)接受。
  • 第二個引數,指定檔案的開啟方式。開啟方式是一個列舉型別,預設是 ios_base::in(輸入)模式。開啟模式如下所示:
enum _Ios_Openmode 
{ 
      _S_app 		= 1L << 0,
      _S_ate 		= 1L << 1,
      _S_bin 		= 1L << 2,
      _S_in 		= 1L << 3,
      _S_out 		= 1L << 4,
      _S_trunc 		= 1L << 5,
      _S_ios_openmode_end = 1L << 16 
 };
 	typedef _Ios_Openmode openmode;
    /// 以寫的方式開啟檔案,寫入的資料追加到檔案末尾
    static const openmode app =		_S_app;
    /// 開啟一個已有的檔案,檔案指標指向檔案末尾
    static const openmode ate =		_S_ate;
    /// 以二進位制方式開啟一個檔案,如不指定,預設為文字檔案方式
    static const openmode binary =	_S_bin;
    /// 以輸入(讀)方式開啟檔案
    static const openmode in =		_S_in;
    /// 以輸出(寫)方式開啟檔案,如果沒有此檔案,則建立,如有此檔案,此清除原檔案中資料
    static const openmode out =		_S_out;
    /// 開啟檔案的時候丟棄現有檔案裡邊的內容
    static const openmode trunc =	_S_trunc;

開啟檔案實現:

#include <iostream>
#include <fstream>
using namespace std;
int main(int argc, char** argv) {
    ifstream inFile;
    //檔案路徑儲存在字元陣列中
    char fileName[50]="d:\\guoke.txt";
    inFile.open(fileName,ios_base::in);   
    //檔案路徑儲存在字串物件中
	string fileName_="d:\\guoke.txt" ;
	inFile.open(fileName_,ios_base::in);   
	return 0;
}

除了直接呼叫 open函數外,還可以使用 ifstream的建構函式,如下程式碼,本質還是呼叫 open函數。

char fileName[50]="d:\\guoke.txt";
//建構函式
ifstream inFile(fileName,ios_base::in);

或者:

string fileName_="d:\\guoke.txt" ;
ifstream inFile(fileName_,ios_base::in);

可以使用ifstreamis_open函數檢查檔案是否開啟成功。

3.1.2 讀資料

開啟檔案後,意味著輸入流通道建立起來,預設情況下,檔案指標指向檔案的首位置,等待讀取操作。

讀或寫都是通過移動檔案指標實現的。

讀取資料的方式:

  • 使用 >> 運運算元。

ifstreamistream的派生類,繼承了父類別中的所有公共函數,如同 cin一樣可以使用 >>運運算元實現對檔案的讀取操作。

cin使用 >> 把標準輸入裝置上的資料輸入至程式。

ifstream 使用 >> 把檔案中的資料輸入至程式。

兩者的資料來源不一樣,目的地一樣。

提前在 guoke.txt檔案中寫入如下內容,也可以用空白隔開數位。

#include <iostream>
#include <fstream>
using namespace std;
int main(int argc, char** argv) {
	//用來儲存檔案中的資料
	int nums[10];
    //檔案輸入流物件
	ifstream inFile;
    //檔案路徑
	char fileName[50]="d:\\guoke.txt";
    //開啟檔案
	inFile.open(fileName,ios_base::in);
	if(inFile.is_open()) {
         //檢查檔案是否正確開啟
		cout<<"檔案開啟成功"<<endl;
		//讀取檔案中的內容
		for(int i=0; i<5; i++){
            //讀取
            inFile>>nums[i];
            //輸出到顯示器
            cout<<nums[i]<<endl;
		}	
	}
	return 0;
}

如上程式碼,把檔案中的 5 個數位讀取到 nums 陣列中。

>>運運算元讀取時,以換行符、空白等符號作為結束符。

  • 使用getgetline函數。

ifstream類提供有 getgetline函數,可用來讀取檔案中資料。get函數有多個過載,本文使用如下的 2 個。getline函數和get函數功能相似,其差異之處後文再述。

//以字元為單位讀取
istream &get( char &ch );
//以字串為單位讀取
istream &get( char *buffer, streamsize num );

先在 D盤使用記事本建立 guoke.txt檔案,並在檔案中輸入以下 2 行資訊:

this is a test
hello wellcome

編寫如下程式碼,使用 get函數以字元型別逐個讀取檔案中的內容。

#include <iostream>
#include <fstream>
using namespace std;
int main(int argc, char** argv) {
	//用來儲存檔案中的資料
	int nums[10];
	ifstream inFile;
	char fileName[50]="d:\\guoke.txt";
	inFile.open(fileName,ios_base::in);
	char myChar;
	if(inFile.is_open()) {
		cout<<"檔案開啟成功"<<endl;
		//以字元為單位讀取資料
		while(inFile.get(myChar)){
			cout<<myChar;
		}		
	}
	return 0;
}
//輸出結果
this is a test
hello wellcome

讀取時,需要知道是否已經達到了檔案的未尾,或者說如何知道檔案中已經沒有資料。

  • 如上使用 get 函數讀取時,如果沒有資料了,會返回false

  • 使用 eof函數。eof的全稱是 end of file, 當檔案指標移動到檔案無資料處時,eof函數返回 true。建議使用此函數。

    while(!inFile.eof()){
    	inFile.get(myChar);
    	cout<<myChar;
    }	
    

使用 get的過載函數以字串型別讀取。

#include <iostream>
#include <fstream>
using namespace std;
int main(int argc, char** argv) {
	//用來儲存檔案中的資料
	int nums[10];
	ifstream inFile;
	char fileName[50]="d:\\guoke.txt";
	inFile.open(fileName,ios_base::in);
	char myChar[100];
	if(inFile.is_open()) {
		cout<<"檔案開啟成功"<<endl;
		while(!inFile.eof() ) {
             //以字串為單位讀取
			inFile.get(myChar,100);
			cout<<myChar<<endl;
             //為什麼要呼叫無參的 get 函數?
			inFile.get();
		}
	}
	return 0;
}

輸出結果:

上述 get函數以字串為單位進行資料讀取,會把讀出來的資料儲存在第一個引數 myChar陣列中,第二個引數限制每次最多讀 num-1個字元。

如果把上述的

inFile.get(myChar,100);

改成

inFile.get(myChar,10);

則程式執行結果如下:

第一次讀了 9 個字元后結束 ,第二次遇到到換行符後結束,第三行讀了 9 個字元后結束,第四行遇到檔案結束後結束 。

為什麼在程式碼要呼叫無參 get函數?

因為get讀資料時會把換行符保留在快取器中,在讀到第二行之前,需要呼叫無參的 get函數提前清除(讀出)快取器。否則後續資料讀不出來。

getlineget函數一樣,可以以字串為單位讀資料,但不會快取換行符(結束符)。如下同樣可以讀取到檔案中的所有內容。

while(inFile.eof()){
    inFile.getline(myChar,100)
	cout<<myChar<<endl;
}
  • 使用 read 函數。

除了getgetline函數還可以使用 read函數。函數原型如下:

istream &read( char *buffer, streamsize num );
#include <iostream>
#include <fstream>
using namespace std;
int main(int argc, char** argv) {
	//用來儲存檔案中的資料
	int nums[10];
	ifstream inFile;
	char fileName[50]="d:\\myinfo.txt";
	inFile.open(fileName,ios_base::in);
	char myChar[100];
	if(inFile.is_open()) {
		cout<<"檔案開啟成功"<<endl;
		inFile.read(myChar,100);
		cout<<myChar; 
	}
	return 0;
}

read一次性讀取到num個位元組或者遇到 eof(檔案結束符)停止讀操作。這點和 getgetline不同,後者以換行符為結束符號。

3.1.3 關閉檔案

讀操作結束後,需要關閉檔案物件。

inFile.close(); 

3.2 檔案輸出流

ofstream稱為檔案輸出流,其派生於ostream,用於把程式中的資料輸出(寫)到檔案中。和使用 ifstream的流程一樣,分 3 步走:

  • 開啟檔案。

使用 ofstream流物件的 open函數(和 ifstreamopen函數引數說明一樣)開啟檔案,因為是寫操作,開啟的模式預設是ios_stream::out,當然,可以指定其它的如ios_stream::app模式。

#include <iostream>
#include <fstream>
using namespace std;
int main(int argc, char** argv) {
	//輸出流物件 
	ofstream outFile;
	char fileName[50]="d:\\guoke.txt";
	outFile.open(fileName,ios_base::out);
	if (outFile.is_open()){
		cout<<"開啟檔案成功"<<endl;	 
	}
	return 0;
}
  • 寫操作和讀操作一樣,有如下幾種方案:
  1. 使用 <<運運算元。
#include <iostream>
#include <fstream>
using namespace std;
int main(int argc, char** argv) {
	//輸出流物件 
	ofstream outFile;
	char fileName[50]="d:\\guoke.txt";
	outFile.open(fileName,ios_base::out);
	if (outFile.is_open()){
		cout<<"開啟檔案成功"<<endl;	 
		for(int i=0;i<10;i++){
            //向檔案中寫入 10 個數位
			outFile<<i;
		} 	
	}
	return 0;
}

輸出結果:

  1. 使用 put write函數。

put函數以字元為單位向檔案中寫入資料,put函數原型如下:

ostream &put( char ch );
#include <iostream>
#include <fstream>
using namespace std;
int main(int argc, char** argv) {
	//輸出流物件 
	ofstream outFile;
	char fileName[50]="d:\\guoke.txt";
	outFile.open(fileName,ios_base::out);
	if (outFile.is_open()){
		cout<<"開啟檔案成功"<<endl;	
		for(int i=0;i<10;i++){
            //寫入 10 個大寫字母
			outFile.put(char(i+65) );	
		} 
	}
	return 0;
}

write可以把字串寫入檔案中,如下為write函數原型:

ostream &write( const char *buffer, streamsize num );

引數說明:

  • 第一個引數:char型別指標。
  • 第二個引數:限制每次寫入的資料大小。
#include <iostream>
#include <fstream>
using namespace std;
int main(int argc, char** argv) {
	//輸出流物件 
	ofstream outFile;
	char fileName[50]="d:\\guoke.txt";
	outFile.open(fileName,ios_base::out);
	char infos[50]="thisisatest";
	if (outFile.is_open()){
		cout<<"開啟檔案成功"<<endl;	 
		outFile.write(infos,50);
	}
	return 0;
}

檔案中內容:

thisisatest

如果把

outFile.write(infos,50);

改成

outFile.write(infos,5);

則檔案中內容為

thisi
  • 關閉資源。

操作完成後,需要呼叫close函數關閉檔案。

outFile.close();

4. 隨機存取檔案

隨機存取指可以根據需要移動二進位制檔案中的檔案指標,隨機讀或寫二進位制檔案中的內容。

隨機存取要求開啟檔案時,指定檔案開啟模式為 ios_base::binary

隨機讀寫分 2 步:

  • 移動檔案指標到讀寫位置。
  • 然後讀或寫。

隨機存取的關鍵是使用檔案指標的定位函數進行位置定位:

gcount() 返回最後一次輸入所讀入的位元組數
tellg() 返回輸入檔案指標的當前位置
seekg(檔案中的位置) 將輸入檔案中指標移到指定的位置
seekg(位移量,參照位置) 以參照位置為基礎移動若干位元組
tellp() 返回輸出檔案指標當前的位置
seekp(檔案中的位置) 將輸出檔案中指標移到指定的位置
seekp(位移量,參照位置) 以參照位置為基礎移動若干位元組

如下程式碼,使用檔案輸出流向檔案中寫入資料,然後隨機定位檔案指標位置,再進行讀操作。

#include<fstream>
#include<iostream>
using namespace std;
int main() {
	int i,x;
	// 以寫的模式開啟檔案
	ofstream outfile("d:\\guoke.txt",ios_base::out | ios_base::binary);
	if(!outfile.is_open()) {
		cout << "open error!";
		exit(1);
	}
	for(i=1; i<100; i+=2)
        //向檔案中寫入資料
		outfile.write((char*)&i,sizeof(int));
	outfile.close();
     
    //輸入流
	ifstream infile("d:\\guoke.txt",ios_base::in|ios_base::binary);
    
	if(!infile.is_open()) {
		cout <<"open error!\n";
		exit(1);
	}
	//定位	
	infile.seekg(30*sizeof(int));
	for(i=0; i<4 &&!infile.eof(); i++) {
         //讀資料
		infile.read((char*)&x,sizeof(int));
		cout<<x<<'\t';
	}
	cout <<endl;
	infile.close();
	return 0;
}

原檔案中內容:

程式碼執行後的執行結果,並沒有輸入檔案中的所有內容。

5. 總結

本文講述了標準輸入、輸出流和檔案流物件。