目录
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流
什么是流
"流"即是流动的意思,是物质从一处向另一处流动的过程,是对一种有序连续且具有方向性的数据( 其单位可以是bit、byte、packet )的抽象描述。
C++流是指信息从外部输入设备(如键盘)向计算机内部(如内存)输入和从内存向外部输出设备(显示器)输出的过程。这种输入输出的过程被形象的比喻为"流"。
它的特性是:有序连续、具有方向性。
为了实现这种流动,C++定义了I/O标准类库,这些每个类都称为流/流类,用以完成某方面的功能。
IO流标准库

C++系统实现了一个庞大的类库,其中ios为基类,其他类都是直接或间接派生自ios类。可以看到整个继承体系还是非常复杂的,中间还使用了菱形继承合并istream和ostream,让一个类既可以输入,也可以输出,然后又自iostream向下继承,实现fstream和stringstream。
我们总体看下来,其实这样的实现也对应了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;
为什么可以呢?我们看向cin的operator>>函数

返回的是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,那么主要用in和binary。
不论是构造还是open函数,默认标志位都是out,他表示输出模式,如果文件不存在,会创建新文件,如果文件已存在,默认会清空内容(除非指定其他模式),且只能写入,不能读取。默认参数是out,即使给了参数比如ate,既没有out,这时也会默认处理加上out。
in则表示输入模式,如果文件不存在,打开失败,只能读取,不能写入。
binary表示二进制模式,其不进行任何字符转换,只是单纯的输入输出字节。
ate表示初始位置在文件末尾,打开文件后,读写位置初始在文件末尾,其不同于app,之后可以用seekg / seekp移动位置。ofstream中很鸡肋,没什么用。
app表示追加模式,所有写入都自动在文件末尾进行,即使使用seekp移动位置,写入时仍会回到末尾,不会清空文件原有内容,打开文件时,如果不存在,会自动创建。
trunc表示打开文件时清空所有内容。out默认包含 trunc(除非同时有 in),不要和 app一起用(互相矛盾),文件不存在时:会创建新文件。
out、app、binary比较常用。
打开文件了之后,我们可以对文件进行写入了,但是对于文件来说,有两种打开方式,一种的文本模式,默认使用输出模式时就是文本模式,这时我们可以使用<<方便的对文件进行写入,<<可以将各种类型转换成字符串输出(自定义类型要重载operator <<).
当我们额外加上标志位binary时,我们就以二进制模式打开了文件流,那么这时我们应该使用函数write来进行二进制写入,即将数据原模原样地写入文本,那么这时因为内存的存储方式和人类地文字不同,这时我们就看不懂写入文件的内容了。之前的文本模式会自动进行转换,比如int类型的1转换成了char类型的1,这样我们查看文件就是1。文本模式和二进制模式各有优劣,二进制读写不用转换,肯定效率高,但是人看不懂,所以如果是日志这样地内容,那么文本相对好一些。
我们的ostream怎么关闭文件呢?使用close函数就行,
cpp
void close();
如果我们不去显示关闭,类销毁时也会自动调用。
值得注意的是因为是同一个类,所以即使我们以二进制打开,却还是能用operator <<,即使我们以文本模式打开,却还是能用write,那么这样串着用有问题吗?会有一些小问题。
文本模式(默认模式):
使用<<运算符时,数据会经过格式化输出。对于基本类型(如int、double等)会转换为字符串表示,并且可能会受到本地化设置的影响。对于字符串,会直接输出字符串内容,但要注意 <<运算符在输出字符指针时,直到遇到空字符为止,因此如果字符串中间有空字符 ,则只会输出空字符之前的部分。
使用write成员函数时,它不会进行任何格式化,而是将给定的内存块按原样写入文件。但是,在文本模式下,系统可能会对某些字符进行转换,例如在Windows平台上,换行符('\n')会被转换为回车换行对("\r\n")。这可能会破坏二进制数据的原始内容。
二进制模式:
使用<<运算符时,同样会进行格式化输出,但写入文件时不会进行字符转换(即不会将换行符'\n'转换为平台特定的换行序列)。但是,<<运算符通常用于输出文本格式的数据,所以如果你要输出二进制数据(比如一个整数的内存表示),那么使用 <<运算符可能会得到文本表示,而不是原始的二进制数据。例如,ofs << 123;会写入字符'1','2','3',而不是整数123的4字节(假设int为4字节)二进制表示。
使用write成员函数时,它不会进行任何格式化,直接按内存中的字节序列写入文件,并且由于是二进制模式,系统不会进行任何字符转换。因此,write通常用于写入二进制数据。
我们最好是在operator <<用来写入文本,write用来写入二进制,模式变了功能实现不会变,而且会因为模式的不同有一些差异,所以我们应该在对应的模式下使用对应的函数。
ifstream
ifstream和ofstream同属一套继承体系,设计理念也相同,所以介绍了ofstream,这里就简单介绍一下ifstream。
ifstream的构造,

默认打开方式是in,可以默认构造,之后要用open函数。
打开方式同样是有文本模式和二进制模式之分,其中同样不能混用operator>>和read,会有细小的转化问题,上面ofstream中有说。
我们来使用一下ofstream和ifstream。
二进制读写
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打开文件。其中in和out/app也可以混用。打开后前两者的功能fstream都能用。
除此之外我还想讲一下seekp、tellp、seekg、tellg。p就是put,即写指针,g就是get,即读指针。我们在ostream中有seekp、tellp,在istream中有seekg、tellg,在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流也分为ostringstream、istringstream、stringstream。简单演示一下使用方法,
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语言方便的地方。