這樣的文件操作有點玄——文件流學習 ( 三 )

這次,我們要討論一個更加靈活的文件形式:二進制文件


首先,明確概念

文件的基本概念:
C++文件 (file) 分爲兩類:二進制文件和文本文件
文本文件由字符序列組成,也稱ASCII文件。在文本文件中存取的最小信息單位爲字符 (character) 。
二級制文件中存取的最小信息單位爲字節 (Byte) 。
C++把每一個文件都看作一個有序的字節流,每一個文件或者以文件結束符 (EOF) 結束,或者在特定的字節號處結束。


其次,解決一下歷史遺留問題

我們在上一次的學習中提出了這樣的問題:

在Windows平臺下,如果以 " 文本 " 方式打開文件
當讀取文件時,系統會將所有的 \r\n 轉換成 \n
當寫入文件時,系統會將 \n 轉換成 \r\n 寫入,在文本中佔據兩個字節
如果以 " 二進制 " ( binary ) 方式打開文件,則讀&&寫都不會進行這樣的轉換

由這個現象引申出來的問題,實際上非常的深奧
在對文本文件的操作中,我們一般使用標準輸入輸出流進行讀寫

#include<fstream>
#include<iostream>
using namespace std;

int main()
{
	ofstream fout("database.dat",ios::out);
	int num=150;
	char name[]="John Doe";
	fout<<num<<"\n";
	fout<<name<<"\n";
	fout.close();

	ifstream fin("database.dat",ios::in);
	int num2;
	char name2[20];
	fin>>num2;
	fin.ignore();   // ignore the space between num and name
	fin.getline(name2,'\n');   // get the space
	fin.close();

	cout<<"Name: "<<name2<<endl;
	system("pause");
	return 0;
}

// Output
Name: John Doe

這裏強調一下利用seekg直接定位的語法:

  • seekg(n)中的參數n表示指針偏移量:指針從當前位置向後偏移的數量,指針默認爲ios::beg(文件開頭),所以seekg(n)的效果顯示在文件中爲:指針停在第n+1個字符
  • 如果以 " 文本 " 方式打開文件, 當寫入文件時,系統會將 \n 轉換成 \r\n 寫入,在文本中佔據兩個字節
ifstream fin("database.dat",ios::in);
int num2;
char name2[20];
fin.seekg(5);
fin.getline(name2,'\n');
fin.close();

文本文件的討論先到這裏,下面要開始研究令人頭大的二進制文件了

我們先做一個簡單的實驗,將開篇的輸入文件設置爲二進制文件:

#include<fstream>
#include<iostream>
using namespace std;

int main()
{
	ofstream fout("database.dat",ios::binary|ios::out);  //ios::binary
	int num=150;
	char name[]="John Doe";
	fout<<num<<"\n";
	fout<<name<<"\n";
	fout.close();

	ifstream fin("database.dat",ios::binary|ios::in);   //ios::binary
	int num2;
	char name2[20];
	fin.seekg(5);
	fin.getline(name2,'\n');
	fin.close();

	cout<<"Name: "<<name2<<endl;
	system("pause");
	return 0;
}

// Output
Name: ohn Doe

咦?出問題了?

第一個問題:
二級制文件中存取的最小信息單位爲字節 (Byte) ,即8位二進制,那麼150應該是不會超過一個字節。之後name中的每一個字母在計算機中都會佔據一個字節。但是我們在使用標準輸入輸出流對二進制文件進行讀寫時,經過特殊重載後的 << 會將int類型的150變成一個字符串 " 150 ",佔據三個字節。

第二個問題:
明白了第一個問題之後,我們執行 seekg(5) ,指針卻停在了 o,明明在文本文件中正確定位了啊?
打開文件看一下,好像沒什麼問題啊:
在這裏插入圖片描述
怎麼解釋?

如果以 " 二進制 " ( binary ) 方式打開文件
則讀&&寫並不會使 \n\r\n 互相轉化,寫入回車符在二進制文本中僅佔一位
TIP. " \n " 和 " endl " 的效果一樣

到目前爲止,我們都是使用標準輸入輸出流對文件進行讀寫
實際上,二進制文件的處理,有特殊的讀寫操作:read & write

#include<fstream>
#include<iostream>
using namespace std;

int main()
{
	ofstream fout("database.dat",ios::binary|ios::out);  //ios::binary
	int num=150;
	char name[]="John Doe";
	fout.write(reinterpret_cast<const char*>(&num),sizeof(num));
	fout.write(name,sizeof(name));
	fout.close();

	ifstream fin("database.dat",ios::binary|ios::in);  //ios::binary
	int num2;
	char name2[20];
	fin.read(reinterpret_cast<char*>(&num2),sizeof(num2));
	fin.getline(name2,'\n');
	fin.close();

	cout<<"Number: "<<num2<<endl;
	cout<<"Name: "<<name2<<endl;
	system("pause");
	return 0;
}

// Output
Number: 150
Name: John Doe

看一下輸出文件:
在這裏插入圖片描述
可以看到,使用二進制文件標準的read和write之後,int類型的150被強制轉換成了一個字符類型的指針
當肉眼觀察文件時,我們發現了一個亂碼,這個亂碼實際上是原先數據轉換成二進制形式後的結果
關於這兩個特殊的成員函數,我們會在下面的學習中重點講解,不過要牢記:

二進制文件進行讀寫時,推薦使用 readwrite


經過之前亂七八糟的講述,我們實際上已經對二進制文件有了基本的瞭解,下面就不能繼續天馬行空的亂來了:

向二進制文件中寫數據

我們使用write成員函數向二進制文件中寫數據
函數原型:

streamObject.write(char *address, int size);
  • 功能:將內存中某處的若干字節,轉移到輸出流中
  • 第一個參數:常量指針 const char * ,指向內存中的字節
  • 第二個參數:整型 int,限定需要寫入文件的字節大小
outFile.write(reinterpret_cast<const char*>(&number),sizeof(number));
Operator reinterpret_cast

強制轉換指針到另一個無聯繫的類型,常應用於指針和int類型之間的轉化
複製比特,不會造成數據的損失,而是對該對象從位模式上進行重新解釋
函數原型:reinterpret_cast<dataType>(address)
address是輸出數據的起始地址
dataType是希望轉出的數據類型


從二進制文件中讀數據

我們使用read員函數從二進制文件中讀數據

streamObject.read(char *address,int size);
  • 功能:從當前打開的文件中提取若干字節,轉移到對象中
  • 第一個參數:常量指針 const char * ,指向內存中的某對象
  • 第二個參數:整型 int,限定需要讀取文件的字節大小
infile.read(reinterpret_cast<char *>(&value),sizeof(value));
int main() 
{
    int num=0x00636261; //用16進製表示32位int,0x61是字符'a'的ASCII碼 
    int *pnum=&num; 
    char *pstr=reinterpret_cast<char *>(pnum); 
    cout<<"pnum指針的值: "<<pnum<<endl; 
    cout<<"pstr指針的值: "<<static_cast<void *>(pstr)<<endl; 
    //直接輸出pstr會輸出其指向的字符串,這裏的類型轉換是爲了保證輸出pstr的值 
    cout<<"pnum指向的內容: "<<hex<<*pnum<<endl; 
    cout<<"pstr指向的內容: "<<pstr<<endl; 
    return 0;
}

// Output
pnum指針的值: 00FCFF00
pstr指針的值: 00FCFF00
pnum指向的內容: 636261
pstr指向的內容: abc

下面就看一個簡單的栗子:

// ClientData.h
#ifndef CLIENTDATA_H 
#define CLIENTDATA_H 

#include<string> 
using std::string; 

class ClientData{ 
public: 
     // default ClientData constructor 
     ClientData(int =0,string ="",string ="",double =0.0); 
     // accessor functions for accountNumber 
     void setAccountNumber(int); 
     int getAccountNumber() const; 
     // accessor functions for lastName 
     void setLastName(string); 
     string getLastName() const;
     // accessor functions for firstName 
     void setFirstName(string); 
     string getFirstName() const; 
     // accessor functions for balance
     void setBalance(double);
     double getBalance() const;
private: 
     int accountNumber;
     char lastName[15];
     char firstName[10];
     double balance; 35 
}; // end class ClientData

#endif  
// ClientData.cpp 
#include<string> 
#include"ClientData.h" 
using std::string; 

// default ClientData constructor 
ClientData::ClientData(int accountNumberValue,string lastNameValue,string firstNameValue,double balanceValue) { 
    setAccountNumber(accountNumberValue);
    setLastName(lastNameValue); 
    setFirstName(firstNameValue);  
    setBalance(balanceValue); 
} // end ClientData constructor 

int ClientData::getAccountNumber() const {return accountNumber;} 

void ClientData::setAccountNumber(int accountNumberValue) {accountNumber=accountNumberValue;}

string ClientData::getLastName() const {return lastName;} 

void ClientData::setLastName(string lastNameString) { 
     const char *lastNameValue=lastNameString.data();
     int length=lastNameString.size();
     length=(length<15 ? length:14); 
     strncpy(lastName,lastNameValue,length); 
     
     lastName[length]='\0'; // append null character to lastName 
     // 注意這裏一定不要忘記添加結束符
 } 

string ClientData::getFirstName() const {return firstName;}

void ClientData::setFirstName(string firstNameString) {
    const char *firstNameValue=firstNameString.data(); 
    int length=firstNameString.size();
    length=(length<10 ? length:9);
    strncpy(firstName,firstNameValue,length);
    firstName[length]='\0'; // append null character to firstName
    // 注意這裏一定不要忘記添加結束符
 }  

double ClientData::getBalance() const {return balance;}

void ClientData::setBalance(double balanceValue) {balance=balanceValue;}
// Test.cpp 
#include<iostream> 
#include<fstream>   
#include<cstdlib>
#include"ClientData.h"
using namespace std;
 
int main() 
{ 
    ofstream outCredit("credit.dat",ios::out|ios::binary);
    if (!outCredit) { 
         cerr<<"File could not be opened."<<endl;
         exit(1);
    }
    ClientData blankClient;
    for (int i=0;i<100;i++) 
        outCredit.write(reinterpret_cast<const char *>(&blankClient),sizeof(ClientData));                                               
    return 0;
} 

注:

二進制文件不可以存儲指針
因爲在讀取二進制文件裏的指針時,該指針原來指向的內存地址已經被回收了(無意義)
在這裏插入圖片描述


之前我們的讀寫都是順序操作
下面我們要考慮如何定點操作

隨機修改文件中數據

一般的文本文件很難修改已經寫入的數據
但是二進制文件以字節爲單位,就可以實現在一定程度上的隨機修改
特別是文本中存儲的是對象時,文件中的每個對象都會封裝成一個整體,大小是類中定義的內存大小(已知),如此可以計算每個對象所處的字節位置,實現定位

  • 隨機向文件中寫入數據
    • 打開指定文件
      • 聲明一個fstream對象
      • 文件打開方式聲明爲 ios::in|ios::out|ios::binary
    • 使用成員函數 seekp 將寫指針定位到正確位置:(n–1)*sizeof(Class)
    • 使用成員函數 write 寫入數據
#include<iostream> 
#include<iomanip>
#include<fstream>
#include<cstdlib>
#include"ClientData.h"
using namespace std;

int main()
{ 
    int accountNumber;  
    char lastName[15]; 
    char firstName[10]; 
    double balance; 

    fstream outCredit("credit.dat",ios::in|ios::out|ios::binary);
    if (!outCredit) 
    { 
         cerr<<"File could not be opened."<<endl;
         exit(1);
    }
    cout<<"Enter account number (1 to 100, 0 to end input)\n? ";
    ClientData client;
    cin>>accountNumber;
    while (accountNumber>0 && accountNumber<=100) {
        cout<<"Enter lastname, firstname, balance\n? ";
        cin>>setw(15)>>lastName;
        cin>>setw(10)>>firstName;
        cin>>balance;
        client.setAccountNumber(accountNumber);      
        client.setLastName(lastName); 
        client.setFirstName(firstName); 
        client.setBalance(balance);

        // seek position in file of user-specified record
        outCredit.seekp((client.getAccountNumber()-1)*sizeof(ClientData));
        
        // write user-specified information in file
        outCredit.write(reinterpret_cast<const char *>(&client),sizeof(ClientData));

        cout<<"Enter account number\n? ";
        cin>>accountNumber;
    } 
    return 0; 
} 

分塊讀取文件中數據

streamObject.read(char *address,int size);
  • 第一個參數:常量指針 const char * ,指向內存中的某對象
  • 第二個參數:整型 int,限定需要讀取文件的字節大小
#include<iostream> 
#include<iomanip>
#include<fstream>
#include<cstdlib>
#include"ClientData.h"

void outputLine(ostream &output,const ClientData &record) { 
    output<<left<<setw(10)<<record.getAccountNumber() 
          <<setw(16)<<record.getLastName() 
          <<setw(11)<<record.getFirstName() 
          <<setw(10)<<setprecision(2)<<right<<fixed<<showpoint<<record.getBalance()<<endl; 
} 

int main() 
{
    ifstream inCredit("credit.dat",ios::in|ios::binary);
    if (!inCredit) {
        cerr<<"File could not be opened."<<endl;
        exit(1); 
    }
    cout<<left<<setw(10)<<"Account"
        <<setw(16)<<"Last Name"
        <<setw( 11 )<<"First Name"
        <<left<<setw(10)<<right<<"Balance"<<endl;
    ClientData client;
    inCredit.read(reinterpret_cast<char *>(&client),sizeof(ClientData));
    while (inCredit && !inCredit.eof()) {
        if (client.getAccountNumber()!= 0)
            outputLine(cout,client);
        inCredit.read(reinterpret_cast<char *>(&client),sizeof(ClientData));
    }
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章