【C++】IO流

目录

C语言中的输入与输出

在C语言中,我们输入输出是使用的printf / scanf系列接口

其中最常用的就是

cpp 复制代码
// 文件
int fprintf(FILE* stream, const char* format, ...);
int fscanf(FILE* stream, const char* format, ...);
// 标准输入输出设备(键盘、屏幕)
int printf(const char* format, ...);
int scanf(const char* format, ...);
// 字符串
int sprintf(char* str, const char* format, ...);
int sscanf(const char* s, const char* format, ...);

也对应了我们最常用的三个场景,文件,标准设备、字符串。

C++的IO流

什么是流

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

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

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

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

IO流标准库

C++系统实现了一个庞大的类库,其中ios为基类,其他类都是直接或间接派生自ios类。可以看到整个继承体系还是非常复杂的,中间还使用了菱形继承合并istreamostream,让一个类既可以输入,也可以输出,然后又自iostream向下继承,实现fstreamstringstream

我们总体看下来,其实这样的实现也对应了C语言中的文件,标准设备、字符串。这种继承的设计风格类似系统编程中的write / read函数,以linux为例,一切皆文件,用文件描述符(fd)作为句柄,操作一切IO。这里的C++也是秉承着这样的设计理念,一切皆流,通过继承让流操作所有IO。

C++的IO流和C语言的接口相比,通过运算符重载<<>>,我们可以实现各种自定义类型的输入输出操作,函数重载也支持自动识别类型,<<>>对于面向对象来说也比较形象。

C++类型转换运算符函数

在C++中,我们可以实现这样的操作,

cpp 复制代码
std::string str;
while (std::cin >> str)
	std::cout << str << std::endl;

为什么可以呢?我们看向cinoperator>>函数

返回的是istream引用,为了实现多个>>链式调用操作。那么这个类型的对象怎么转换成bool类型的呢?因为istream对象内部实现了operator bool(),它是类型转换运算符函数,它允许将自定义类对象隐式或显式地转换为 bool类型。

其类内部有一个标志位,记录了各种flag,如果是error flag,那么就返回false,否则就是true。注意这个函数中的explicit的意思不是防止单参构造函数的隐式类型转换,而是防止自己这个类型的对象被隐式转换成另一种类型,加了这个关键字,那就只能显示转换。

cpp 复制代码
class A
{
	int _a = 2;
public:
	A() {}
	explicit operator int()
	{
		return _a;
	}
};

int main()
{
	A a;
	//int b = a;
	int b = static_cast<int>(a);
	std::cout << b << std::endl;
	return 0;
}

上面定义的是operator int,想转什么类型都能写(自定义也行)。

文件IO流

我们可以这样使用文件IO流,

cpp 复制代码
std::ofstream ofs("log.txt");
ofs << "hello_world";
ofs.close();
std::ifstream ifs("log.txt");
std::string str;
ifs >> str;
std::cout << str << std::endl;
ofstream

ofstream是文件输出流,

创建方式有好几种,我们可以选择默认构造,然后使用open函数打开。

或者我们直接在构造函数中给明参数打开,我们也能调用close函数关闭文件流,

注意,如果已经打开了文件,调用open会失败,反之调用close会失败。

在ofstream中,调用函数失败的话,不会返回值,不会抛异常,而是会设置错误状态,ostream分别有这几种错误状态,

可以看到,每一种错误状态都有一个函数可以单独检查,然后有一个good函数,它会返回当前ostream对象有没有任何一个错误状态被设置,有的话就返回false,没有一个被设置就返回true。rdstate则可以返回当前ostream的错误标志,这些标志以类似位图的形式存储,不同标志位按位或在一起,如果有多个,那么返回的就是多个标志位或在一起的结果。

创建对象或调用open时,我们可以选择文件的打开方式,这同样也是一个个标志位,多个的话可以按位或在一起,即

如果是ofstream,那么除了in之外都能用,如果是ifstream,那么主要用inbinary

不论是构造还是open函数,默认标志位都是out,他表示输出模式,如果文件不存在,会创建新文件,如果文件已存在,默认会清空内容(除非指定其他模式),且只能写入,不能读取。默认参数是out,即使给了参数比如ate,既没有out,这时也会默认处理加上out

in则表示输入模式,如果文件不存在,打开失败,只能读取,不能写入。

binary表示二进制模式,其不进行任何字符转换,只是单纯的输入输出字节。

ate表示初始位置在文件末尾,打开文件后,读写位置初始在文件末尾,其不同于app,之后可以用seekg / seekp移动位置。ofstream中很鸡肋,没什么用。

app表示追加模式,所有写入都自动在文件末尾进行,即使使用seekp移动位置,写入时仍会回到末尾,不会清空文件原有内容,打开文件时,如果不存在,会自动创建。

trunc表示打开文件时清空所有内容。out默认包含 trunc(除非同时有 in),不要和 app一起用(互相矛盾),文件不存在时:会创建新文件。

outappbinary比较常用。

打开文件了之后,我们可以对文件进行写入了,但是对于文件来说,有两种打开方式,一种的文本模式,默认使用输出模式时就是文本模式,这时我们可以使用<<方便的对文件进行写入,<<可以将各种类型转换成字符串输出(自定义类型要重载operator <<).

当我们额外加上标志位binary时,我们就以二进制模式打开了文件流,那么这时我们应该使用函数write来进行二进制写入,即将数据原模原样地写入文本,那么这时因为内存的存储方式和人类地文字不同,这时我们就看不懂写入文件的内容了。之前的文本模式会自动进行转换,比如int类型的1转换成了char类型的1,这样我们查看文件就是1。文本模式和二进制模式各有优劣,二进制读写不用转换,肯定效率高,但是人看不懂,所以如果是日志这样地内容,那么文本相对好一些。

我们的ostream怎么关闭文件呢?使用close函数就行,

cpp 复制代码
void close();

如果我们不去显示关闭,类销毁时也会自动调用。

值得注意的是因为是同一个类,所以即使我们以二进制打开,却还是能用operator <<,即使我们以文本模式打开,却还是能用write,那么这样串着用有问题吗?会有一些小问题。

文本模式(默认模式):

使用<<运算符时,数据会经过格式化输出。对于基本类型(如intdouble等)会转换为字符串表示,并且可能会受到本地化设置的影响。对于字符串,会直接输出字符串内容,但要注意 <<运算符在输出字符指针时,直到遇到空字符为止,因此如果字符串中间有空字符 ,则只会输出空字符之前的部分。

使用write成员函数时,它不会进行任何格式化,而是将给定的内存块按原样写入文件。但是,在文本模式下,系统可能会对某些字符进行转换,例如在Windows平台上,换行符('\n')会被转换为回车换行对("\r\n")。这可能会破坏二进制数据的原始内容。

二进制模式:

使用<<运算符时,同样会进行格式化输出,但写入文件时不会进行字符转换(即不会将换行符'\n'转换为平台特定的换行序列)。但是,<<运算符通常用于输出文本格式的数据,所以如果你要输出二进制数据(比如一个整数的内存表示),那么使用 <<运算符可能会得到文本表示,而不是原始的二进制数据。例如,ofs << 123;会写入字符'1','2','3',而不是整数123的4字节(假设int为4字节)二进制表示。

使用write成员函数时,它不会进行任何格式化,直接按内存中的字节序列写入文件,并且由于是二进制模式,系统不会进行任何字符转换。因此,write通常用于写入二进制数据。

我们最好是在operator <<用来写入文本,write用来写入二进制,模式变了功能实现不会变,而且会因为模式的不同有一些差异,所以我们应该在对应的模式下使用对应的函数。

ifstream

ifstreamofstream同属一套继承体系,设计理念也相同,所以介绍了ofstream,这里就简单介绍一下ifstream

ifstream的构造,

默认打开方式是in,可以默认构造,之后要用open函数。

打开方式同样是有文本模式和二进制模式之分,其中同样不能混用operator>>read,会有细小的转化问题,上面ofstream中有说。

我们来使用一下ofstreamifstream

二进制读写

cpp 复制代码
struct Date
{
	int _year;
	int _month;
	int _day;
	void PRINT()
	{
		std::cout << _year << "-" << _month << "-" << _day << std::endl;
	}
};

int main()
{
	std::ofstream ofs("log.txt", std::ios_base::out | std::ios_base::binary);
	Date d{ 2026, 1, 19 };
	ofs.write((const char*)&d, sizeof(d));
	ofs.close(); // 先手动关闭,写入才会生效,不然因为打开时清空了文件,输出为空
	std::ifstream ifs("log.txt", std::ios_base::in | std::ios_base::binary);
	Date ret;
	ifs.read((char*)&ret, sizeof(ret));
	ret.PRINT();
	//打印结果: 2026-1-19
	return 0;
}

文本格式查看log.txt是一堆乱码,因为是二进制写入。

文本读写

cpp 复制代码
struct Date
{
	int _year;
	int _month;
	int _day;
	void PRINT()
	{
		std::cout << _year << "-" << _month << "-" << _day << std::endl;
	}
};

int main()
{
	std::ofstream ofs("log.txt", std::ios_base::out);
	Date d{ 2026, 1, 19 };
	ofs << d._year << " " << d._month << " " << d._day << std::endl;
	ofs.close(); // 先手动关闭,写入才会生效,不然因为打开时清空了文件,输出为空
	std::ifstream ifs("log.txt", std::ios_base::in);
	Date ret;
	ifs >> ret._year >> ret._month >> ret._day;
	ret.PRINT();
	//打印结果: 2026-1-19
	return 0;
}

我们在写入时在一个个数据之间加上空格(或'\n''\t'),这样读取时因为遇到空格会停止读写然后跳过它,所以我们就能实现连续写入。而且我们也能看到文本读取那里因为因为实现了多种类型的operator>>,所以可以实现各种内置类型的写入,如果我们想要写入自定义类型,我们实现对应类型的operator>>就行。

fstream

fstream是对前两者的合并,我们日常用的也挺多,上述的标志位都可以用来初始化或open打开文件。其中inout/app也可以混用。打开后前两者的功能fstream都能用。

除此之外我还想讲一下seekptellpseekgtellg。p就是put,即写指针,g就是get,即读指针。我们在ostream中有seekptellp,在istream中有seekgtellg,在fsteram中,当然是全都有。

读写指针代表当前的读写位置,这里以写指针为例:
seekp是文件位置定位函数,用于在输出文件流中移动写入位置。

cpp 复制代码
ostream& seekp (streampos pos);
ostream& seekp (streamoff off, ios_base::seekdir way);

第一个函数是移动到绝对位置,即参数给5,那就移动到从0开始的第五个位置,第二个函数是移动到相对位置,这里的ios_base::seekdir可以认为是一个枚举,分别有

对应着开始、结束、当前位置,我们加上对应的参数,然后给出偏移量,就能移动相对位置。

tellp则是告知当前读指针的位置,

cpp 复制代码
streampos tellp();

值得注意的是,fstream打开方式指定std::ios::in | std::ios::out时并不会清空文件(因为要兼顾in),除非指定。那么则就解决了之前的一个问题,那就是ostream打开文件只默认的话会清空文件内容,指定app的话移动指针也不能写入,指定ate还是会清空(app又包含ate,合并使用无意义),总之就是说没法修改原有文件的指定位置的部分内容,要么清空重写,要么追加,这也和ostream的设计目的有关,只写模式,就只实现了这样的功能。而上面所说的fstream的特性就解决了这样的问题,因为其是读写模式,会更加灵活,而要实现修改原有文件的指定位置的部分内容的作用,我们势必用到seekp。我们来演示一下,

cpp 复制代码
std::fstream fs("log.txt", std::ios::in | std::ios::out);
fs << "hello_world" << std::endl;
fs.seekp(5);
fs << "hello_world" << std::endl;

log.txt

bash 复制代码
hellohello_world

我们还可以使用seekg来获取文件大小,

cpp 复制代码
std::fstream fs("log.txt", std::ios::in | std::ios::out | std::ios::ate);
std::cout << fs.tellg() << std::endl;

因为加了ate标志位,所以起始读写指针在文件尾,然后这时的指针位置就是文件大小(按字节),当然这里用p指针也行,但是由于是读文件大小,g指针更合适。

当然如果时顺便操作文件时用这么一出算一下文件大小是可以的,如果不是操作文件,那么搞这么一出有点麻烦,很多人也这么吐槽C++,测个文件大小这么麻烦,所以C++17出了直接获得指定文件的接口。

cpp 复制代码
std::fstream fs("log.txt", std::ios::in | std::ios::out | std::ios::ate);
auto size = std::filesystem::file_size("log.txt");
std::cout << size << " " << fs.tellp() << std::endl;

可以看到结果是一样的。

字符串IO流

字符串IO流也分为ostringstreamistringstreamstringstream。简单演示一下使用方法,

cpp 复制代码
std::ostringstream oss;
std::istringstream iss("hello world 2");
oss << "hello world " << 1;
std::cout << oss.str() << std::endl;
std::string str1, str2;
int a;
std::cout << iss.str() << std::endl;
iss >> str1 >> str2 >> a;
std::cout << str1 << " " <<  str2 << " " << a << std::endl;
std::cout << iss.str() << std::endl;
//打印结果:
//hello world 1
//hello world 2
//hello world 2
//hello world 2
//

基本一看就会,很简单,学完上面的可以说看几眼就知道怎么用了。字符串IO流不仅简单,而且好用,在格式化输出字符串时很好用,比如网络编程中。从这里我们也能更加明白<<>>都是跟字符串打交道,通过重载函数完成各种转化,这也是比C语言方便的地方。

相关推荐
froginwe112 小时前
C 语言输入与输出详解
开发语言
_童年的回忆_3 小时前
【PHP】关于守护进程报错:SQLSTATE[HY000]: General error: 2006 MySQL server has gone away
开发语言·oracle·php
少林码僧3 小时前
2.30 传统行业预测神器:为什么GBDT系列算法在企业中最受欢迎
开发语言·人工智能·算法·机器学习·ai·数据分析
CoderCodingNo3 小时前
【GESP】C++六级考试大纲知识点梳理, (7) 栈与队列
开发语言·c++
edisao3 小时前
六、 读者高频疑问解答 & 架构价值延伸
大数据·开发语言·人工智能·科技·架构·php
超级大福宝3 小时前
【力扣200. 岛屿数量】的一种错误解法(BFS)
数据结构·c++·算法·leetcode·广度优先
范纹杉想快点毕业3 小时前
C语言实现埃拉托斯特尼筛法
c语言·开发语言
catchadmin3 小时前
Laravel12 + Vue3 的免费可商用 PHP 管理后台 CatchAdmin V5.1.0 发布 新增 AI AGENTS 配置
开发语言·php