C++的IO流

一、C语言的输入与输出

我们先来复习一下C语言中的IO流

C语言中我们用到的最频繁的输入输出方式就是scanf()和printf()。

scanf() : 从标准输入设备(键盘)读取数据,并将值存放到变量中。

printf() : 将指定的文字/字符串输出到标准输出设备(屏幕)。

C语言借助了相应的缓冲区进行输入与输出:

对于输入输出缓冲区的理解:

1. 可以屏蔽掉低级I/O的实现,低级I/O的实现依赖操作系统本身内核的实现,所以如果能够给屏蔽这部分的差异,可以很容易写出可移植的程序。

2. 可以使用这部分的内容实现"行"读取的行为,对于计算机而言是没有"行"这个概念,有了这个部分,就可以定义"行"的概念,然后解析缓冲区的内容,返回一个"行"。 

二、流是什么

"流" 即流动的意思,是物质从一处向另一处流动的过程,是对一种有序连续且具有方向性的数据(其单位可以是bit,byte,packet)的抽象描述。

C++流是指信息从外部输入设备(如键盘)向计算机内部(如内存)输入和从内存向外部输出设备(显示器)输出的过程。这种输入输出的过程被形象的比喻为"流"。

其特性为:有序连续,具有方向性。

为了实现这种流动,C++定义了I/O标准类库,这些每个类都成为流/流类,用以完成某方面的功能。

三、 C++IO流

C++系统实现了一个庞大的类库,其中ios为基类,其他类都是直接或间接派生自ios类。

3.1 C++标准IO流

C++标准IO流是一组用于输入和输出的类和函数。它们提供了一种通用的方式来处理不同的数据源和目标,例如键盘、屏幕、文件等。

C++标准IO流库包括了三个主要的类:istream、ostream和iostream。istream类用于输入操作,ostream类用于输出操作,iostream则同时支持输入和输出操作。

C++标准库提供了4个全局流对象cin、cout、cerr、clog,使用cout进行标准输出,即数据从内存流向控制台。使用cin进行标准输入即数据通过键盘输入到程序中,同时C++标准库还提供了cerr用来进行标准错误的输出,以及clog进行日志输出。

cout、cerr、clog都是ostream类的三个不同的对象,用于向终端或日志文件等输出设备输出信息。

3.2 operator bool

在线OJ中的输入:
// 单个元素循环输入
while(cin>>str)
{
    // ...
}

OJ是如何判断当前是输出结束的呢?

底层调用了 operator bool .

        C++中的输入流操作符 >> 会返回左操作数(cin对象--istream)的引用。此时 while 条件判断需要一个bool类型,则会自动调用operator bool函数,该函数会将一个对象隐式地转换为 bool 类型。

当输入流(cin)达到文件末尾(EOF)或者输入格式错误时,operator bool会返回false,循环终止;反之会返回true,继续读取。

通过这种方式,while(cin>>str)循环会自动判断输入结束的条件,无需用户手动输入特定的结束标志。编译器会根据输入流的状态自动决定循环何时结束。

tips:

(库中的explicit是防止隐式类型转化的,如果使用了explict就禁掉了隐式类型转化,只能使用显示类型转换,用于提高代码可读性)


在C++中还有很多这种对象转换为内置类型的函数,比如我们可以编写一个operator int 、operator string ,当需要int、string类型的时候,会自动调用operator int/operator string,返回int类型的值。

3.3 C++文件IO流

C++根据文件内容的数据格式分为二进制文件和文本文件。采用文件流对象操作文件的一般步骤:

1. 定义一个文件流对象

  • ifstream ifile(只输入用)
  • ofstream ofile(只输出用)
  • fstream iofile(既输入又输出用)

2.使用文件流对象的成员函数打开一个函数文件,使得文件流对象和磁盘文件之间建立联系。

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

4.关闭文件。

读取文件的使用举例:

我们除了可以读取同一种类型,我们还可以使用提取不同类型的数据

并将其取出。

//data类:
class Date
{
	friend ostream& operator << (ostream& out, const Date& d)
	{
		out << d._year << "年" << d._month << "月" << d._day<<"日";
		return out;
	}
	friend istream& operator >> (istream& in, Date& d)
	{
		in >> d._year >> d._month >> d._day;
		return in;
	}

public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year), _month(month), _day(day)
	{}
private:
	int _year,_month,_day;
};

C++中的IO流对自定义类型进行了更好的支持,使用的方式进行了同一的管理,对控制台、文件的读写十分便捷。

注意

        不要使用string这种涉及深浅拷贝的类进行文件读写。

 

        当我们运行程序时,程序将Serverinfo中的数据整个写入到文件中,string中写入的是_ptr指针,而_ptr指向着实际的字符串,该字符串存储在堆上;当该进程退出,程序地址空间回收,则_ptr成为野指针,此时文件便存储着一系列无效信息。
        当我们再运行进程时,程序将文件中的数据再写入到info中,此时文件中存储着上个进程中的数据,此进程因为拷贝来的_ptr是野指针,便无法获取到字符串;则内部都存放着非法地址(野指针),程序崩溃。


        即使现在我们对程序进行改写,让其读、写文件在同一个进程中:

 

 但是程序仍然出现了奔溃,因为我们我们ServerInfo中的string指向了同一空间,我们如果直接使用ReadBin将文件中的内容直接拷贝(浅拷贝)到rinfo中,此时rinfo中的string是浅拷贝,当出作用域时,winfo、rinfo中的string都会调用析构函数,则会产生内存泄漏,便程序崩溃。

这里给出几种解决方法:

        1. 自己写一个string,不编写析构函数,当文件关闭时,自己释放string指向的空间,

        2. 自己写一个string,使用智能指针来控制析构函数。

        3. 直接使用C语言的字符数组,简单省事。

3.4 二进制读写

二进制读写和文本读写

配合(或 | )操作符来设置打开方式

 接下来我们使用二进制来进行读和写:

//存储的文件
struct ServerInfo
{
	char _address[32];
	int _port;
};
//进行存储的对象
struct ConfigManager_Bin
{
public:
	ConfigManager_Bin(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));
	}
private:
	string _filename; // 配置文件
};

 写入后,查看文件:

发现其中字符串部分正常显示,而数字部分为乱码。

这是因为:

        数字其以补码的形式进行存放,在程序中,数字以特定的方式进行存储的,当以二进制的方式写入到文件中,其会按存储方式的二进制写入文件,当打开文件时,该二进制会根据编码进行显示,则会出现乱码。
如果想让该数字正常显示,要将其转为字符串,再进行文件的写入

但是其确是是将内容以二进制的形式进行了写入。

然后我们使用二进制读取的方式将bin.txt中的内容读取出来:

3.5 文本读写

此时我们只用修改打开方式,并将其数据部分改为字符串类型进行写入,修改后的ConfigManager对象如下:

struct ConfigManager_Text
{
public:
	ConfigManager_Text(const char* filename)
		:_filename(filename)
	{}
	//文本的形式写
	void WriteText(const ServerInfo& info)
	{
		ofstream ofs(_filename, ios_base::out );
		ofs.write(info._address, strlen(info._address));
		char str[20]="";
		sprintf(str, "\n%d\n", info._port);  //将整形写入到str中
		ofs.write(str, strlen(str));
	}
	//文本的形式读
	void ReadText(ServerInfo& info)
	{
		ifstream ifs(_filename, ios_base::in );
		//行读取
		ifs.getline(info._address, 128);
		char port[32];
		ifs.getline(port, 32);
		info._port = atoi(port);
	}
private:
	string _filename; // 配置文件
};

读/写的函数如下:

void test_file03()
{
	ServerInfo winfo = { "192.0.0.1", 80 };
	ConfigManager_Text cf_bin("bin2.txt");
	cf_bin.WriteText(winfo);
}
void test_file04()
{
	ServerInfo rinfo;
	ConfigManager_Text cm("bin2.txt");
	cm.ReadText(rinfo);
	cout << rinfo._address << endl << rinfo._port << endl;
}

tips:

        此处我们使用C语言中的sprintf,将字符串作为输入,可以格式化写入到数组中,读的时候,也可以将数字作为输入,使用sscanf,将数字写入到字符串中。

但是这样的写入和输出,非常麻烦,所以C++库提供了流插入和流提取,让数据的读写变得十分便捷。修改如下:

//文本的形式写
	void WriteText(const ServerInfo_date& info)
	{
		ofstream ofs(_filename, ios_base::out);
		ofs << info._address << endl;
		ofs << info._port << endl;
		ofs << info._date << endl;
	}
	//文本的形式读
	void ReadText(ServerInfo_date& info)
	{
		ifstream ifs(_filename, ios_base::in);
		ifs >> info._address >> info._port >> info._date;
	}

 运行结果如下:

四、stringstream的简单介绍

4.1 stringstream转换与拼接

在C语言中,想将一个整形变量的数据转化为字符串格式,我们会使用以下两种方式:

  1. 使用itoa()函数
  2. 使用sprintf()函数

但是两个函数在转化时,都得需要先给出保存结果的空间,那空间需要给多大呢?这个问题就不界定了,而且转化格式不匹配时,还会得到错误的结果甚至程序崩溃。

以下是使用sscanf、sprintf进行整形和字符串之间的转换。

但是在C++中,可以使用stringstream类对象来避免此问题。 

使用方式如下:

 注意:
        多次转换后,必须使用clear()将上次转换状态进行清空。

        同样的可以使用s.str()取出stringstream中存储的数据,然后进行赋值。

我们还可以使用stringstream进行字符串的拼接:

4.2 stringstream序列化操作

我们还可以使用stringstream进行序列化和反序列化的操作,

void test_chat_info()
{
	ChatInfo winfo = { "张三", 15, { 2022, 4, 10 }, "晚上一起看电影吧"};
	ostringstream oss;
	oss << winfo._name << endl;
	oss << winfo._id << endl;
	oss << winfo._date << endl;
	oss << winfo._msg << endl;

	string str = oss.str();
	cout << str << endl;

	//网络传输str……
    //一般会使用json、xml等方式进行更好的支持
    //字符串解析成结构信息

	// 反序列化  ----------------------
	ChatInfo rinfo; 
	istringstream iss(str);
	iss >> rinfo._name >> rinfo._id >> rinfo._date >> rinfo._msg;
	cout << "-----------------" << endl;
	cout << "日期:" << rinfo._date << endl;
	
	cout << rinfo._id<<" 名字:" << rinfo._name << endl;
	cout << rinfo._msg << endl;
	cout << "-----------------" << endl;


	//更甚至,直接使用stringstream来进行读/写  -- 其同时具有ostringstream/istringstream的功能
	stringstream ss;
	

}

注意:

  1. stringstream实际是在其底层维护了一个string类型的对象用来保存对象。
  2. 多次数据类型转化时,一定要使用clear()来清空,才能正确转化,但clear()不会将stringstream底层的string对象清空。
  3. 可以使用s.str("")方法将底层string对象设置为""空字符串
  4. stringstream使用string类对象代替字符数组,可以避免缓冲区溢出的危险,而且其会对参数类型进行推演,不需要格式化控制,也不会出现格式化失败的风险,因此使用过更方便,更安全。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Brant_zero2022

素材免费分享不求打赏,只求关注

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值