C++学习-入门到精通【12】文件处理

发布于:2025-06-02 ⋅ 阅读:(27) ⋅ 点赞:(0)

C++学习-入门到精通【12】文件处理



一、文件和流

在内存中数据的存储是临时的。文件是用使数据持久化的,即永久保存数据。计算机将文件存储在辅助存储设备中,比如,磁盘、CD等等。

C++将每个文件看成字符序列。每个文件都以一个文件结束符(end-of-file marker)或以存储在操作系统维护、管理的数据结构中的一个特定字节数作为结尾。

当打开一个文件时,一个对象被创建,并且将一个流关联到这个对象上。与这些对象关联的流提供了程序和特定文件或设备之间的通信通道。比如,cin对象允许程序从键盘或其他设备输入数据,cout对象允许程序将数据输出到屏幕或其他设备。

在这里插入图片描述

文件处理模板类
为了在C++中执行文件处理,必须包含头文件<iostream><fstream>。头文件<fstream>包含了多种流类模板的定义:basic_ifstreambasic_ofstreambasic_fstream

它们的UML类图如下:

在这里插入图片描述

二、创建顺序文件

C++没有在文件上强加任何结构。因此,像“记录”这样的概念在C++文件中是不存在的。所以程序员必须自己设计文件结构来满足应用程序的需要。

下面我们会展示如何一个在文件上强加一个简单的记录结构。

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

int main()
{
	// 创建一个名为 client.txt 的文件,并以输出的方式打开它(“出”相对于内存,所以是将数据写进文件)
	ofstream outClientFile("client.txt", ios::out);

	if (!outClientFile)
	{
		cerr << "File could not be opened" << endl;
		exit(EXIT_FAILURE);
	}

	cout << "Enter the account, name and balance." << endl
		<< "Enter end-of-file to end input.\n? ";

	int account;
	string name;
	double balance;

	while (cin >> account >> name >> balance)
	{
		outClientFile << account << ' ' << name << ' ' << balance << endl;
		cout << "? ";
	}
	outClientFile.close(); // 关闭文件
}

运行结果:

在这里插入图片描述

上面的程序的目的是将数据写入一个文件,所以需要通过创建ofstream对象用来打开文件进行输出。该对象的构造函数有两个参数——文件名文件打开模式。对于一个ofstream对象,文件打开模式可以是ios::out默认——向一个文件输出数据,或者是ios::app——将数据输出到文件的结尾(不会改变文件原来的内容)。

使用ios::out模式打开文件,会出现两种情况,文件本来就存在,此时会将原文件截顶(即所有存在于文件中的内容都将被丢弃)。当成这个文件原本就不存在,像是创建了一个新文件;
如果文件原来不存在,那么就创建一个新文件。

下面给出文件的打开模式:

模式 描述
ios::app 将所有输出数据添加到文件的结尾(只能将数据写到结尾)
ios::ate 将一个文件打开作为输出文件,并移动到文件尾。可以在文件的任何位置写数据。(只是在打开时,将文件指针移到文件尾,之后可以改变其位置)
ios::in 打开一个文件作为输入文件
ios::out 打开一个文件作为输出文件
ios::trunc 丢弃文件的内容
ios::binary 打开一个文件进行二进制(非文本方式),不包含输入或输出权限

上面程序中使用语句ofstream outClientFile("client.txt", ios::out);创建了一个ofstream对象,与打开用来输出的文件client.txt相关联。这建立了一个到文件的“通信通道”。

通过成员函数open打开一个文件

一个ofstream对象可以在没有打开特定文件的情况下被创建,文件可以在之后关联到这个对象。例如,下面的代码,先是创建了一个ofstream的对象,再使用open成员函数打开一个文件并将它关联到一个已存在的ofstream对象上。

ofstream outClientFile;
outClientFile.open("client.txt", ios::out);

使用该成员函数时,文件的打开模式作用相同。

测试一个文件是否被成功打开

在创建了一个ostream对象后尝试打开它时,程序会测试打开操作是否成功。使用重载的ios操纵符成员函数operator!来判定打开操作是否成功。如果在打开操作中,failbitbadbit位中的任何一个被设置了,则该条件返回true。

导致错误发生的原因可能是:尝试打开并读取一个不存在的文件,在没有权限的情况下对文件进行读写操作,或是在打开文件并写入时没有磁盘空间。

如果文件打开失败,输出一条错误信息,并调用函数exit来结束程序。该函数的参数是返回给程序调用环境的。有两种EXIT_SUCCESSEXIT_FAILURE。分别表示正常退出和因错误退出。

将istream对象的引用作为判断条件时,会隐式地调用重载的类型转换函数operator void*将流对象转换成一个指针。输入失败时,会产生一个空指针,之后C++会将这个空指针转换成bool值false,将一个非空指针转换成bool值true。

关闭文件

一旦用户输入文件结束指示符,此时显式的调用成员函数close来关闭ofstream对象。在main函数结束时,也会隐式的调用ofstream对象的析构函数,以此来关闭文件。

但是建议在不使用文件的地方,立刻关闭文件。

三、从顺序文件读取数据

本节将讨论如何顺序地从文件读取数据。直接看下面的代码。

#include <iostream>
#include <fstream>
#include <string>
#include <cstdlib>
#include <iomanip>
using namespace std;

void outputLine(int, const string&, double);

int main()
{
	// 创建一个ifstream对象用来打开一个文件进行输入(进行内存叫做“入”)
	ifstream inClientFile("client.txt", ios::in);

	// 文件打开失败,
	if (!inClientFile)
	{
		cerr << "File could not be opened." << endl;
		exit(EXIT_FAILURE); // 因失败退出程序
	}

	int account;
	string name;
	double balance;

	cout << left << setw(10) << "Account" << setw(13) << "Name"
		<< "Balance" << endl << fixed << showpoint;

	// 从文件中顺序读取数据
	while (inClientFile >> account >> name >> balance)
	{
		// 输出从文件中读取的一行数据
		outputLine(account, name, balance);
	}
	// 显式的关闭文件
	inClientFile.close();
}

void outputLine(int account, const string& name, double balance)
{
	cout << left << setw(10) << account << setw(13) << name
		<< setw(7) << setprecision(2) << right << balance << endl;
}

运行结果:

在这里插入图片描述

打开一个文件用于输入

ifstream类对象的默认打开模式为输入模式。所以可以使用下面的语句打开文件进行输入
ifstream("client.txt");

注意,如果文件的内容不应该被修改,则应该只用输入模式(ios::in)打开文件,这样可以避免意外的修改文件内容,同样这也遵循了最小特权原则。

文件定位指针

为了顺序地从文件中取得数据,程序一般从文件的起始位置开始连续地读取所有数据,直到找到需要的数据为止。istreamostream都提供了成员函数来重定位文件定位指针(文件下一个被读取或写入的字节号)。在istream中,使用的成员函数为seekg(“seek get”);在ostream中,使用的成员函数为seekp(“seek put”)。每个istream对象都有一个"get"指针来指出文件中下一个输入的字节号,每个ostream对象都有一个"put"指针来指出文件中下一个输出的字节号。

inClientFile.seekg(0);将与inClientFile关联的文件定位指针重定位于起始位置(位置0)。

在这里插入图片描述

注意上面的第二个版本的必须使用作用域分辨运算符指定它的作用域,例如:ios::cur

seekp的用法相同:

在这里插入图片描述

如果想要知道当前的指针位置,可以使用成员函数tellgtellp。例如:
long location = fileObject.tellg()。这条语句将"get"文件定位指针的值赋给了变量location。

对之前的程序进行修改:贷款查询程序

下面的程序可以显示相关贷款信息,包括余额为0的客户(不欠公司任何钱的客户)、贷款余额为负的客户(向公司提供贷款的客户)和贷款余额为正的客户(使用公司提供的服务或货物而有欠款的客户)。

#include <iostream>
#include <fstream>
#include <cstdlib>
#include <string>
#include <iomanip>
using namespace std;

// 创建一个枚举类型用于表示不同的账户类型
enum RequestType {
	END, ZERO_BALANCE, CREDIT_BALANCE, DEBIT_BALANCE
};

// 让用户选择输出的需求
int getRequest();

bool shouldDisplay(int , double);

void outputLine(int, const string&, double);

int main()
{
	ifstream inClientFile("client.txt", ios::in);

	if (!inClientFile)
	{
		cerr << "File could not be opened." << endl;
		exit(EXIT_FAILURE);
	}

	int account;
	string name;
	double balance;

	int request = 0;

	do {
		request = getRequest();

		switch (request)
		{
		case ZERO_BALANCE:
			cout << "\nAccounts with zero balances:\n";
			break;
		case CREDIT_BALANCE:
			cout << "\nAccounts with credit balances:\n";
			break;
		case DEBIT_BALANCE:
			cout << "\nAccounts with debit balances:\n";
			break;
		case END:
			break;
		default: // 在getRequest函数中处理了异常情况,所以这里不会存在默认情况
			break;
		}

		if (request == END)
			break;

		// 直到读到文件末尾,读完整个文件
		while (inClientFile >> account >> name >> balance)
		{
			// 判断当前读取的数据是否是需要显示的数据
			if (shouldDisplay(request, balance))
				outputLine(account, name, balance);
		}
		
		cout << "\n";
		// 重置该流的错误状态,为下一次使用作准备
		inClientFile.clear();
		// 将文件定位指针重置到起始位置
		inClientFile.seekg(0);
	} while(1);

	cout << "\nEnd of run.";
}

int getRequest()
{
	int request;

	cout << "Enter request" << endl
		<< " 1 - List accounts with zero balances." << endl
		<< " 2 - List accounts with credit balances." << endl
		<< " 3 - List accounts with debit balances." << endl
		<< " 0 - End of run." << fixed << showpoint;

	do {
		cout << "\n?";
		cin >> request;
	}while(request < END || request > DEBIT_BALANCE);

	return request;
}

bool shouldDisplay(int type, double balance)
{
	if (type == ZERO_BALANCE && balance == 0)
	{
		return true;
	}

	if (type == CREDIT_BALANCE && balance < 0)
	{
		return true;
	}

	if (type == DEBIT_BALANCE && balance > 0)
	{
		return true;
	}

	return false;
}

void outputLine(int account, const string& name, double balance)
{
	cout << left << setw(10) << account << setw(13) << name
		<< setw(7) << right << setprecision(2) << balance << "\n";
}

运行结果:

在这里插入图片描述

四、更新顺序文件

对格式化并写入顺序文件的数据进行修改,可能会有破坏其他数据的风险。例如,根据名字Lois改成Quagmire,则原有的名字在没有破坏文件的情况下是不可能被重写的。
Lois的记录被写到文件中,格式如下:
3 Lois 345.67,现在要将这条记录的名字改成Quagmire,就需要在文件相同的位置用更长的名字重写该记录,则新记录会变成:3 Quagmire 345.67。新的记录比原记录多了4个字符,此时这条新记录就会覆盖掉下一条记录的一部分。

问题在于,在使用流插入运算符<<和流提取运算符>>的格式化输入/输出模式时,字段和记录在大小上可以变化的。这些值在内存内部存储空间大小相同,但是在格式化输出为文本时,却变成了大小不同的字段。因此,格式化的输入/输出模式通常不会用来原地更新记录

像这样的更新操作,通常是使用复制的方法完成的。比如,为了实现前述的名字更改,会将在3 Lois 345.67之前的记录复制到一个新文件中,然后将更新的记录也复制到这个新文件中,最后将该记录之后的记录也复制过来,这种方法要求在更新一条记录时,也要对文件中的每条记录进行处理。如果文件中的多条记录都需要进行更新,那么这种方法是可接受的,否则,额外开销过大。

五、随机存取文件

顺序文件不适合即时存取应用程序,这些应用程序要求必须立即定位某个特定的记录。即时存取可以通过随机存取文件实现。

C++没有将结构强加到文件中,所以应用程序想要使用随机存取文件,就必须自己创建。

可以采用多种技术来创建随机存取文件。而最容易的方法就是要求文件中的所有记录的长度相同。通过运用相同大小的定长记录,程序只需要简单的计算,就可以找出任何一条记录到文件起始位置的精确位置。

在这里插入图片描述
使用这样的定长的结构,可以在不破坏文件中其他数据的情况下将数据插入到一个随机存取文件中。之前存在的数据也可以在不重写整个文件的情况下被更新或删除。

1.创建随机存取文件

ostream的成员函数write从内存中的一个指定位置输出固定数目的字节到指定的流。当流被关联到文件时,函数write在文件中从“put”文件定位指针指定的位置开始写入数据。
istream的成员函数read则将固定数目的字节从一个指定的流输入到内存中指定地址开始的一部分空间。如果流被关联到一个文件,那么函数read在文件中从由“get”文件定位指针所指定的位置读取字节数据。

利用ostream的成员函数write写入字节数据

还记得我们上一章中提到的ostream类的成员函数write吗,我们在将一个数据写入文件中时,除了使用流插入运算符之外,还可以使用这个成员函数来实现。例如:将一个整数写入到文件中
outFile.write(reinterpret_cast<const char*>(&number), sizeof(number));
注意成员函数write的第一个参数必须是一个声明为const的char指针,&number是一个int类型的指针,这里使用reinterpret_cast直接将强制转换成char类型的指针。

reinterpret_cast这个运算符,可以强制的将一个指针类型转换成另一个没有任何关联的指针类型。相当于是重新解释,在编译过程中进行。

注意,使用 reinterpret_cast 这个运算符是很容易导致严重的执行时错误的,因为进行指针类型转换时,编译器不会进行任何检查。

reinterpret_cast这个运算符与编译器相关,所以可移植性较差。

2.修改程序:贷款处理程序

需求如下:
在这里插入图片描述

ClientData.h

#pragma once
#include <string>

class ClientData
{
public:
	ClientData(int = 0, const std::string& = "", 
		const std::string& = "", double = 0.0);

	void setAccountNumber(int);
	int getAccountNumber() const;

	void setLastName(const std::string&);
	std::string getLastName() const;

	void setFirstName(const std::string&);
	std::string getFirstName() const;

	void setBalance(double);
	double getBalance() const;
private:
	int accountNumber;
	char lastName[15];
	char firstName[10];
	double balance;
};

ClientData.cpp

#include "ClientData.h"
using namespace std;

ClientData::ClientData(int accountNumberValue, const string& lastname,
	const string& firstname, double balanceValue)
	: accountNumber(accountNumberValue), balance(balanceValue)
{
	setLastName(lastname);
	setFirstName(firstname);
}

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

void ClientData::setLastName(const string& lastname)
{
	int length = lastname.length();

	// 一个C风格的字符串,最后数组最后一个元素必须放置\0
	length = (length < 15 ? length : 14);

	lastname.copy(lastName, length);
	lastName[length] = '\0'; // 在字符串最后添加一个\0作为字符串结束标志
}
string ClientData::getLastName() const
{
	return lastName;
}

void ClientData::setFirstName(const string& firstname)
{
	int length = firstname.length();

	// 一个C风格的字符串,最后数组最后一个元素必须放置\0
	length = (length < 10 ? length : 9);

	firstname.copy(firstName, length);
	firstName[length] = '\0'; // 在字符串最后添加一个\0作为字符串结束标志
}
string ClientData::getFirstName() const
{
	return firstName;
}

void ClientData::setBalance(double balanceValue)
{
	balance = balanceValue;
}
double ClientData::getBalance() const
{
	return balance;
}

类string的对象是没有统一长度的,因为它们的内存动态分配的以适应不同长度的字符串。这个程序维护的是固定长度的记录,所以类ClientData中客户的姓和名使用的是定长的char数组存储的。

在这个程序执行过程中我们使用二进制的格式ios::binary打开文件。注意必须使二进制模式打开文件,这是因为文件中是没有结构的,我们是通过在内存中的设计结构,然后将存储为这种结构的数据存储到文件中,下次读取时,这些数据仍能还原成这种结构。所以这里的定长是在内存中定长,而内存中的数据是以二进制形式存储的,所以我们必须使用二进制模式打开文件。

测试代码,test.cpp

#include <iostream>
#include <fstream>
#include <iomanip>
#include <cstdlib>
#include "ClientData.h"
using namespace std;

int main()
{
	ofstream outCredit("credit.dat", ios::binary);
	
	if (!outCredit)
	{
		cerr << "File could not be opened." << endl;
		exit(EXIT_FAILURE);
	}

	ClientData blankClient; // 使用默认构造函数创建ClientData对象,每个数据成员都被初始化成0

	// 在文件中保存100条空的记录
	for (int i = 0; i < 100; i++)
	{
		outCredit.write(reinterpret_cast<const char*>(&blankClient), sizeof(ClientData));
	}
}

向随机存取文件写入数据
下面我们向文件credit.dat写入数据,并使用fstream的函数seekpwrite的组合来将数据存储到文件的精确位置。

#include <iostream>
#include <fstream>
#include <cstdlib>
#include "ClientData.h"
using namespace std;

int main()
{
	int accountNumber;
	string lastName;
	string firstName;
	double balance;

	// fstream的对象即可以输入也可输出
	fstream outCredit("credit.dat", ios::in|ios::out|ios::binary);

	if (!outCredit)
	{
		cerr << "File could not be opened." << endl;
		exit(EXIT_FAILURE);
	}

	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 >> lastName >> firstName >> balance;

		client.setAccountNumber(accountNumber);
		client.setLastName(lastName);
		client.setFirstName(firstName);
		client.setBalance(balance);

		// 将put文件定位指针设置到文件中的第 帐号 - 1 个记录的位置
		outCredit.seekp((client.getAccountNumber() - 1) * sizeof(ClientData));
		// 将数据写入文件
		outCredit.write(reinterpret_cast<const char*>(&client), sizeof(ClientData));
		
		cout << "Enter account number\n? ";
		cin >> accountNumber;
	}
}

运行结果:

在这里插入图片描述

此时如果你在图形界面直接打开credit.dat这个文件,里面会全部都是乱码,因为它们是以二进制格式存储的。

只能通过二进制模式读取这个文件,才能得到其他存储的内容(当然如果你能直接将二进制序列以对应的编码方式直接转换成对应的文本信息,那么你也能得到文件中存储的内容)。

上面代码中,我们使用了一个fstream对象来打开文件credit.dat。通过文件打开模式 ios::in、ios::out、ios::binary 的组合,可以按照二进制模式打开,从而进行输入和输出。多种文件打开模式可以通过使用按位或运算符(|)将单独的打开模式组合起来。

提示
使用ios::in|ios::out表示以读写模式打开文件,这种方式打开不会擦除文件的原内容。

擦除文件内容的真正原因是使用了ios::trunc,在打开文件时,单独使用ios::out会隐式的触发ios::trunc模式。

从随机存取文件顺序读取数据
现在我们来试试,如何从上面创建的随机存取文件中读取数据。

测试程序,test.cpp

#include <iostream>
#include <fstream>
#include <cstdlib>
#include <iomanip>
#include "ClientData.h"
using namespace std;

// 输出文件中的一条记录
void outputLine(ostream&, const ClientData&);

int main()
{
	ifstream inCredit("credit.dat", ios::in|ios::binary);

	if (!inCredit)
	{
		cerr << "File could not be opened." << endl;
		exit(EXIT_FAILURE);
	}

	// 输出表头
	cout << left << setw(10) << "Account" << setw(16) << "LastName"
		<< setw(11) << "FirstName" << setw(10) << right << "Balance" << endl;

	ClientData client;

	do {
		// 注意read函数会修改数据,所以将ClientData的指针转换成char*,而不是const char*
		inCredit.read(reinterpret_cast<char*>(&client), sizeof(ClientData));

		if(client.getAccountNumber() != 0)
			outputLine(cout, client);
	}while (inCredit && !inCredit.eof());
	// 当读取到文件末尾或文件读取失败时结束循环
}

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;
}

运行结果:

在这里插入图片描述

六、实例研究:事务处理程序

现在给出一个通过随机存取文件来实现“即时”存取处理的事务处理程序。这个程序维护了一个银行的账户信息。程序可以更新现有的账户、加入新的账户、删除账户,并在文本文件中存储一个格式化的所有当前账户列表。

#include <iostream>
#include <fstream>
#include <iomanip>
#include <cstdlib>
#include "ClientData.h"
using namespace std;

int enterChoice();
void createTextFile(fstream&);
void updateRecord(fstream&);
void newRecord(fstream&);
void deleteRecord(fstream&);
void outputLine(ostream&, const ClientData&);
int getAccount(const char* const);

enum Choices {
	END, PRINT, UPDATE, NEW, DELETE
};

int main()
{
	fstream inOutCredit("credit.dat", ios::in|ios::out|ios::binary);

	if (!inOutCredit)
	{
		cerr << "File could not be opened." << endl;
		exit(EXIT_FAILURE);
	}

	int choice;

	while ((choice = enterChoice()) != END)
	{
		switch (choice)
		{
		case PRINT:
			createTextFile(inOutCredit);
			break;
		case UPDATE:
			updateRecord(inOutCredit);
			break;
		case NEW:
			newRecord(inOutCredit);
			break;
		case DELETE:
			deleteRecord(inOutCredit);
			break;
		default:
			cerr << "Incorrect choice." << endl;
			break;
		}

		inOutCredit.clear(); // 重置错误状态和文件定位指针的位置
	}

	inOutCredit.close();
}

int enterChoice()
{
	cout << "\nEnter your choice" << endl
		<< "1 - store a formatted text file of accounts" << endl
		<< "    called \"print.txt\" for printing" << endl
		<< "2 - update an account" << endl
		<< "3 - add a new account" << endl
		<< "4 - delete an account" << endl
		<< "0 - end program\n? ";

	int menuChoice;
	cin >> menuChoice;
	return menuChoice;
}

// 从二进制文件中将数据写入格式化保存数据的文件
void createTextFile(fstream& readFromFile)
{
	ofstream outPrintFile("print.txt", ios::out);

	if (!outPrintFile)
	{
		cerr << "File could not be opened." << endl;
		exit(EXIT_FAILURE);
	}

	outPrintFile << left << setw(10) << "Account" << setw(16)
		<< "LastName" << setw(11) << "FirstName"
		<< right << setw(10) << "Balance" << endl;

	readFromFile.seekg(0);

	ClientData client;
	readFromFile.read(reinterpret_cast<char*>(&client), sizeof(ClientData));

	while (!readFromFile.eof())
	{
		if (client.getAccountNumber() != 0) // 跳过空记录
		{
			outputLine(outPrintFile, client);
		}

		readFromFile.read(reinterpret_cast<char*>(&client), sizeof(ClientData));
	}
}

void updateRecord(fstream& updateFile)
{
	// 获取要更新的记录的账号
	int accountNumber = getAccount("Enter account to update");

	updateFile.seekg((accountNumber - 1) * sizeof(ClientData));

	ClientData client;
	updateFile.read(reinterpret_cast<char*>(&client), sizeof(ClientData));

	if (client.getAccountNumber() != 0)
	{
		outputLine(cout, client); // 显示要更新的信息

		cout << "\nEnter charge(+) or payment(-): ";
		double transaction; // 存款或取款
		cin >> transaction;

		double oldBalance = client.getBalance();
		client.setBalance(oldBalance + transaction);
		outputLine(cout, client); // 显示更新之后的记录

		updateFile.seekp((accountNumber - 1) * sizeof(ClientData));

		// 将更新的内容写入文本文件
		updateFile.write(reinterpret_cast<const char*>(&client), sizeof(ClientData));
	}
	else
	{
		cerr << "Account #" << accountNumber 
			<< " has no information." << endl;
	}
}

void newRecord(fstream& insertInFile)
{
	int accountNumber = getAccount("Enter new account number");

	// 找到对应的记录
	insertInFile.seekg((accountNumber - 1) * sizeof(ClientData));

	ClientData client;
	// 将数据提取到内存,保存在变量client中
	insertInFile.read(reinterpret_cast<char*>(&client), sizeof(ClientData));

	// 插入的位置,没有记录
	if (client.getAccountNumber() == 0)
	{
		string lastName;
		string firstName;
		double balance;

		cout << "Enter lastname, firstname and balance\n? ";
		// 注意在输入操作中,使用setw操纵符设置提取的字符数,
		// 这里面会包含一个终止符\0的位置,如果设置提取 n 个字符,实际上最多提取 n - 1 个字符
		cin >> setw(15) >> lastName; 
		cin >> setw(10) >> firstName;
		cin >> balance;

		client.setAccountNumber(accountNumber);
		client.setLastName(lastName);
		client.setFirstName(firstName);
		client.setBalance(balance);

		// 将数据写入文本文件
		insertInFile.seekp((accountNumber - 1) * sizeof(ClientData));
		insertInFile.write(reinterpret_cast<const char*>(&client), sizeof(ClientData));
	}
	else
	{
		cerr << "Account #" << accountNumber 
			<< " already contains information." << endl;
	}
}

void deleteRecord(fstream& deleteFromFile)
{
	int accountNumber = getAccount("Enter account to delete");

	// 找到待删除的记录
	deleteFromFile.seekg((accountNumber - 1) * sizeof(ClientData));

	ClientData client;
	deleteFromFile.read(reinterpret_cast<char*>(&client), sizeof(ClientData));

	// 要删除记录不是空记录
	if (client.getAccountNumber() != 0)
	{
		ClientData blankClient; // 创建一个空记录

		deleteFromFile.seekp((accountNumber - 1) * sizeof(ClientData));

		deleteFromFile.write(reinterpret_cast<const char*>(&blankClient), sizeof(ClientData));

		cout << "Account #" << accountNumber << " deleted.\n";
	}
	else
	{
		cerr << "Account #" << accountNumber << " is empty.\n";
	}
}

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

int getAccount(const char* const prompt)
{
	int accountNumber;

	do
	{
		cout << prompt << "(1 - 100): ";
		cin >> accountNumber;
	} while(accountNumber < 1 || accountNumber > 100);

	return accountNumber;
}

运行结果:

选项1:
调用createTextFile函数,
将credit.dat中的数据读取出来,并将它们写入到一个格式化的文本文件中。
print.txt文件的内容如下:
在这里插入图片描述

选项2:
调用updateRecord函数来更新账户,该函数只能更新已存在的记录,所以该函数首先判断指定的记录是否为空。不为空继续进行下一步,显示原来的记录,之后进行余额的修改操作。

在这里插入图片描述

选项3:
调用newRecod函数,创建一个新账户,只能在一个空记录上创建一个账户,所以需要先判断指定的记录是否为空,为空才可以继续下一步。

在这里插入图片描述

选项4:
调用deleteRecord函数来删除一个账户,只有账户存在才能进行删除操作。

在这里插入图片描述

经过上述一系列操作之后,文件(print.txt)内容如下:

在这里插入图片描述

七、对象序列化

大家经过了这一章的学习,应该都知道了文件中只存储字节数据,并没有其他的信息(比如,类型信息)。如果将不同对象的数据保存到同一个文件中,要将它们再次读入内存中时,程序如何区分它们谁是谁(它们的类型是什么)。而对象是一般是没有类型域的(在对象中标记该对象属于哪一类的信息,因为多态性(都使用基类指针/引用来操作,由编译器运行时决定使用什么函数)所以一般没有)。

一些编程语言使用的方法是对象序列化。所谓序列化的对象,是指一个字节序列表示的对象,这个序列不仅包含这个对象的数据,它也包含有关对象类型和对象中存储的数据的类型信息。当序列化的对象被写入文件后,它可以从文件中读出并反序列化,即类型信息可以被用来在内存中重新创建这个对象。


网站公告

今日签到

点亮在社区的每一天
去签到