注:这个是博主复习使用的专题,仅适用于自己以及学习过C++知识点的同学

文章目录

前言

一、IO继承家族的认识

二、IO流状态

2.1.  IO 流对象的四种状态标志

2.2. 使用 cin.clear 恢复 cin 的状态为 goodbit

2.3. 通过IO流状态,实现从控制台中将数字读取出来

三、管理输出缓冲区

3.1. 讲解 endl 

3.2. 认识缓冲区

3.3. 触发缓冲区刷新的条件

四、标准IO流

五、文件IO流

5.1. 文件操作步骤

5.2. 以二进制的形式操作文件

5.3. 以文本的形式操作文件

5.4. 比较 app 和 ate 

5.5. ios_base::out | ios_base::trunc 和 ios_base::out 

六、观察下面的问题:

七、string IO流


前言

注:这个是博主复习使用的专题,仅适用于自己以及学习过C++知识点的同学。

思维导图:


一、IO继承家族的认识

C语言是采用面向过程的方式,直接处理输入输出的;而C++语言是通过一族定义在标准库中的类型来处理IO。这些类型支持从设备中读取数据和向设备中写入数据的IO操作,设备可以是文件、控制台窗口等。

我们之前写的代码,都是使用IO类型和对象都是操作char数据的,默认情况下这些对象都是关联到用户的控制台窗口。但是实际中IO类不仅仅是从控制台窗口控制输入输出,还支持文件和string类的IO操作。其次IO类型是使用模版实现的,还支持对wchar_t 数据的输入输出。


二、IO流状态

2.1.  IO 流对象的四种状态标志

(1). goodbit 表示 流没有错误。

(2). eofbit 表示流到达文件结束。

(3). failbit 表示IO操作失败,通常情况下,failbit 一旦被设置了,流是可以恢复的,并且继续使用。

(4). badbit 表示流崩溃了出现了系统级错误。通常情况下,badbit 一旦被设置了,流就无法使用。

(5). 如果到达文件结束为止,eofbit和failbit都会被置位,如果想再次读取当前位置,可以恢复流的状态,同时重置一个文件指针位置、

int main()
{
	cout << cin.good() << endl;	 // 1
	cout << cin.eof() << endl;	// 0
	cout << cin.bad() << endl;	// 0
	cout << cin.fail() << endl << endl;	//0
	return 0;
}

下面代码运行,输入 x ,此时就会报错。

int main()
{
	int i = 0;
	cin >> i;
	cout << cin.good() << endl;	 // 0
	cout << cin.eof() << endl;	 // 0
	cout << cin.bad() << endl;	 // 0
	cout << cin.fail() << endl << endl; // 1	
    return 0;
}

2.2.  使用 cin.clear 恢复 cin 的状态为 goodbit

可以使用 clear() 将指定的  IO流的状态设置为 goodbit。

int main()
{
	int i = 0;
	cin >> i;
	cout << cin.good() << endl;	 // 0
	cout << cin.eof() << endl;   // 0
	cout << cin.bad() << endl;	// 0
	cout << cin.fail() << endl << endl; // 1	

	cin.clear();
	cout << cin.good() << endl;    // 1
	cout << cin.eof() << endl;    // 0
	cout << cin.bad() << endl;    // 0
	cout << cin.fail() << endl << endl; // 0
    return 0;
}

但是我们在后面继续输入 cin >> i 这个指令,在打印这四个状态,我们会发现,failbit 还是1。

这是因为 之前写入到控制台的内容,没有丢弃。

解决方案一:丢弃 get()

解决方案二:读取下一个数据 peek()

2.3. 通过IO流状态,实现从控制台中将数字读取出来

int main()
{
	int i;
	// 要求从控制台中将数字读取出来
	// 控制台结束需要 Ctrl+z
	while (cin.peek()!=EOF)
	{
		cin >> i;// 用于从控制台(或输入流)读取数据并存储到变量 i 
		if (cin.fail())  
		{
			cin.clear();
			char ch = cin.peek();
			while (ch!=EOF && !(ch >= '0' && ch <='9'))
			{
				cin.get(); // 丢弃数据
				ch = cin.peek();  // 查看下一个字符
			}
		}
		else
		{
			cout << i << endl;
		}
	}
	return 0;
}


三、管理输出缓冲区

3.1. 讲解 endl 

之前我们学习 endl,得知 是一个换行操作,这次我们在重新讲解,把内容写入文件,更加直观。

步骤一:写代码

#include<fstream>
void func(ostream& os)
{
	os << "hello world";
	os << "hello bit";
	system("pause"); // 暂停
	os << endl;
	system("pause"); // 暂停
}
int main()
{
	ofstream ofs("test.txt");
	func(ofs);
	return 0;
}

运行到 第一个 system("pause"); 新创建的 test.txt 文件中是没有数据的,然后第二个 system("pause"); ,就有数据了,并且有换行操作。

第二步,将 endl 改为 \n 。

void func(ostream& os)
{
	os << "hello world";
	os << "hello bit";
	system("pause"); // 暂停
	os << "\n";
	system("pause"); // 暂停

}
int main()
{
	ofstream ofs("test.txt");
	func(ofs);
	return 0;
}

运行到 第一个 system("pause"); 新创建的 test.txt 文件中是没有数据的,然后第二个 system("pause"); 两个都没有数据。说明 endl 不等同于 \n。

第三步:查官网

最后一行,写到 等价于 os.put('\n') 和 os.flush() 两个操作,而 os.flush() 的作用是 刷新缓冲区。

并且 endl 是一个函数,<< 重载了。

第四步:验证 os.flush()

#include<fstream>
void func(ostream& os)
{
	os << "hello world";
	os << "hello bit";
	system("pause"); // 暂停
	os << flush;
	system("pause"); // 暂停

}
int main()
{
	ofstream ofs("test.txt");
	func(ofs);
	return 0;
}

最后的结果就是,只有刷新,没有换行

3.2. 认识缓冲区

任何输出流都管理者一个缓冲区,用来保存程序写的数据。执行到 os << "hello world"; 字符串可能就立即输出,也可能被操作系统保存在缓冲区中,随后再输出。有了缓冲区机制,操作系统就可能将多个输出操作组合成为一个单一的系统级写操作。因为设备的写操作通常是很耗时的,允许操作系统将多个输出操作组合为单一的设备写操作,会带来很大的性能提升。

3.3. 触发缓冲区刷新的条件

(1) 程序正常结束

(2) 缓冲区满了

(3) 输出了操作符 endl 或者 flush 会立即刷新缓冲区 (上面的例子)

(4) 使用操作符unitbuf设置流的内部状态,来清空缓冲区。

为 str 流设置 unitbuf 格式标志,被设置为,关联的缓冲区会在每次插入操作后被刷新;也可以通过 nounitbuf 不强制在每次插入后刷新缓冲区。

#include<fstream>
void func(ostream& os)
{
	os << "hello world";
	system("pause"); // 暂停
	os << "hello bit";
	system("pause"); // 暂停
}
int main()
{
	ofstream ofs("test.txt");
	ofs << unitbuf;
	func(ofs);
	return 0;
}

每次运行到 system("pause"); 就有数据。

(5) 一个输出流关联到另一个流时,当这个流读写时,输出流会立即刷新缓冲区。

tie可以支持跟其他流绑定和解绑。

绑定:cin.tie()

#include<fstream>
void func(ostream& os)
{
	os << "hello world";
	system("pause"); // 暂停
	os << "hello bit";
	system("pause"); // 暂停
	int i;
	cin >> i;
	os << "hello cat";
}
int main()
{
	ofstream ofs("test.txt");
	//cin绑定到ofs,cin进行读时,会刷新ofs的缓冲区
	cin.tie(&ofs);
	func(ofs);
	int i;
	cin >> i;
	return 0;
}

连续两次运行到 system("pause"); 都是没有数据的,运行到 cin>>i,才有数据。

#include<fstream>
void func(ostream& os)
{
	os << "hello world";
	system("pause"); // 暂停
	os << "hello bit";
	system("pause"); // 暂停
	int i;
	cin >> i;
	cin.tie(nullptr);
	os << "hello cat";
}
int main()
{
	ofstream ofs("test.txt");
	//cin绑定到ofs,cin进行读时,会刷新ofs的缓冲区
	cin.tie(&ofs);
	func(ofs);
	int i;
	cin >> i;
	return 0;
}

运行到 func 的 cin >> i,文件中有 "hello worldhello bit" func结束之后,不用到 main的 system("pause"); 就有数据。


四、标准IO流

C++标准库提供了4个全局流对象(cin、cout、cerr、clog):

  1. 使用cout进行标准输出,即数据从内存流向控制台(显示器)。
  2. 使用cin进行标准输入,即数据通过键盘输入到程序中。
  3. 使用cerr进行标准错误的输出。
  4. 使用clog进行日志的输出。

        C++标准IO流前面已经使用得比较较多了,C++标准IO流默认是关联到控制台窗口的。cin是istream类型全局对象,cout/cerr/clog是ostream类型的全局对象,内置类型这两个类都直接进行了重载实现,所以可以直接使用,自定义类型就需要我们自己重载<<和>>运算符。

        ostream和istream是不支持拷贝的,只支持移动(外部不能使用,因为是保护成员)。

istream的cin对象支持转换为bool值,进行条件逻辑判断,⼀旦被设置了badbit或failbit标志位,

就返回false,如果是goodbit就返回true。

        ostream和istream还有不少其他接口,实践中相对用得比较少,需要时大家查查文档

#include<iostream>
#include<fstream>
#include<string>
using namespace std;
int main()
{
	int i = 0, j = 1;

	// 持续的输入,要结束需要输入Ctrl+Z换行,Ctrl+Z用于告诉程序输入已经完成,类似于在
	// 文件末尾添加一个标记。
	// istream& operator>>(int i),>>运算符重载的返回值是istream对象,istream对象可以
	// 调用operator bool转换为bool值
	// 本质在底层是将cin的eofbit和failbit标志位设置了,cin调用operator bool函数语法逻
	// 辑上实现转换为bool值
	while (cin >> i >> j)
	{
		cout << i << ":" << j << endl;
	}
	cout << cin.good() << endl;
	cout << cin.eof() << endl;
	cout << cin.bad() << endl;
	cout << cin.fail() << endl << endl;

	// 流一旦发生错误就不能再用了,清理重置一下再能使用
	cin.clear();
	string s;
	while (cin >> s)
	{
		cout << s << endl;
	}
}


五、文件IO流

5.1. 文件操作步骤

C++根据文件内容的数据格式将文件分为二进制文件和文本文件,采用文件流对象操作文件的一般步骤如下:
1、定义一个文件流对象。
操作文件的类有以下三个:

对应操作场景
ofstream 只写
ifstream 只读
fstream 读+写

2、使用文件流对象的成员函数打开一个磁盘文件,使得文件流对象和磁盘文件之间建立联系。
文件常见的打开方式如下:

打开方式 功能
in 以读的方式打开文件
out 以写的方式打开文件
binary 以二进制方式对文件进行操作
ate 追加内容(可以更改文件指针)
app 在文件末尾追加内容(不能修改指针)
trunc 先将文件内容清空再打开文件

3、使用提取和插入运算符对文件进行读写操作,或使用成员函数进行读写。
对文件进行提取和插入操作的常用成员函数:

成员函数 功能
put 插入一个字符到文件
write 插入一段字符到文件
get 从文件中提取字符
read 从文件中提取多个字符
tellg 获取当前字符在文件当中的位置
seekg 设置对文件进行操作的设置
>>运算符重载 将数据形象地以“流”的形式进行输入
<<运算符重载 将数据形象地以"流"的形式进行输出

4、关闭文件。

文件流打开后如果需要可以主动调用close函数关闭,也可以不关闭,因为流对象析构函数中会关闭。

5.2. 以二进制的形式操作文件

以二进制的形式对文件进行写入操作:

#include <fstream>
int main()
{
	ifstream ifs("C:\\Users\\Lenovo\\Desktop\\Reactor.png",
			ios_base::in | ios_base::binary);
	ofstream ofs("C:\\Users\\Lenovo\\Desktop\\Reactor-copy.png",
		ios_base::out | ios_base::binary);
	int n = 0;
	while (ifs && ofs)
	{
		char ch = ifs.get();
		ofs << ch;
		++n;
	}
	cout << n << endl; // 字节大小
	cout << ifs.good() << endl;
	cout << ifs.eof() << endl;
	cout << ifs.fail() << endl;
	cout << ifs.bad() << endl << endl;
	return 0;
}

5.3. 以文本的形式操作文件

以文本的形式对文件进行写入操作:

#include<fstream>
int main()
{
	ofstream ofs("test.txt");
	// 字符串和字符写入
	ofs.put('x');
	ofs.write("hello\nworld",8); // 还可以指定写入字符个数

	// 也可以使用 << 进行写入
	ofs << "222222" << endl;
	int x = 12;
	double y = 3.3;
	ofs << x << endl;
	ofs << y << endl;
    ofs.close();
	return 0;
}

以文本的形式对文件进行读取操作:

#include<fstream>
#include<string>
using namespace std;
int main()
{
	ifstream ifile; //定义文件流对象
	ifile.open("test.txt"); //以读取的方式打开test.txt文件
	ifile.seekg(0, ifile.end); //跳转到文件末尾
	int length = ifile.tellg(); //获取当前字符在文件当中的位置,即文件的字符总数
	ifile.seekg(0, std::ios_base::beg); //重新回到文件开头
	std::string data;
	data.resize(length);
	ifile.read(&data[0], length); //将文件当中的数据全部读取到字符串data当中
	cout << length << endl;
	cout << data.c_str() << endl;
	ifile.close(); //关闭文件
}

注意: 使用ofstream类对象的open函数时,若不指定打开方式,则默认以写的方式打开文件;使用ifstream类对象的open函数时,若不指定打开方式,则默认以读的方式打开文件;使用fstream类对象的open函数时,若不指定打开方式,则默认以写+读的方式打开文件。

5.4. 比较 app 和 ate 

1. app 不能移动文件指针,只能在文件末尾追加
 

int main()
{
	ofstream ofs("test.txt");
	// 字符串和字符写入
	ofs.put('x');
	ofs.write("hello\nworld", 8); // 还可以指定写入字符个数

	// 也可以使用 << 进行写入
	ofs << "222222" << endl;
	int x = 12;
	double y = 3.3;
	ofs << x << endl;
	ofs << y << endl;
	// 关闭文件流
	ofs.close();

	cout << ofs.fail() << endl; // 0
	// app 不能移动指针,只能文件结尾追加
	ofs.open("test.txt", ios_base::out | ios_base::app); 
	cout << ofs.fail() << endl; // 0
	ofs << "11111" << endl;
	// 换位置
	long pos = ofs.tellp(); // 返回输出流中当前字符的位置 -- 在末尾  
	ofs.seekp(pos - 7);
	cout << ofs.fail() << endl; // 0
	ofs << x << endl;
	ofs << y << endl;
	return 0;
}

2. ate可以移动文件指针,进行追加

int main()
{
	ofstream ofs("test.txt");
	// 字符串和字符写入
	ofs.put('x');
	ofs.write("hello\nworld", 8); // 还可以指定写入字符个数
	// 也可以使用 << 进行写入
	ofs << "222222" << endl;
	int x = 66;
	double y = 3.3;
	ofs << x << endl;
	ofs << y << endl;
	// 关闭文件流
	ofs.close();

	// ate可以移动文件指针在文件尾部写入
	ofs.open("test.txt", ios_base::out | ios_base::ate);
	cout << ofs.fail() << endl; // 0
	ofs << "11111" << endl;
	// 换位置
	ofs.seekp(0, ios_base::beg); //  从起始位置开始,移动0个偏移量
	cout << ofs.fail() << endl; // 0
	ofs << x << endl;
	ofs << y << endl;
	return 0;
}

5.5. ios_base::out | ios_base::trunc 和 ios_base::out 

两个没有任何区别,ios_base::out | ios_base::trunc 可以显式地、明确地告诉编译器你的意图


六、观察下面的问题:

#include<fstream>
class Date
{
	friend ostream& operator << (ostream& out, const Date& d);
	friend istream& operator >> (istream& in, Date& d);
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};

istream& operator >> (istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
	return in;
}

ostream& operator << (ostream& out, const Date& d)
{
	out << d._year << " " << d._month << " " << d._day << endl;
	return out;
}
// 客户端
struct ServerInfo
{
	// 二进制读写时,这里不能用string,否则写到文件中的是string中指向字符数组的指针
	// 若string对象析构后再去文件中读取string对象,string中读到是一个野指针。
	char _address[32];
	//string _address;
	int _port;
	Date _date;

};
struct ConfigManager
{
public:
	ConfigManager(const char* filename)
		:_filename(filename)
	{}

	// 二进制写
	// 内存中怎么存,囫囵吞枣,就怎么直接写出去
	void WriteBin(const ServerInfo& info)
	{
		ofstream ofs(_filename, ios_base::out | ios_base::binary);
		ofs.write((const char*)&info, sizeof(info));
	}

	// 二进制读
	// 将文件中的内容直接囫囵吞枣,直接读到内存中
	void ReadBin(ServerInfo& info)
	{
		ifstream ifs(_filename, ios_base::in | ios_base::binary);
		ifs.read((char*)&info, sizeof(info));
	}

	void WriteText(const ServerInfo& info)
	{
		ofstream ofs(_filename);
		ofs << info._address << " " << info._port << " " << info._date;
	}

	void ReadText(ServerInfo& info)
	{
		ifstream ifs(_filename);
		ifs >> info._address >> info._port >> info._date;
	}

private:
	string _filename; // 配置文件
};

void WriteBin()
{
	ServerInfo winfo = { "192.0.0.1111111111111111111111", 80, { 2025, 1, 10 } };

	// 二进制读写
	ConfigManager cf_bin("test.bin");
	cf_bin.WriteBin(winfo);
}

void ReadBin()
{
	// 二进制读写
	ConfigManager cf_bin("test.bin");
	ServerInfo rbinfo;

	cf_bin.ReadBin(rbinfo);
	cout << rbinfo._address << " " << rbinfo._port << " " << rbinfo._date << endl;
}

void WriteText()
{
	ServerInfo winfo = { "192.0.0.11111111111111111", 80, { 2025, 1, 10 } };
	// 文本读写
	ConfigManager cf_text("test.txt");
	cf_text.WriteText(winfo);
}

void ReadText()
{
	ConfigManager cf_text("test.txt");
	ServerInfo rtinfo;
	cf_text.ReadText(rtinfo);
	cout << rtinfo._address << " " << rtinfo._port << " " << rtinfo._date <<
		endl;
}

int main()
{
	WriteBin();
	ReadBin();

	//WriteText();
	//ReadText();

	return 0;
}

我们把ServerInfo类中的char _address[32] 改为 string _address 就会出现野指针的问题,这是为什么?

这是因为:

二进制直接读写对象内存 ≈ 最危险的浅拷贝

  1. 普通浅拷贝:至少原始数据还存在(在另一个对象中)

  2. 二进制读写浅拷贝:原始数据可能已经销毁,留下野指针。

时间线:

本身其实是,string 的指针指向的空间被释放了,从而造成的野指针。


七、string IO流

        ostringstream是string的的写入流,ostringstream是ostream的派生类;istringstream是string的的读出流,istringstream是istream的派生类;stringstream是ostringstream和istringstream的派
生类,既可以读也可以写。这里使用stringstream会很方便。
        stringstream系列底层维护了⼀个string类型的对象用来保存结果,使用方法跟上面的文件流类似,只是数据读写交互的都是底层的string对象。

        stringstream最常用的方式还是使用<<和>>重载,进行数据和string之间的IO转换。

        string流使用str函数获取底层的string对象,或者写入底层的string对象,具体细节参考下面代码理解。

1. 向 stringstream 写入数据

#include<sstream>
int main()
{
	int i = 123;
	Date d = { 2025, 4, 10 };
	//ostringstream oss;
	stringstream oss;
	oss << i << endl;
	oss << d << endl;
	string s = oss.str();
	cout << s << endl;
    return 0;
}

2. 向 stringstream 输出数据

int main()
{
    stringstream iss;
    iss.str("100 2025 9 9");
    int j;
    Date x;
    iss >> j >> x;
    cout << j << endl;
    cout << x << endl;    
    return 0;
}

3.  关于自定义类型的操作

struct ChatInfo
{
	string _name; // 名字
	int _id;      // id
	Date _date;   // 时间
	string _msg;  // 聊天信息
};

int main()
{
	// 结构信息序列化为字符串
	ChatInfo winfo = { "张三", 135246, { 2022, 4, 10 }, "晚上一起看电影吧" };
	ostringstream oss;
	oss << winfo._name << " " << winfo._id << " " << winfo._date << " " <<
		winfo._msg;
	string str = oss.str();
	cout << str << endl << endl;

	// 我们通过网络这个字符串发送给对象,实际开发中,信息相对更复杂,
	// 一般会选用Json、xml等方式进行更好的支持
	// 字符串解析成结构信息
	ChatInfo rInfo;

	istringstream iss(str);
	iss >> rInfo._name >> rInfo._id >> rInfo._date >> rInfo._msg;
	cout << "-------------------------------------------------------" << endl;
	cout << "姓名:" << rInfo._name << "(" << rInfo._id << ") ";
	cout << rInfo._date << endl;
	cout << rInfo._name << ":>" << rInfo._msg << endl;
	cout << "-------------------------------------------------------" << endl;

	return 0;
}

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐