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

news2025/7/21 22:58:29

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


目录

  • C++学习-入门到精通【12】文件处理
    • 一、文件和流
    • 二、创建顺序文件
    • 三、从顺序文件读取数据
      • 文件定位指针
      • 对之前的程序进行修改:贷款查询程序
    • 四、更新顺序文件
    • 五、随机存取文件
      • 1.创建随机存取文件
      • 2.修改程序:贷款处理程序
    • 六、实例研究:事务处理程序
    • 七、对象序列化


一、文件和流

在内存中数据的存储是临时的。文件是用使数据持久化的,即永久保存数据。计算机将文件存储在辅助存储设备中,比如,磁盘、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)内容如下:

在这里插入图片描述

七、对象序列化

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2396566.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

记一次 Starrocks be 内存异常宕机

突发性 be 内存飙高&#xff0c;直至被系统 kill 掉&#xff0c;be 内存如下&#xff1a;其中 starrocks_be_update_mem_bytes 指标打满&#xff0c;重启也是如此 [rootlocalhost bin]# curl -XGET -s http://192.168.1.49:8040/metrics | grep "^starrocks_be_.*_mem_b…

LangChain-结合GLM+SQL+函数调用实现数据库查询(一)

业务流程 实现步骤 1. 加载数据库配置 在项目的根目录下创建.env 文件&#xff0c;设置文件内容&#xff1a; DB_HOSTxxx DB_PORT3306 DB_USERxxx DB_PASSWORDxxx DB_NAMExxx DB_CHARSETutf8mb4 加载环境变量&#xff0c;从 .env 文件中读取数据库配置信息 使用 os.getenv…

2025年渗透测试面试题总结-匿名[校招]安全工程师(甲方)(题目+回答)

安全领域各种资源&#xff0c;学习文档&#xff0c;以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具&#xff0c;欢迎关注。 目录 匿名[校招]安全工程师(甲方) 1. 介绍自己熟悉的渗透领域 2. 编程语言与开发能力 3. 实习工作内容与流程 …

PySide6 GUI 学习笔记——常用类及控件使用方法(地址类QUrl)

文章目录 地址类QUrl主要功能URL 格式介绍常见 scheme&#xff08;协议&#xff09;类型QUrl 类常用方法常用方法示例典型应用场景 地址类QUrl QUrl 是 PySide6.QtCore 模块中的一个类&#xff0c;用于处理和操作 URL&#xff08;统一资源定位符&#xff09;。它可以解析、构建…

任务23:创建天气信息大屏Django项目

任务描述 知识点&#xff1a; Django 重 点&#xff1a; Django创建项目Django视图函数Django路由Django静态文件Django渲染模板 内 容&#xff1a; 使用PyCharm创建大屏项目渲染大屏主页 任务指导 1. 使用PyCharm创建大屏项目。 创建weather项目配置虚拟环境创建ch…

数学分析——一致性(均匀性)和收敛

目录 1. 连续函数 1.1 连续函数的定义 1.2 连续函数的性质 1.2.1 性质一 1.2.2 性质二 1.2.3 性质三 1.2.4 性质四 2. 一致连续函数 2.1 一致连续函数的定义 2.2 一致连续性定理(小间距定理)(一致连续函数的另一种定义) 2.3 一致连续性判定法 2.4 连…

Flutter GridView网格组件

目录 常用属性 GridView使用配置 GridView.count使用 GridView.extent使用 GridView.count Container 实现列表 GridView.extent Container 实现列表 GridView.builder使用 GridView网格布局在实际项目中用的也是非常多的&#xff0c;当我们想让可以滚动的元素使用矩阵…

【深度学习】18. 生成模型:Variational Auto-Encoder(VAE)详解

Variational Auto-Encoder&#xff08;VAE&#xff09;详解 本节内容完整介绍 VAE 的模型结构、优化目标、重参数化技巧及其生成机制。 回顾&#xff1a;Autoencoder&#xff08;自编码器&#xff09; Autoencoder 是一种无监督学习模型&#xff0c;旨在从未标注的数据中学习压…

解决Window10上IP映射重启失效的问题

问题 在实际网络搭建过程中&#xff0c;大家有可能会遇到在局域网范围内&#xff0c;在自己本机上搭建一个网站或者应用时&#xff0c;其他设备通过本机的IP地址无法访问的问题,这个问题可以通过设置IP映射来解决&#xff0c;但是通过netsh interface命令设置的IP映射&#xf…

python h5py 读取mat文件的<HDF5 object reference> 问题

我用python加载matlab的mat文件 mat文件&#xff1a; 加载方式&#xff1a; mat_file h5py.File(base_dir str(N) _nodes_dataset_snr- str(snr) _M_ str(M) .mat, r) Signals mat_file["Signals"][()] Tp mat_file["Tp"][()] Tp_list mat_fil…

linux命令 systemctl 和 supervisord 区别及用法解读

目录 基础与背景服务管理范围配置文件和管理方式监控与日志依赖管理适用场景常用命令对照表实际应用场景举例优缺点对比小结参考链接 1. 基础与背景 systemctl 和 supervisord 都是用于管理和控制服务&#xff08;进程&#xff09;的工具&#xff0c;但它们在设计、使用场景和…

Spring Boot + MyBatis 实现的简单用户管理项目的完整目录结构示例

&#x1f4c1; 示例项目结构&#xff08;基于 Maven&#xff09; user-management/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/example/usermanagement/ │ │ │ ├── controller/ │ │ │ │ └── UserC…

stm32 + ads1292心率检测报警设置上下限

这个项目是在做心率检测的时候一个小伙伴提出来的&#xff0c;今年五一的时候提出来的想法&#xff0c;五一假期的时候没时间&#xff0c;也没心情做这个&#xff0c;就把这个事情搁置了&#xff0c;在月中做工作计划的时候&#xff0c;就把这个小项目排进来了&#xff0c;五一…

项目练习:element ui 的icon放在button的右侧

文章目录 一、需求描述二、左侧实现三、右侧实现 一、需求描述 我们知道&#xff0c;element ui的button一般都会配置一个icon 这个icon默认是放在左侧的。 如何让它放在右侧了&#xff1f; 二、左侧实现 <el-buttontype"primary"plainicon"el-icon-d-arr…

性能诊断工具AWR配置策略与报告内容解析

AWR&#xff08;Automatic Workload Repository&#xff09;是 Oracle 数据库中的一个重要性能诊断工具。AWR 会按照固定的时间间隔自动收集数据库系统的性能统计信息。这些信息涵盖了数据库运行状态的方方面面&#xff0c;像SQL 执行情况、系统资源利用率、等待事件等。AWR抓取…

Tailwind CSS 实战,基于 Kooboo 构建 AI 对话框页面(三):实现暗黑模式主题切换

基于前两篇的内容&#xff0c;为页面添加主题切换功能&#xff0c;实现网站页面的暗黑模式&#xff1a; Tailwind css实战&#xff0c;基于Kooboo构建AI对话框页面&#xff08;一&#xff09;-CSDN博客 Tailwind css实战&#xff0c;基于Kooboo构建AI对话框页面&#xff08;…

MySQL 8.0 OCP 英文题库解析(十一)

Oracle 为庆祝 MySQL 30 周年&#xff0c;截止到 2025.07.31 之前。所有人均可以免费考取原价245美元的MySQL OCP 认证。 从今天开始&#xff0c;将英文题库免费公布出来&#xff0c;并进行解析&#xff0c;帮助大家在一个月之内轻松通过OCP认证。 本期公布试题91~100 试题91…

ADQ36-2通道2.5G,4通道5G采样PXIE

ADQ36是一款高端12位四通道灵活数据采集板&#xff0c;针对高通道数科学应用进行了优化。ADQ36具有以下特性: 4 / 2模拟输入通道每通道2.5 / 5 GSPS7gb/秒的持续数据传输速率两个外部触发器通用输入/输出&#xff08;GPIO&#xff09;ADQ36数字化仪包括固件FWDAQ ADQ36简介 特…

数字创新智慧园区建设及运维方案

该文档是 “数字创新智慧园区” 建设及运维方案,指出传统产业园区存在管理粗放等问题,“数字创新园区” 通过大数据、AI、物联网、云计算等数字化技术,旨在提升园区产业服务、运营管理水平,增强竞争力,实现绿色节能、高效管理等目标。建设内容包括智能设施、核心支撑平台、…

【科研绘图系列】R语言绘制森林图(forest plot)

禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 文章目录 介绍加载R包数据下载导入数据数据预处理画图系统信息介绍 本文介绍使用R语言绘制森林图(forest plot)的方法。首先加载必要的R包(grid、forestploter、openxlsx、stringr),导入并预处…