C++IO流

发布于:2025-08-11 ⋅ 阅读:(16) ⋅ 点赞:(0)

一、C语言的输入和输出

C语言中我们用到的最频繁的输入输出方式就是scanf()与printf()。
scanf(): 从标准输入设备(键盘)读取数据,并将值存放在变量中。
printf(): 将指定的文字/字符串输出到标准输出设备(屏幕)。注意宽度输出和精度输出控制。
C语言借助了相应的缓冲区来进行输入与输出。如下图所示:

在这里插入图片描述
对输入输出缓冲区的理解:

(1) 可以屏蔽掉低级I/O的实现,低级I/O的实现依赖操作系统本身内核的实现,所以如果能够屏蔽这部分的差异,可以很容易写出可移植的程序。
(2) 可以使用这部分的内容实现 “行“ 读取的行为,对于计算机而言是没有”行”这个概念,有了这部分,就可以定义“行“的概念,然后解析缓冲区的内容,返回一个“行”。

注意IOIin(进来),Oout(出去)。不管是标准IO流、文件流还是字符串流,都是相对内存而言的: 从内存到设备就是输出(out)设备到内存就是输入(in)

二、流是什么?

“流” 即是流动的意思,是物质从一处向另一处流动的过程,是对一种有序连续且具有方向性的数据(其单位可以是bit、byte、packet)的抽象描述C++流是指信息从外部输入设备(如键盘)向计算机内部(如内存)输入和从内存向外部输出设备(显示器)输出的过程。这种输入输出的过程被形象的比喻为 “流”

它的特性是: 有序连续、具有方向性

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

三、C++IO流

C++系统实现了一个庞大的类库,其中ios为基类,其他类都是直接或间接派生自ios类。
在这里插入图片描述
以下是C++三种流(标准IO流、文件IO流、字符串流)与iostream基类关系的总结:
▲ iostream是‌多重继承‌的产物,同时继承自istream(输入流基类)和ostream(输出流基类),因此它本身是一个支持双向读写的流类
标准IO流‌:cin(istream对象)、cout/cerr/clog(ostream对象)直接继承自istream或ostream,而非iostream。
文件IO流‌:fstream继承自iostream,而ifstream(文件输入流)和ofstream(文件输出流)分别继承自istream和ostream。
字符串流‌:stringstream继承自iostream,而istringstream和ostringstream分别继承自istream和ostream。

🍅🍅并非所有流都直接继承iostream,只有‌需要同时支持读写‌的流类(如fstream、stringstream)会继承iostream,而单一功能的流类(如ifstream、istringstream)仅继承istream或ostream。C++通过多重继承实现流功能的模块化,避免了冗余。例如: 文件输入流(ifstream)只需继承输入功能(istream),无需继承输出功能。

3.1 C++标准IO流

C++标准库提供了4个全局流对象cin、cout、cerr、clog,使用cout进行标准输出,即数据从内存流向控制台(显示器)使用cin进行标准输入即数据通过键盘输入到程序中,同时C++标准库还提供了cerr用来进行标准错误的输出,以及clog进行日志的输出,从上图可以看出,cout、cerr、clog是ostream类的三个不同的对象,因此这三个对象现在基本没有区别,只是应用场景不同。在使用时候必须要包含头文件<iostream>并引入std标准命名空间。

🍋注意🍋:

  1. cin为缓冲流。键盘输入的数据保存在缓冲区中,当要提取时,是从缓冲区中拿。如果一次输入过多,会留在那儿慢慢用,如果输入错了,必须在回车之前修改,如果回车键按下就无法挽回了。只有把输入缓冲区中的数据取完后,才要求输入新的数据。
  2. 输入的数据类型必须与要提取的数据类型一致,否则出错。出错只是在流的状态字state中对应位置位(置1),程序继续。
  3. 空格回车都可以作为数据之间的分格符,所以多个数据可以在一行输入,也可以分行输入。但如果是字符型和字符串,则空格(ASCII码为32)无法用cin输入,字符串中也不能有空格。回车符也无法读入。
  4. cin和cout可以直接输入和输出内置类型数据,原因: 标准库已经将所有内置类型的输入和输出全部重载了。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  1. 对于自定义类型,如果要支持cin和cout的标准输入输出,需要对<<和>>进行重载。
  2. 在线OJ中的输入和输出:
    ○ 对于IO类型的算法,一般都需要循环输入
    ○ 输出: 严格按照题目的要求进行,多一个少一个空格都不行
    连续输入时,vs系列编译器下在输入 ctrl+z(加换行) 时结束
//单个元素循环输入
while (cin >> a)
{
	//...
}

//多个元素循环输入
while (c >> a >> b >> c)
{
	//...
}

//整行接收
while (cin >> str)
{
	//...
}
  1. istream类型对象转换为逻辑条件判断值

实际上我们看到使用while(cin>>i)去流中提取对象数据时,调用的是operator>>,返回值是istream类型的对象,那么这里可以做逻辑条件值,源自于istream类的对象又调用了operator bool,operator bool调用时如果接收流失败,或者有结束标志,则返回false。

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

这里要解释一下:将cin>>变量/对象这种作为循环语句的循环条件,本质是一种函数调用,也就是上面重载了流提取操作符>>。但返回值却是一个istream类型的对象,那为什么可以作为循环语句的条件呢?这里就涉及到类型的转换问题:

class A
{
public:
	A(const int& i)
	{ }
};

class C
{
public:
	operator int()
	{
		//...
		return 0;
	}
};

int main()
{
	int* p = nullptr;
	int i = (int)p; //强制类型转换
	cout << i << endl;

	//调用A类型的构造函数完成:内置类型->自定义类型
    A a1 = 10; 
	string str = "xxxx"; //隐式类型转换其实就是因为有构造函数支持,才实现的转换
	cout << str << endl;

	set<int> s;
	//set类的insert接口就是普通迭代器转const迭代器:自定义类型->自定义类型
	set<int>::const_iterator it = s.begin(); 

	C c;
	int b = c; //C类的operator int支持:自定义类型->内置类型
	cout << b << endl;

	return 0;
}

在这里插入图片描述
在循环语句中,之所以cin>>变量/对象的返回值可以继续作为循环的判断条件,本质就是进行了类型转换。即istream类中实现了:istream->bool的转换接口,这样istream类的对象在做逻辑条件值时,编译器会自动去调类型转换的接口:
在这里插入图片描述
自定义类型转内置类型接口的书写格式为:

operator type(); 其中type就为要转换的内置类型,在该函数中要返回一个type类型的值。

下面有一个例子可以看一下:

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

注意:由于C++兼容C语言,所以C和C++的输入输出系统在默认情况下会共享缓冲区。即C++标准流(cin/cout)默认会与C标准流(stdin/stdout)保持同步,这种设计使得两者可以安全混用。同步状态下,C++流操作会直接影响C标准流的缓冲区状态。

int main()
{
	int i = 0;
	int j = 0;
	cin >> i;
	scanf("%d", &j);

	return 0;
}

在这里插入图片描述
在这里插入图片描述

● C语言使用stdio.h提供的独立缓冲区系统
● C++通过iostream类体系管理自己的缓冲区

3.2 C++文件IO流

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

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

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

在这里插入图片描述
ofstream类就是对文件进行操作的输出流类:
在这里插入图片描述
该类的对象维护一个filebuf对象作为它们的内部流缓冲区,它对与它们相关联的文件(如果有的话)执行输入/输出操作。来看一下该类的构造函数:
在这里插入图片描述
红框里的第二个构造函数:该构造函数有两个参数,第一个参数const string& filename是一个string类型的常量引用,用于指定要打开的文件的名称。第二个参数ios_base::openmode mode = ios_base::out是文件的打开模式,它是一个可选参数,默认值为ios_base::out。

而ios_base::out表示以输出方式打开文件(也可以用ofstream::out, 下同),这是ofstream类最基本的打开模式,用于向文件写入数据。如果文件不存在,会创建一个新文件;如果文件已存在,默认情况下会将文件内容截断(清空文件)再写入新内容。除了默认的ios_base::out,还可以通过组合其他模式来改变文件的打开行为(通过按位或组合在一起),例如: ios_base::app表示追加模式,此时写入的数据会添加到文件末尾而不是覆盖原有内容;ios_base::binary表示以二进制方式打开文件,用于处理非文本文件等。

当创建好了一个ofstream类型的对象,打开了相关的文件以后。紧接着就要将数据写到(输出到)文件中:
在这里插入图片描述
将s指向的数组的前n个字符插入到(文件)流中,n是字符个数(也指字符数组的大小)。如果我们还想把文件中的数据读到(输入到)内存中:
在这里插入图片描述从流中提取n个字符,并将它们存储在s所指向的数组中

🍓🍓与C语言类似,文件读写完数据后,要将打开的文件关闭。在C++中可以不用手动调close关闭文件,因为ofstream是类,该类对象在进程结束时,会自动调用析构函数去关闭文件。

(1) 下面我们就试着以二进制的形式去读写一个文件:

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

//设计一个类用来读写上面ServerInfo类对象的内容(二进制读写)
class BinIO
{
public:
	BinIO(const char* filename = "info.bin")
		:_filename(filename)
	{ }

	//将一个ServerInfo类对象中的内容以二进制的形式输出到文件中
	void Write(const ServerInfo& winfo)
	{
		ofstream ofs(_filename, ofstream::out | ofstream::binary);
		ofs.write((char*)&winfo, sizeof(winfo));
	}

	//将文件中的内容以二进制的形式输入到一个ServerInfo类对象中
	void Read(const ServerInfo& rinfo)
	{
		ifstream ifs(_filename, ifstream::in | ifstream::binary);
		ifs.read((char*)&rinfo, sizeof(rinfo));
	}
private:
	string _filename; //文件名
};

void test_Write()
{
	ServerInfo winfo = { "192.0.0.1", 3.14, { 2022, 4, 10 } };

	BinIO bin;
	bin.Write(winfo); //将winfo对象里的内容按字节写到文件中
}

void test_Read()
{
	ServerInfo info;
	BinIO bin;
	bin.Read(info); //将文件里的内容读到info对象中
}

在这里插入图片描述
再把文件中的数据读到一个ServerInfo类型的对象info中(内存中):
在这里插入图片描述
在这里插入图片描述
注意:上面的ServerInfo结构体中是用字符数组作为其成员,即在栈上开辟数组。但不能换成string类的成员,因为这样通过二进制形式读写会出问题:

struct ServerInfo
{
	string s;
	double _d;
	Date _date;
};

如果是在同一个进程中(都在main函数中进行读写),先把一个ServerInfo类对象以二进制形式写进文件中,然后紧接着又将这个文件中的内容读给另一个ServerInfo类对象,跟浅拷贝的问题类似,即两个对象共享同一块资源。因为string类的底层是三个成员:_str、_size、_capacity,而_str指向着堆上申请的资源,字符串就存储在这块资源中。比如:

int main()
{
	BinIO bin;

	ServerInfo winfo = { "https://legacy.cplusplus.com/reference/istream/istream/read/", 3.14, { 2022, 4, 10 } };
	bin.Write(winfo); //将winfo对象里的内容按字节读到文件中

	ServerInfo info;
	bin.Read(info); //将文件里的内容读到info对象里

	return 0;
}

在这里插入图片描述
在这里插入图片描述
所以winfo与info共享着同一块资源,析构的时候就会出现同一块资源析构两次,程序报错。就是因为在同一个进程中,将winfo对象中的string类成员s中的_str存储的值(地址)写进了文件中,而此时堆上申请的资源并没有销毁,再将文件中的内容读给了另一个对象info,那正好就将winfo的s的_str的值读给了info中的s的_str。所以析构时才会报错。当然这是在同一个进程中才出现的问题。

如果是在两个进程中分别进行上面的写和读,也还是会报错:

int main()
{
	BinIO bin;
	ServerInfo winfo = { "https://legacy.cplusplus.com/reference/istream/istream/read/", 3.14, { 2022, 4, 10 } };
	bin.Write(winfo); //将winfo对象里的内容按字节读到文件中

	return 0;
}

int main()
{
	BinIO bin;
	ServerInfo info;
	bin.Read(info); //将文件里的内容读到info对象里
	cout << info.s << endl;
	cout << info._d << endl;
	cout << info._date << endl;

	return 0;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
原因就是先在一个进程中把winfo中的s的_str的值写进文件中,再在另一个进程中把文件里的内容读给另一个对象info中的s的_str,但原来的winfo对象的s的_str所指向的堆上的资源早就在原来的进程中析构了,s也销毁了。所以此时文件中存储的地址就是一个野指针!那再读给info的s的_str,访问时就是野指针的使用。

🔥所以总结:只要是容器时,都要小心!不能以二进制的形式进行文件的读写🔥

(2) 既然二进制读写会出现上面的错误,那可以进行文本读写:

注意:在C++的文件流操作中,ios_base::out默认是以‌文本模式‌进行读写操作的。

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

//设计一个类用来读写上面ServerInfo类对象的内容(文本读写)
class TextIO
{
public:
	TextIO(const char* filename = "info.text")
		:_filename(filename)
	{ }

	//将一个ServerInfo类对象中的内容以字符串的形式输出到文件中
	void Write(const ServerInfo& winfo)
	{
		ofstream ofs(_filename); //使用默认的缺省参数ios_base::out
		ofs << winfo._address << endl;
		ofs << winfo._d << endl;
		ofs << winfo._date << endl;
	}

	//将文件中的内容输入到一个ServerInfo类对象中
	void Read(ServerInfo& rinfo)
	{
		ifstream ifs(_filename); //使用默认的缺省参数ios_base::in
		ifs >> rinfo._address;
		ifs >> rinfo._d;
		ifs >> rinfo._date;
	}
private:
	string _filename; //文件名
};

int main()
{
	TextIO text;
	ServerInfo winfo = { "192.0.0.1", 3.14, { 2022, 4, 10 } };
	text.Write(winfo);

	ServerInfo info;
	text.Read(info);
	cout << info._address << endl;
	cout << info._d << endl;
	cout << info._date << endl;

	return 0;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
文件流(如ofstream和ifstream)和标准IO流(如cout和cin)、字符串流(如stringstream)对‌内置类型‌(int、float、string等)已预定义重载,不管是哪种流的输入和输出均能自动识别数据类型。
● 对于‌自定义类型‌(如类对象),必须 手动重载<<>> 运算符才能支持流操作。
返回流引用以支持链式调用(如cout << obj1 << obj2)。
输入重载需修改对象参数,故不能加const限定

😎流对象的统一性:

所有流(标准IO/文件/字符串流)均继承自ios_base重载后的<<和>>运算符可通用。例如: 自定义类型重载<<和>>后,既能用cout输出到控制台,也能用ofstream类对象写入文件。

四、stringstream的简单介绍

在C语言中,如果想将一个整形变量的数据转化为字符串格式,如何去做呢?

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

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

int main()
{
	int n = 123456789;
	char s1[32];
	_itoa(n, s1, 10);

	char s2[32];
	sprintf(s2, "%d", n);

	char s3[32];
	sprintf(s3, "%f", n);

	return 0;
}

在C++中,可以使用stringstream类对象来避开此问题,在程序中如果想要使用stringstream,必须要包含头文件 <sstream>。在该头文件下,标准库的三个类istringstream、ostringstream和stringstream,分别用来进行流的输入输出输入输出操作,本文主要介绍stringstream。

stringstream主要可以用来:
(1) 将数值类型数据格式化为字符串

int main()
{
	int a = 12345678;
	string sa;

	//将一个整形变量转化为字符串,存储到string类对象中
	stringstream ss;
	ss << a;
	ss >> sa;

	// clear()
	// 注意多次转换时,必须使用clear将上次转换的状态清空掉
    // stringstream在转换结尾时(即最后一个转换后),会将其内部状态设置为badbit
	// 因此下一次转换是必须调用clear()将状态重置为goodbit才可以转换
    // 但是clear()不会将stringstream底层字符串清空掉
    // oss.str("");
	// 将stringstream底层管理的string对象设置成"", 
    // 否则多次转换时,会将结果全部累积在底层string对象中
	ss.str("");
	ss.clear();
	// 清空ss, 不清空会转化失败
	double d = 12.34;
	ss << d;
	ss >> sa;

	string sValue;
	sValue = ss.str();
	// str()接口:返回stringsteam中管理的string类型
	cout << sValue << endl;

	return 0;
}

(2) 字符串拼接

int main()
{
	stringstream ss;

	// 将多个字符串放入sstream中
	ss << "first" << " " << "string,";
	ss << " second string";
	cout << "strResult is: " << ss.str() << endl;

	// 清空sstream
	ss.str("");
	ss << "third string";
	cout << "After clear, strResult is: " << ss.str() << endl;

	return 0;
}

在这里插入图片描述
在这里插入图片描述
stringstream的流插入(<<)和流提取(>>)操作底层通过‌字符串缓冲区管理‌和‌类型格式化转换‌实现,具体机制如下:

  1. 底层数据结构‌
    内部缓冲区‌:基于stringbuf类管理,继承自basic_streambuf,负责存储字符串数据。
    双向流支持‌:stringstream同时继承了istream和ostream,允许读写操作共享同一缓冲区。
  2. 流插入(<<)的底层流程‌
    类型格式化‌:通过重载<<操作符将数据(如int、double等)转换为字符序列,调用num_put等locale工具处理格式化。
    缓冲区写入‌:格式化后的字符序列被追加到stringbuf的内部string中,动态扩展容量(类似vector的扩容机制)。
    状态维护‌:更新流状态标志(如eofbit、failbit)以反映操作结果。
  3. 流提取(>>)的底层流程‌
    字符解析‌:从stringbuf中读取字符序列,通过num_get等工具将字符转换为目标类型(如解析"123"为int)。
    ‌分隔符处理‌:默认以空格/换行符作为分隔符,遇到分隔符时停止提取。
    ‌错误处理‌:若类型不匹配(如尝试提取数字但遇到字母),设置failbit并终止操作。

避免频繁创建‌:重复使用同一stringstream对象时,需先调用str(“”)清空缓冲区,再调用clear()重置状态标志。

(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 << endl;
	oss << winfo._id << endl;
	oss << winfo._date << endl;
	oss << winfo._msg << endl;
	cout << oss.str() << endl;

	// 我们通过网络把这个字符串发送给对象,实际开发中,信息相对更复杂,
    // 一般会选用Json、xml等方式进行更好的支持
    // 字符串解析成结构信息(反序列化)
	ChatInfo rinfo;
	string str = oss.str();
	istringstream 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;
}

在这里插入图片描述
🍑总结🍑:

○ stringstream实际是在其底层维护了一个string类型的对象用来保存结果。
○ 多次数据类型转化时,一定要用clear()清空,才能正确转化,但clear()不会将stringstream底层的string对象清空。
○ 可以使用ss.str(“”)方法,将底层的string类对象设置为""(空字符串)。
○ 可以使用ss.str()将让stringstream返回其底层的string类对象。
○ stringstream使用string类对象代替字符数组,可以避免缓冲区溢出的危险,而且其会对参数类型进行推演,不需要格式化控制,也不会出现格式化失败的风险。因此使用更方便,更安全。

在这里插入图片描述


网站公告

今日签到

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