这次,我们要讨论一个更加灵活的文件形式:二进制文件
文件的基本概念:
C++文件 (file) 分为两类:二进制文件和文本文件
文本文件由字符序列组成,也称ASCII文件。在文本文件中存取的最小信息单位为字符 (character) 。
二级制文件中存取的最小信息单位为字节 (Byte) 。
C++把每一个文件都看作一个有序的字节流,每一个文件或者以文件结束符 (EOF) 结束,或者在特定的字节号处结束。
我们在上一次的学习中提出了这样的问题:
在Windows平台下,如果以 " 文本 " 方式打开文件
当读取文件时,系统会将所有的\r\n
转换成\n
当写入文件时,系统会将\n
转换成\r\n
写入,在文本中占据两个字节
如果以 " 二进制 " ( binary ) 方式打开文件,则读&&写都不会进行这样的转换
由这个现象引申出来的问题,实际上非常的深奥
在对文本文件的操作中,我们一般使用标准输入输出流进行读写
#include
#include
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
#include
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
#include
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
(address)
address是输出数据的起始地址
dataType是希望转出的数据类型
我们使用read员函数从二进制文件中读数据
streamObject.read(char *address,int size);
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
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
#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
#include
#include
#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;
}
注:
之前我们的读写都是顺序操作
下面我们要考虑如何定点操作
一般的文本文件很难修改已经写入的数据
但是二进制文件以字节为单位,就可以实现在一定程度上的随机修改
特别是文本中存储的是对象时,文件中的每个对象都会封装成一个整体,大小是类中定义的内存大小(已知),如此可以计算每个对象所处的字节位置,实现定位
ios::in|ios::out|ios::binary
seekp
将写指针定位到正确位置:(n–1)*sizeof(Class)
write
写入数据#include
#include
#include
#include
#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
#include
#include
#include
#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;
}