這次,我們要討論一個更加靈活的文件形式:二進制文件
首先,明確概念
文件的基本概念:
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被強制轉換成了一個字符類型的指針
當肉眼觀察文件時,我們發現了一個亂碼,這個亂碼實際上是原先數據轉換成二進制形式後的結果
關於這兩個特殊的成員函數,我們會在下面的學習中重點講解,不過要牢記:
二進制文件進行讀寫時,推薦使用
read
和write
經過之前亂七八糟的講述,我們實際上已經對二進制文件有了基本的瞭解,下面就不能繼續天馬行空的亂來了:
向二進制文件中寫數據
我們使用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=#
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;
}