【C++】IO流

目录

一、C语言的输入和输出

二、缓冲区 (以输入缓冲区为例)

三、流 (以C语言为例)

四、C++ IO流

1.C++标准IO流 

类型转换:

标准IO流中的一些细节:

 2.C++文件IO流

基本概念 

常用接口

二进制格式的读写

文本格式的读写 

 五、 stringstream的简单介绍

一、C语言的输入和输出

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

  • scanf(): 从标准输入设备(键盘)读取数据,并将值存放在变量中。
  • printf(): 将指定的文字/字符串输出到标准输出设备(屏幕)。(注意宽度输出和精度输出控制)

二、缓冲区 (以输入缓冲区为例)

1.输入分类:
    输入分为无缓冲输入和缓冲输入,无缓冲输入意味着正在等待的程序可以立即使用输入的字符;缓冲输入,就是将用户输入的字符先放在一个区域(缓冲区),等用户按下某个代表结束的键后,程序才可以使用键入的数据,在这个过程中存储收集用户输入字符的区域就叫做缓冲区。
2.缓冲区的意义:

  • 节约时间,将若干字符当成一个块进行传输比单个字节传输快;
  • 易于修改,无缓冲输入中,输入的字符立即被使用,无法修改,而有缓冲输入中,在按下结束键(一般都是Enter)前都可以修改要输入的数据。

3.缓冲分类:

  •  完全缓冲输入(完全缓冲I/O):缓冲区被填满时才刷新缓冲区(内容内发送到目的地)。一般文件输入都是完全缓冲输入。
  • 行缓冲输入(行缓冲I/O):在出现换行符时刷新缓冲区。键盘输入通常是行缓冲。

 4.缓冲区大小
取决于系统。一般是512B和4096B。

三、流 (以C语言为例)

 不同的系统处理文件的规则往往不同,主要体现在存储、文件末尾标记和衡量文件的大小三个方面。(详情见 C Primer Plus p188)
由于这些差异,编程语言为了增强系统间的兼容性,不会直接处理文件,而是使用标准的I/O包处理流。流是一个实际输入或输出映射的理想化数据流。C/C++语言将不同属性和不同种类的输入,用属性更统一的流来表示,文件的打开和关闭等过程就是把流与文件相关联,这涉及创建用于处理文件的标准模型和一套标准的I/O函数。举个例子,用if(ch\=='\\n')检查换行符,不同的系统会使用不同的方式标记文件末尾(回车符或者换行符),I/O函数会在这两种表示方法之间转换。流是一个将实际输入或输出的内容映射成理想化的数据流。比如C语言中,stdin流表示键盘输入,stdout流表示屏幕输出。

四、C++ IO流

 cplusplus.com/reference/iolibrary/

1.C++标准IO流 

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

int main()
{
	ostream out(nullptr);
	cerr << "xxxx" << endl;
	clog << "yyyy" << endl;

	//ostream& put (char c);
	cout.put(48);//整形截断,打印0
	//https://cplusplus.com/reference/ostream/ostream/put/

	// 作条件判断的类型(表真假):bool 整形 指针
	string s1;
	// istream& operator>> (istream& is, string& str);
	//while (cin>>s1)
	while (operator>>(cin, s1))//括号内的表达式返回值类型是istream&,可以隐式转换成bool
	{
		cout << s1 << endl;
	}
	//结束:暴力ctrl+c,非暴力ctrl+/n
	//https://cplusplus.com/reference/ios/ios/operator_bool/

	return 0;
}

关于istream&转换成bool,大家可能疑惑?这是怎么转换的呢?

下面我们对类型之间的转换,做一个小归纳。

类型转换:

1.内置类型转换成内置类型,一般进行隐式转换或强制类型转换即可。

int* p = nullptr;
int i = (int)p;

2.内置类型转换成自定义类型。需要通过构造函数,将内置类型的变量构造成自定义类的的对象。

class C
{
public:
	C(int x)
	{}
};
int main()
{
	C c1 = 2;
	string s = "yls";
}

3.自定义类型转换成自定义类型,通过构造函数。

class C
{
public:
	C(int x)
	{}
};
class D
{
public:
	D(const C& c)
	{}
};
int main()
{
	list<int> lt;
	list<int>::const_iterator it = lt.begin();//普通迭代器通过构造函数转换成const迭代器
}

4.自定义类型转内置类型,使用opera built-in_type()实现类型转换。

cplusplus.com/reference/ios/ios/operator_bool/

在上面的while循环中,本质上就是调用istream中operator bool()。我们举一个简单的例子,

class E
{
public:
	operator int()
	{
		// ...
		return 0;
	}
};
int main()
{
	E e;
	int x = e;
	cout << x << endl;
}

输出:

标准IO流中的一些细节:

1. cin为缓冲流。键盘输入的数据保存在缓冲区中,当要提取时,是从缓冲区中拿。如果一次输入过多,数据会存在缓冲区待下一次提取,如果输入错了,必须在回车之前修改,如果回车键按下就无法挽回了。只有把输入缓冲区中的数据取完后,才要求输入新的数据

2. 输入的数据类型必须与要提取的数据类型一致,否则出错。出错只是在流的状态字state中对应位置位(置1),程序继续。

3. 空格和回车都可以作为数据之间的分格符,所以多个数据可以在一行输入,也可以分行输入。但如果是字符型和字符串,则空格(ASCII码为32)无法用cin输入,字符串中也不能有空格。回车符也无法读入。

4. cin和cout可以直接输入和输出内置类型数据,原因:标准库已经将所有内置类型的输入和输出全部重载了。

5. 对于自定义类型,如果要支持cin和cout的标准输入输出,需要对<<和>>进行重载。

6. 在线OJ中的输入和输出:

  • 对于IO类型的算法,一般都需要循环输入;
  • 输出:严格按照题目的要求进行,多一个少一个空格都不行;
  • 连续输入时,vs系列编译器下在输入ctrl+Z时结束。
// 单个元素循环输入
while(cin>>a)
{
    // ...
}
// 多个元素循环输入
while(c>>a>>b>>c)
{
    // ...
}
// 整行接收
while(cin>>str)
{
    // ...
}

7. istream类型对象转换为逻辑条件判断值

istream& operator>> (int& val);
explicit operator bool() const;

cplusplus.com/reference/istream/istream/operator>>/

cplusplus.com/reference/ios/ios/operator_bool/

实际上我们看到使用while(cin>>i)去流中提取对象数据时,调用的是operator>>,返回值是istream类型的对象,那么这里可以做逻辑条件值,源自于istream的对象又调用了operator

bool,operator bool调用时如果接收流失败,或者有结束标志,则返回false。

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)
	{}

	operator bool()
	{
		// 这里是随意写的,假设输入_year为0,则结束
		if (_year == 0)
			return false;
		else
			return true;
	}
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;
	return out;
}

// C++ IO流,使用面向对象+运算符重载的方式
// 能更好的兼容自定义类型,流插入和流提取
int main()
{
	// 自动识别类型的本质--函数重载
	// 内置类型可以直接使用--因为库里面ostream类型已经实现了
	int i = 1;
	double j = 2.2;
	cout << i << endl;
	cout << j << endl;

	// 自定义类型则需要我们自己重载<< 和 >>
	Date d(2022, 4, 10);
	cout << d;
	while (d)
	{
		cin >> d;
		cout << d;
	}

	return 0;
}

8.默认情况下,C++和C流是同步的

cplusplus.com/reference/ios/ios_base/sync_with_stdio/

std::cinstdio同步,就是std::cinstdin共用缓冲区。通过设置in.sync_with_stdio(false)之后,std::cin就有了自己的缓冲区。不过解绑之后, C++和 C 的输入输出使用不同的缓冲区,会导致输入和输出顺序无法保障。因此要么用std::cin/std::cout,要么用printf/scanf,尽量不要混用。(注:该机制可能依赖编译器和操作系统的实现。)

 2.C++文件IO流

基本概念 

1.文件是什么?
文件通常是在磁盘或固态硬盘上的一段已命名的存储区。C语言把文件看成一系列连续的字节,每个字节都能被单独的读取。这与UNIX环境中(C的 发源地)的文件结构相对应。由于其他环境中可能无法完全对应这个模型,C提供两种读取文件的模式:文本模式和二进制模式。

2.文本模式和二进制模式
- 所有文件的内容都以二进制的形式存储。
- 文本文件是指用二进制编码的字符表示文本的文件,内容为文本内容,容易被人理解;
- 二进制文件是指用二进制值代表机器语言、数值数据、图片和音乐编码的文件,内容为二进制内容,容易被计算机理解。
C语言提供两种访问文件的途径:二进制模式和文本模式。
在二进制模式中,程序可以访问文件的每个字节;在文本模式中,程序所见的内容和文件的实际内容不同。
程序以文本模式读取文件时,会把本地环境表示的行末尾和文件结束映射为C模式,以二进制模式读取文件,一般不发生映射。

常用接口

同样的,C++根据文件内容的存储格式分为二进制文件和文本文件,并通常用文件流对象读写文件。采用文件流对象操作文件的一般步骤:

  1. 定义一个文件流对象

    ifstream ifile(输入,程序从文件流中读取数据)

    ofstream ofile(输出,程序中的数据读出到文件流)

    fstream iofile(既输入又输出)

  2. 使用文件流对象的成员函数打开一个磁盘文件,使得文件流对象和磁盘文件之间建立联系
  3. 使用提取和插入运算符对文件进行读写操作,或使用成员函数进行读写
  4. 关闭文件

cplusplus.com/reference/fstream/

 我们写一个类,记录服务器的相关信息,

struct ServerInfo
{
	char _address[32];
	double _x;
	Date _date;
};

将服务器类的文件读写封装在另一个类中。(二进制模式的读写类名就是BinIO,文本模式的读写类名是TextIO。)

二进制格式的读写

class BinIO
{
	BinIO(const char* filename = "info.bin")
		:_filename(filename)
	{}
	//写入文件中
	void write(const ServerInfo& winfo)
	{
		//定义一个输出文件流对象
		ofstream ofs(_filename, ofstream::out | ofstream::binary);
		//调用该对象的写
		ofs.write((char*)&winfo, sizeof(winfo));//ostream& write (const char* s, streamsize n);char*可以隐式类型转换成const char*
	}
	//读出文件
	void read(ServerInfo& rinfo)
	{
		ifstream ifs(_filename, ofstream::in | ofstream::binary);
		ifs.read((const char*)&rinfo, sizeof(rinfo));
	}
private:
	string _filename;
};

我们执行下面的主函数,

// 写
int main()
{
	ServerInfo winfo = { "192.0.0.1",12.13,{ 2022, 4, 10 } };

	BinIO bin;
	bin.write(winfo);

	return 0;
}

源文件所在文件夹就多了文件“info.bin”。

我们再执行“二进制的写”

//二进制读
int main()
{
	ServerInfo info;
	BinIO bin;
	bin.read(info);

	cout << info._address << endl;
	cout << info._x << endl;
	cout << info._date << endl;

	return 0;
}

 程序打印:

 上面的服务器信息的地址是用char数组存储,我们换成string,如下

struct ServerInfo
{
	//char _address[32];
	string _address;
	double _x;
	Date _date;
};

 重新执行“二进制的写”,

 没有问题。

我们再执行一下“二进制的读”,

调用析构函数时抛异常了。这是为什么呢?

ServerInfo中成员变量_address的_str(string的私有成员)变成了野指针,在运行“写进程”后,该指针指向的空间已经释放,所以在“读进程”中该指针是野指针。我们模拟string的类,都有一个成员变量初始化需要动态开辟内存。注意:当前文件夹已有"yls.txt"文件。

根据“说法”模拟的代码,

struct new_S
{
	new_S(const char* s = "")
	{
		_s = new char[strlen(s) + 1];
		strcpy(_s, s);
	}
	new_S(const new_S& s)
	{
		_s = new char[strlen(s._s) + 1];
		strcpy(_s, s._s);
	}
	~new_S()
	{
		delete[] _s;
		cout << "~new_S()" << endl;
	}
	char* _s;
};
int main()
{
	new_S s;
	ifstream ifs("yls.txt", ofstream::in | ofstream::binary);
	ifs.read((char*)&s, sizeof(s));
}

根据上面调试的结果,不难发现在read之后,s的成员变量_s变成了野指针,丧失了访问空间的权限。

这段程序调试中显示无法访问内存,但对“读进程”的调试并没有显示内存访问错误,

这也符合野指针的特性——可能使程序出现不可预知的错误。

这是用两个进程分别运行“读写”。我们把读写放在一个进程中看看,

int main()
{
	//写
	ServerInfo winfo = { "192.0.0.1",12.13,{ 2022, 4, 10 } };

	BinIO bin;
	bin.write(winfo);
	//读
	ServerInfo info;
	bin.read(info);

	cout << info._address << endl;
	cout << info._x << endl;
	cout << info._date << endl;
	return 0;
}

调试一下,调用write前后,winfo无变化。

调用read前, 

调用read后,

 我们根据调试图片,不难发现_Ptr都指向同一块空间,这样会对同一块空间析构两次,而且全是野指针。

文本格式的读写 

文本格式的写: 

//文本格式读写
class TextIO
{
public:
	TextIO(const char* filename = "info.text")
		:_filename(filename)
	{}
	//写入文件中
	void write(const ServerInfo& winfo)
	{
		ofstream ofs(_filename);
		ofs << winfo._address << endl;
		ofs << winfo._x << endl;
		ofs << winfo._date << endl;
	}
	//读出文件
	void read(ServerInfo& rinfo)
	{
		ifstream ifs(_filename);
		
	}
private:
	string _filename;
};

// 写
int main()
{
	ServerInfo winfo = { "https://legacy.cplusplus.com/reference/fstream/ifstream/ifstream/", 12.13, { 2022, 4, 10 } };

	TextIO text;
	text.write(winfo);
	
	//浮点型数据转换成字符串
	//c环境
	//double d = 1.111;
	//char buf[128];
	//sprintf(buf, "%lf", d);
	//cout << d << "\n" << buf << endl;
	//cpp环境

执行上面的程序,

 

 文本格式的读:

class TextIO
{
public:
	TextIO(const char* filename = "info.text")
		:_filename(filename)
	{}
	//写入文件中
	void write(const ServerInfo& winfo)
	{
		ofstream ofs(_filename);
		ofs << winfo._address << endl;
		ofs << winfo._x << endl;
		ofs << winfo._date << endl;
	}
	//读出文件
	void read(ServerInfo& rinfo)
	{
		//这里要不要处理换行?
		//不需要,因为当流提取多个值时,默认空格和换行就是多个值之间的分割符
		//如果值里面包括空格呢?可以用getline,https://cplusplus.com/reference/istream/istream/getline/
		ifstream ifs(_filename);
		ifs >> rinfo._address ;
		ifs >> rinfo._x;
		ifs >> rinfo._date;
	}
private:
	string _filename;
};

//读
int main()
{
	ServerInfo info;
	TextIO text;
	text.read(info);
	
	//打印
	cout << info._address << endl;
	cout << info._x << endl;
	cout << info._date << endl;
	return 0;
}

Output:

 五、 stringstream的简单介绍

 1.引入stringstream

int main()
{
	char sql1[128];
	char name[10];
	scanf("%s", name);
	//我们假设spl1是一个数据库 ,我们在这个数据库查某人的分数,我们需要输入sql语句(虽然是在内存中查询)
	//比如下面这个语句,我们只要修改name,就可以在数据库中查询任意name对应的分数
	
	//这是C语言的写法
	sprintf(sql1, "select * from t_scroe where name = '%s'", name);
	printf("%s\n", sql1);

	//cpp写法
	string sql2;
	sql2 += "select * from t_scroe where name = '";
	sql2 += name;
	sql2 += "'";
	cout << sql2 << endl;

	//如果是我们要查询某天的数据呢
	Date d(2024, 1, 19);
	string sql3;
	sql3 += "select * from t_data where date = '";
	sql3 += d;//这样转换就会出错
	sql3 += "'";
	cout << sql3 << endl;

	return 0;
}

 Output:

 那么我们怎么把Date类型的对象转换成字符串类型呢?我们就可以使用stringstream。

 cplusplus.com/reference/sstream/stringstream/str/

 使用stringstream后的代码,

 但一般我们不像上面那样写,而是下面这样写,

 把各种信息转换成字符串,我们把这个过程叫做序列化。

把字符串中的信息提取出来转换成各种类型的数据叫做反序列化。

但stringstream对一些复杂的类型不能很好的支持,在项目中更多的使用json。

我们再举一个例子,

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

int main()
{
	//假设是从界面获取的信息winfo
	ChatInfo winfo = { "张三", 135246, { 2022, 4, 10 }, "晚上一起看电影吧" };
	//转换成字符串并打印
	stringstream oss;
	oss << winfo._name << endl;
	oss << winfo._id << endl;
	oss << winfo._date << endl;
	oss << winfo._msg << endl;
	cout << oss.str() << endl;

	// 通过网络传输到另一个人的设备上,
	ChatInfo rinfo;
	string str = oss.str();//获取输出流上的字符串
	stringstream iss(str);//将字符串转换成流插入
	//插入程序的某个对象中
	iss >> rinfo._name;
	iss >> rinfo._id;
	iss >> rinfo._date;
	iss >> rinfo._msg;

	//再输出对象
	cout << "-------------------------------------------------------" << endl;
	cout << "姓名:" << rinfo._name << "(" << rinfo._id << ") ";
	cout << rinfo._date << endl;
	cout << rinfo._name << ":>" << rinfo._msg << endl;
	cout << "-------------------------------------------------------" << endl;

	return 0;
}

Output: 

参考资料:

C++ 中 sync_with_stdio 的作用 - 阅微堂 (zhiqiang.org)

上一篇:RocketMQ之消费者,重平衡机制与流程详解附带源码解析-2. 概要设计


下一篇:什么是向量