C++的IO流

目录

1. 流

2. C++IO流

2.1. C++标准IO流

std::istream类型对象转换为bool用于逻辑判断

补充:

1. 字符串转整形

2. 整形转字符串

2.2. C++文件IO流 

2.2.1. 二进制的读写

2.2.2. 文本读写

3. stringstream流的简单介绍


1. 流

在 C++ 中,流(stream)是一种用于输入和输出操作的抽象概念。它被用于实现数据的输入和输出,使程序能够与外部环境进行交互。

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

C++ 提供了一个标准库(iostream),其中包含了用于输入和输出的流类。流类分为输入流(istream)和输出流(ostream)两种类型。输入流用于从外部读取数据到程序中,而输出流用于将程序的数据输出到外部。

C++ 的流可以连接到各种设备,例如键盘、屏幕、文件等。流与设备之间的链接是通过流插入运算符(<<)和流提取运算符(>>) 实现的。

以下是几个重要的流对象:

  • cin:标准输入流,用于从键盘接收输入。
  • cout:标准输出流,用于将数据输出到屏幕。
  • cerr:标准错误流,用于将错误信息输出到屏幕。
  • clog:标准日志流,用于将日志信息输出到屏幕。

流的特点:有序连续,具有方向性。

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

2. C++IO流

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

C++的IO流_第1张图片

2.1. C++标准IO流

C++标准库提供了 4 个全局流对象 cin cout cerr clog。
使用cout 进行标准输出,即数据从内 存流向控制台 ( 显示器 )
使用cin 进行标准输入即数据通过键盘输入到进程中。
C++标准库还提供了cerr 用来进行标准错误的输出
C++标准特提供了clog进行日志的输出
从上图可以看出,cout 、 cerr、 clog ostream类的三个不同的对象,因此这三个对象现在基本没有区别,只是应用场景不同。
注意:
1. cin为缓冲流。 键盘输入的数据保存在缓冲区中,当要提取时,是从缓冲区中拿 。如果一次输入过多,会留在那儿慢慢用,如果输入错了,必须在回车之前修改,如果回车键按下就无法 挽回了 只有把输入缓冲区中的数据取完后,才要求输入新的数据。
2. 输入的数据类型必须与要提取的数据类型一致 ,否则出错。出错只是在流的状态字 state 中对应位置位(置1),进程继续。
3. 空格和回车都可以作为数据之间的分格符,所以多个数据可以在一行输入,也可以分行输
入。但如果是字符型和字符串,则空格( ASCII 码为 32 )无法用 cin输入,字符串中也不能有
空格。回车符也无法读入。
4. cin和 cout 可以直接输入和输出内置类型数据,原因:标准库已经将所有内置类型的输入和
输出全部重载了

C++的IO流_第2张图片

那如果是个自定义类型呢?对于自定义类型,默认情况下是不支持<<流插入和>>流提取的,但是我们可以自己实现这个自定义类型专属的operator<<和operator>>,例如:

class Time
{
public:
	Time(int hour = 0, int min = 0, int second = 0)
		:_hour(hour)
		, _min(min)
		, _second(second)
		{}
private:
	int _hour;
	int _min;
	int _second;
};
void Test2(void)
{
	Time t1(22, 35, 36);
	Time t2;
	std::cin >> t2;  // 默认情况下,时间类不支持operator>>
	std::cout << t2; // 同样,它也不支持operator<<
}

因此,我们可以显示实现这个类的operator<<和operator>>,实现如下:

class Time
{
public:
	Time(int hour = 0, int min = 0, int second = 0)
		:_hour(hour)
		, _min(min)
		, _second(second)
		{}
    // 为了让这两个函数在类外访问私有成员属性,因此声明为友元函数
	friend std::istream& operator>>(std::istream& in, Time& t);
	friend std::ostream& operator<<(std::ostream& out, const Time& t);
private:
	int _hour;
	int _min;
	int _second;
};
// 之所以在类外定义,因为需要第一个参数是in
std::istream& operator>>(std::istream& in, Time& t)
{
	// 为了获取类的私有成员属性,需要将该函数设置为友元
	in >> t._hour >> t._min >> t._second;
	// 为了可以连续输入,需要返回值是标准输入流的引用
	return in;
}
// 同理,之所以在类外定义,因为需要第一个参数是out
std::ostream& operator<<(std::ostream& out, const Time& t)
{
	// 为了获取类的私有成员属性,需要将该函数设置为友元
	out << t._hour << ":" << t._min << ":" << t._second << std::endl;
	// 为了可以连续输出,需要返回值是标准输出流的引用
	return out;
}

需要注意,scanf(还有fscanf,sscanf),cin,当输入多个值时,它们默认都是以空格和换行符为分隔符的。

void Test3(void)
{
	// 假如有一个日期
	int year, month, day;
	// C的输入
	// 中间不需要加空格,因为scanf就是默认以空格或者换行符为分隔符的
	scanf("%d%d%d", &year, &month, &day);
	// C++的输入
	std::cin >> year >> month >> day;
	// ----------------------------
	// 那如果此时假如有一个日期是这样的
	// 20231021,如何获得它的年月日呢
	// C的做法:
	scanf("%4d%2d%2d", &year, &month, &day);
	// C++的做法呢?
	// 1. 利用substr,构造年月日的字符串,再通过stoi,获得年月日的整形
	std::string dath("20231021");
    // 从0下标开始,依次构造四个字符的string
	std::string str_year = dath.substr(0, 4);
	std::string str_month = dath.substr(4, 2);
    // 从6下标开始,依次构造到结尾的string
	std::string str_day = dath.substr(6, -1);
	year = std::stoi(str_year);
	month = std::stoi(str_month);
	day = std::stoi(str_day);
}

std::istream类型对象转换为bool用于逻辑判断

在我们做OJ题时,可能会碰到有多组测试的练习,具体情况大概如下:

void Test4(void)
{
	int year, month, day;
	std::string dath;
    // C++的多组测试大概如下
	while (std::cin >> dath)
	{
		year = std::stoi(dath.substr(0, 4));
		month = std::stoi(dath.substr(4, 2));
		day = std::stoi(dath.substr(6, -1));
		std::cout << year << "年" << month << "月" << day << "日" << std::endl;
	}
}

C++的IO流_第3张图片

那么此时的问题就是,如何终止这个进程呢?在这里有两种方式:

第一种方式:较为直接暴力,Ctrl + c,给终端进程发送一个信号,相当于杀掉了该进程,致使给进程终止。

第二种方式:Ctrl + z + 换行, 这种方式会给终端进程输入了一个流的结束标志,致使该进程结束。

第一种方式,没什么解释的。但是第二种方式的原理是什么呢?

首先我们知道,std::cin >> 是一个函数调用,返回一个标准输入流,而标准输入流会从ios类继承一个函数,operator bool(),当该函数遇到了一个流的结束标志,会将其类型转化为一个bool类型。

C++的IO流_第4张图片

首先,我们通过下面的实例,理解一下上面的这种函数。

class A
{
public:
	A(int a = 0) :_a(a) {}
private:
	int _a;
};

void Test5(void)
{
	// 在这里之所以支持 内置类型 --> 自定义类型
	// 原因是因为:隐式类型转换
	// 10会先构造一个A的匿名对象,在调用拷贝构造,编译器进行优化,直接构造a这个对象
	A a = 10;  
	// 那如果我要支持  自定义类型 --> 内置类型呢?
	// 默认情况下,无法支持,编译报错
	int i = a;   
}
// 因此我们可以实现一个operator int
// 它会支持将一个自定义类型转为内置类型
class A
{
public:
	A(int a = 0):_a(a) {}
	operator int()
	{
		return _a;
	}
private:
	int _a;
};

那么像OJ那种情况是什么样子的呢?我们可以看看下面的这个例子:

class Time
{
public:
	Time(int hour = 0, int min = 0, int second = 0)
		:_hour(hour)
		, _min(min)
		, _second(second)
		{}
	operator bool()
	{
		// 当_hour == -1,相当于收到了一个结束标志,
        // 此时标准输入流的类型将转化为一个bool类型,具体为false
        // 如果_hour != -1,那么*this 也会转化为一个bool类型,但具体为true
		if (_hour == -1)
			return false;
		else
			return true;
	}
	friend std::istream& operator>>(std::istream& in, Time& t);
	friend std::ostream& operator<<(std::ostream& out, const Time& t);
private:
	int _hour;
	int _min;
	int _second;
};
// 为了避免产生this指针,我们需要写成一个全局函数
std::istream& operator>>(std::istream& in, Time& t)
{
	// 为了获取类的私有成员属性,需要将该函数设置为友元
	in >> t._hour >> t._min >> t._second;
	// 为了可以连续输入,需要返回值是标准输入流的引用
	return in;
}

// 同理,为了避免产生this指针,我们需要写成一个全局函数
std::ostream& operator<<(std::ostream& out, const Time& t)
{
	// 为了获取类的私有成员属性,需要将该函数设置为友元
	out << t._hour << ":" << t._min << ":" << t._second;
	// 为了可以连续输出,需要返回值是标准输出流的引用
	return out;
}

上面的这个类重载了一个 operator bool(),大概意思就是当_hour == -1的时候,此时t的这个类型会被转化为bool类型,具体为false。那么就可以用它来判断是否继续循环,例如下面的代码:

void Test6(void)
{
	Time t;
	std::cin >> t;
    // 当t的这个对象的_hour == -1时,终止循环
	while (t)
	{
		std::cout << t << std::endl;
		std::cin >> t;
	}
}

此时我们也可以理解,为什么OJ题,通过如下代码输入多组情况:

void Test4(void)
{
	Time time;
    // C++的多组测试大概如下
	while (std::cin >> time)
	{
        // ... 
	}
}

C++的IO流_第5张图片

顺便在这里说一下, 库里面的operator bool 用了explicit修饰。那它有什么作用呢?

C++的IO流_第6张图片

class A
{
public:
	A(int a = 0) :_a(a) {}

	explicit operator int()
	{
		return _a;
	}

private: 
	int _a;
};

void Test10(void)
{
	A a;
	// 如果没有explicit修饰,那么在这里编译器支持,发生了隐式类型转换.
	//int i = a;
	// explicit 会禁止隐式类型转换
	// 但我们支持 显式类型转换
	int j = (int)a;
	int k = static_cast(a); 
}

补充:

1. 整形转字符串
namespace Xq
{
    std::string to_string(int val)
	{
		std::string ret;
		while (val > 0)
		{
			int tmp = val % 10;  // 取当前数字的最后一位
			ret += tmp + '0';
			val /= 10;
		}
		std::reverse(ret.begin(), ret.end());  // 逆序
		return ret;
	}
}
2. 字符串转整形
namespace Xq
{
    int stoi(const std::string& str)
	{
		int k = 0;
		int ret = 0;
		for (int i = static_cast(str.size() - 1); i >= 0; --i)
		{
			ret += (str[i] - '0') * static_cast(pow(10,k++));
		}
		return ret;
	}
}

2.2. C++文件IO流 

C++根据文件内容的数据格式分为二进制文件文本文件。采用文件流对象操作文件的一般步骤:
1. 定义一个文件流对象
ifstream ifile(只输入用) ,用于从特定流(例如键盘)中读取数据
ofstream ofile(只输出用),用于向特定流(例如显示器)中写入数据
fstream iofile(既输入又输出用)
2. 使用文件流对象的成员函数打开一个磁盘文件,使得文件流对象和磁盘文件之间建立联系
3. 使用提取和插入运算符对文件进行读写操作,或使用成员函数进行读写
4. 关闭文件

ifstream可以支持实例化出一个对象,该对象会打开一个文件,例如:

C++的IO流_第7张图片

C++的IO流_第8张图片

 打开一个文件,并会提供一个缺省参数,它将默认为输入模式,用于从文件中读取数据。

并且,不支持拷贝构造,支持移动构造。我们也可以通过,调无参构造实例化一个ifstream,再通过open从特定文件中读取数据。

C++的IO流_第9张图片

当使用 C++ 标准库中的  std::ifstream类打开文件时,可以通过 openmode 参数指定不同的模式。

以下是几种常见的文件打开模式:

- std::ios_base::in:以输入模式打开文件,允许读取文件内容。
- std::ios_base::out:以输出模式打开文件,可以向文件中写入数据。如果文件不存在,则创建新文件;如果文件已存在,则截断文件内容。
- std::ios_base::app:以追加模式打开文件,在文件末尾添加新数据。如果文件不存在,则创建新文件。
- std::ios_base::ate:以追加模式打开文件,但是初始位置定位在文件末尾。如果文件不存在,则创建新文件。
- std::ios_base::trunc:以输出模式打开文件,在打开文件之前先清空文件内容。如果文件不存在,则创建新文件。
- std::ios_base::binary:以二进制模式打开文件,用于处理二进制文件或者禁止特定的文本转换。

这些模式可以组合使用,通过使用按位或运算符 | 连接它们。例如,std::ios_base::in | std::ios_base::binary 表示以二进制模式打开文件用于读取。

需要注意的是,如果不指定打开模式,默认是以输入模式打开文件(即 std::ios_base::in)。

通过将指定的打开模式传递给 std::ifstream 对象的构造函数或者调用 open() 函数,可以打开特定模式的文件用于读取或写入操作。

在这里演示一下,通过ifstream读取特定文件。如下所示:

C++的IO流_第10张图片

void Test7(void)
{
	//std::ifstream ifs("Test.cc", std::ifstream::in);
	// 默认情况下,打开文件方式如上所示
	// 在这里,以默认方式,打开 Test.cc
	std::ifstream ifs("Test.cc");
	char ch = ifs.get();  // get这个成员函数对应C的fgetc
	// 同样,ifstream这个类继承了ios::operator bool
	// 当文件内容读取结束时,ifs返回一个bool类型,具体为false,循环结束
	while (ifs)
	{
		std::cout << ch;
		ch = ifs.get();
	}
	std::cout << std::endl;
    // 在这里不用调close关闭文件,因为当ifs这个对象生命周期结束时,会关闭打开的文件
}

当然,如果你是open打开文件的,自然也需要close关闭文件

void Test8(void)
{
	// 实例化一个ifstream对象,但不打开文件
	std::ifstream ifs;
	// 通过调用open接口,打开文件
	//ifs.open("Test.cc", std::ifstream::in);
	// 默认情况下,打开文件方式如上所示
	// 在这里,以默认方式,打开 Test.cc
	ifs.open("Test.cc");
	char ch = ifs.get();
	while (ifs)
	{
		std::cout << ch;
		ch = ifs.get();
	}
	std::cout << std::endl;
	// 由于这里是open打开文件的,因此当读取操作结束时,需要显示关闭文件
	ifs.close();
}

光看上面,C++做的,C的文件读写也能做,然而有时候,我们却可以利用ifstream做一些C不好操作的情况,具体如下:

在当前目录下,创建一个log.txt,里面有三种数据类型,整形,字符,浮点,如何把它们从文件中读取出来呢? 

C++的IO流_第11张图片

具体实现如下:以默认方式,打开log.txt,读取log.txt中文件的内容

你怎么用C++标准IO流的流提取,你就怎么用C++文件IO流的流提取

void Test9(void)
{
	std::ifstream ifs("log.txt", std::ifstream::in);
	int i;
	char ch;
	double d;
	ifs >> i >> ch >> d;
	std::cout << i << std::endl;
	std::cout << ch << std::endl;
	std::cout << d << std::endl;
}

但是,上面的文件内容,C的文件读写同样可以做到,但如果是下面的场景呢?

C++的IO流_第12张图片

此时多了两个自定义类型,分别是string和一个时间类。这时候C的读写就不好操作了 ,但是C++依旧可以,具体如下:

C++的IO流_第13张图片

void Test9(void)
{
	std::ifstream ifs("log.txt", std::ifstream::in);
	int i;
	char ch;
	double d;
	std::string str;
	Time time;
	ifs >> i >> ch >> d >> str >> time;
	std::cout << i << std::endl;
	std::cout << ch << std::endl;
	std::cout << d << std::endl;
	std::cout << str << std::endl;
	std::cout << time << std::endl;
}

之所以可以读取成功自定义类型,原因是因为,这两个自定义类型都重载了operator>>(流提取)和operator<<(流插入)

2.2.1. 二进制的读写

二进制读写:在内存中如何存储的,就如何写到磁盘文件, 优点是快,缺点是:写向文件的内容不可见。

struct person_info
{
	char _name[32];
	int _age;
};

struct config_manager
{
public:
	config_manager(std::string filename = "log.txt")
		:_filename(filename)
	{}

	void binary_write(const person_info& p_if)
	{
		// 向_filename文件写数据
		// 以二进制的方式向文件写入数据
		std::ofstream ofs(_filename,std::ios::out | std::ios::binary | std::ios::trunc);
		ofs.write((char*)&p_if, sizeof(p_if));
	}

	void binary_read(person_info& p_if) 
	{
        // 从_filename文件读数据
		// 以二进制的方式从文件读取数据
		std::ifstream ifs(_filename, std::fstream::in | std::fstream::binary);
		ifs.read((char*)&p_if, sizeof(p_if));
	}
private:
	std::string _filename;
};

void Test11(void)
{
    // 测试写
	person_info p_if{ "lisi", 20 };
	config_manager cmg;
	cmg.binary_write(p_if);
}

void Test12(void)
{
    // 测试读
	person_info p_if;
	config_manager cmg;
	cmg.binary_read(p_if);
	std::cout << p_if._name << ":" << p_if._age << std::endl;
}

注意:

C++ 的二进制读写可能不好支持具有深浅拷贝的类,主要是因为深浅拷贝的概念是针对对象的内存布局和数据拷贝而言的。

在二进制读写过程中,通常是按照对象的内存布局进行读写的,即将对象的二进制表示直接写入到文件中,或者从文件中读取二进制表示并重新构造对象。这种方式对于简单的、只包含基本数据类型成员的类是有效的。

然而,对于具有深浅拷贝概念的类而言,对象的拷贝通常涉及到动态内存分配和指针成员的处理。深拷贝会创建一个新的对象,并为其成员变量分配独立的内存空间,然后将原对象的值复制到新的对象中。而浅拷贝只是简单地将指针成员的地址复制到新对象中,这样多个对象将共享同一块内存,可能导致释放内存时出现问题。

在进行二进制读写时,由于直接将对象的二进制表示写入文件或者从文件中读取并重新构造对象,并没有进行深拷贝的操作,因此可能无法正确地处理类对象中的指针成员和动态分配的内存。

为了正确地进行二进制读写,对于具有深浅拷贝概念的类,通常需要自定义序列化和反序列化方法,通过在二进制读写过程中显式地处理指针成员和动态分配的内存,以确保正确地复制和释放资源。

总之,C++ 的二进制读写对于简单的、只包含基本数据类型成员的类是有效的,但对于具有深浅拷贝概念的类,可能需要自定义序列化和反序列化方法来正确处理指针成员和动态分配的内存。

2.2.2. 文本读写

文本读写:对象数据序列化字符串写出来,读回来也是字符串,反序列化转成对象数据。

优点:可以看见写出是什么

缺点:存在一个转换过程,要慢一些

struct person_info
{
	std::string _name;
	int _age;
};

struct config_manager
{
public:
	config_manager(std::string filename = "log.txt")
		:_filename(filename)
	{}

	void text_write(const person_info& p_if)
	{
		// 向filename文件写数据
		// 以文本方式向文件写入数据
		std::ofstream ofs(_filename, std::ios::out | std::ios::trunc);
		// 都需要转化为字符串,然后向文件写入
		ofs.write(p_if._name.c_str(), p_if._name.size());
		ofs.put('\n');
		std::string str = std::to_string(p_if._age);
		ofs.write(str.c_str(), str.size());
	}

	void text_read(person_info& p_if)
	{
		std::ifstream ifs(_filename, std::fstream::in);
		// 由于写我们是一行一行写的
		// 因此可以一行一行的读取文件内容
		char buff[128] = { 0 };
		ifs.getline(buff, 128);
		p_if._name = buff;
		ifs.getline(buff, 128);
		p_if._age = std::stoi(buff);
	}
private:
	std::string _filename;
};

void Test13(void)
{
	person_info p_if{ "lisi", 20 };
	config_manager cmg;
	cmg.text_write(p_if);
}


void Test14(void)
{
	person_info p_if;
	config_manager cmg;
	cmg.text_read(p_if);
	std::cout << p_if._name << ":" << p_if._age << std::endl;
}

虽然可以正常读写,但是如果此时的信息又多了一些自定义类型呢?是不是感觉太麻烦了。而C++提供了新的操作,具体如下:

struct person_info
{
	std::string _name;
	int _age;
	Time _time;
};

struct config_manager
{
public:
	config_manager(std::string filename = "log.txt")
		:_filename(filename)
	{}

	void text_write(const person_info& p_if)
	{
		// 向filename文件写数据
		// 以文本方式向文件写入数据
		std::ofstream ofs(_filename, std::ios::out | std::ios::trunc);
		// 只要你这个类型支持了 operator<< 流插入 ,我就可以这样操作
		ofs << p_if._name << std::endl;
		ofs << p_if._age << std::endl;
		ofs << p_if._time << std::endl;
	}

	void text_read(person_info& p_if)
	{
		std::ifstream ifs(_filename, std::fstream::in);
		// 同理,只要你这个类型支持了 operator>>流提取,我可以如下操作
		ifs >> p_if._name >> p_if._age >> p_if._time;
	}
private:
	std::string _filename;
};

3. stringstream流的简单介绍

std::stringstream 是 C++ 标准库中的一个类,它允许将字符串作为流来处理。它基于 std::iostream,并提供了读写字符串的功能。

std::stringstream 类可以用于将数据从字符串中提取出来,或将数据写入到字符串中。它使用了与标准输入输出流 (std::cin 和 std::cout) 类似的接口和语法。

你可以通过 std::stringstream 对象执行以下操作:

  • 将数据插入到流中:使用 << 运算符将数据插入到流中,就像将数据输出到标准输出流一样。例如:myStream << "Hello World";
  • 从流中提取数据:使用 >> 运算符从流中提取数据,就像从标准输入流中提取数据一样。例如:myStream >> myVariable;
  • 获取流中的字符串:使用 str() 函数可以获取流中的字符串表示。例如:std::string result = myStream.str();

std::stringstream 类在处理字符串和数据的转换、解析等方面非常有用,

void Test15(void)
{
    // stringstream 既具有 ostringstream 也具有 istringstream的功能
	chat_info c_if = { "lisi", 101, { 18, 05, 45 }, "早上一起去跑步" };
	// 如何将上面的信息 转化为 字符串?
	// 只要这个类型重载了 operator<< 流插入即可
	std::ostringstream oss;
	// ostringstream 可以将数据 转化为 一个字符串
	oss << c_if._name << std::endl;
	oss << c_if._id << std::endl;
	oss << c_if._time << std::endl;
	oss << c_if._msg << std::endl;
	std::string str = oss.str();

	std::istringstream iss(str);
	// istringstream 可以将一个字符串转为原始数据
	chat_info new_c_if;
	// 只要这个类型重载了 operator>> 流提取即可
	iss >> new_c_if._name;
	iss >> new_c_if._id;
	iss >> new_c_if._time;
	iss >> new_c_if._msg;
}

你可能感兴趣的:(c++)