这样的文件操作有点玄——文件流学习 ( 三 )

这次,我们要讨论一个更加灵活的文件形式:二进制文件


首先,明确概念

文件的基本概念:
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);
  • 功能:从当前打开的文件中提取若干字节,转移到对象中
  • 第一个参数:常量指针 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;
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值