C++IO流讲解
本文详细介绍了C/C++中的IO流系统。首先讲解C语言的基本输入输出函数scanf和printf的使用方法,包括格式控制、宽度设置等技巧,并深入分析了缓冲区的概念及其作用。其次,文章阐述了C++面向对象的IO流体系,包括标准输入输出流对象(cin/cout)、文件流和字符串流的类关系图。重点探讨了流状态管理机制,通过示例代码展示了good()、eof()、fail()和bad()等状态检测方法的应
文章目录
C/C++IO流详解
1. C语言的输入与输出
1.1 基本输入输出函数
scanf() 函数,标准输入
// 基本用法
int num;
float f;
char str[100];
scanf("%d %f %s", &num, &f, str);
// 宽度控制
char name[11];
scanf("%10s", name); // 最多读取10个字符
// 格式控制
int a, b;
scanf("%d,%d", &a, &b); // 输入格式必须为 "10,20"
scanf("%d @ %d", &a, &b); // 输入格式必须为 "10 @ 20"
占位符(也叫格式符,printf相同,printf还提供特定格式,下面讲解):
%d - 整数
%f - 浮点数
%c - 字符
%s - 字符串
%lf - 双精度浮点数
%x - 十六进制整数
printf() 函数详解
// 简单格式控制
int num = 123;
double f = 3.14159;
printf("整数: %d, 浮点数: %.3f\n", num, f);
// 更复杂的格式控制
printf("%-10s: %08d\n", "ID", 123); // "ID : 00000123"
printf("价格: $%6.2f\n", 99.9); // "价格: $ 99.90"
运行结果:

printf常用格式控制
%-10s - 左对齐,宽度10
%10s - 右对齐,宽度10
%08d - 宽度8,不足补0
%.3f - 保留3位小数
%x - 十六进制输出
%p - 指针地址输出
1.2 缓冲区深入理解
缓冲区的类型
全缓冲:文件操作通常使用,缓冲区满时刷新
行缓冲:标准输入输出使用,遇到换行符时刷新
无缓冲:标准错误输出使用,立即输出
缓冲区的作用
当程序在进行IO(外设与内存间数据传输)的时候,会调用一些系统调用,具有一定的开销。
举个例子:当我们在进行寄快递的时候,先将快递交给快递站,快递站不会立即将我们的快递发出,而是等待,等待更多的人来寄快递,等到快递站的快递到达一定的数量,才会统一装车,统一运出去。这里的快递站就相当于一个缓冲区,我们如果每个人都让快递站给我们单独运送包裹,那对于整个快递系统来说,费时费力,几乎不可能实现。
我们在编写程序的时候,我们输出的数据不会从内存直接输出到外设(显示器,文件等),输入数据也同理,不会直接从外设输入(键盘,文件),而是有一个缓冲区,将内容先输入/输出在缓冲区里,输出缓冲区在等到具有一定的数据的时候就会刷新,即将数据输出。而输入缓冲区则是将输入的数据缓存起来,等待我们去拿。
#include <stdio.h>
int main() {
int a, b;
// 示例1:缓冲区中的数据会保留
printf("请输入两个整数: ");
scanf("%d", &a);
// 如果输入 "100 200",则200会留在缓冲区中
scanf("%d", &b); // 直接从缓冲区读取200
// 示例2:清空输入缓冲区
int c;
char ch;
scanf("%d", &c);
while((ch = getchar()) != '\n' && ch != EOF); // 每次读取一个字节,直到读取到结束符EOF
return 0;
}
缓冲区想要了解更细致,可以参考我的操作系统文章:Linux文件系统理解1,其中的第5部分为缓冲区理解
2. 流是什么
C++的IO流系统构建了一套完整的面向对象输入输出框架,以"流"这一抽象概念为核心,重新定义了数据在不同设备间的传输方式。与C语言中基于函数的scanf和printf不同,C++通过流类体系将输入输出操作封装为更加安全、灵活的面向对象的操作。
在标准IO流方面,C++提供了cin、cout、cerr和clog四个全局流对象,分别对应标准输入、标准输出、标准错误输出和日志输出。这些流对象通过运算符重载技术,不仅支持内置数据类型的直接读写,还能通过自定义重载>>和<<运算符来扩展对用户定义类型的支持。
文件IO流通过ifstream、ofstream和fstream等类,为文件操作提供了统一而便捷的接口。无论是文本文件还是二进制文件,都能通过相似的流操作语法进行读写,同时支持多种文件打开模式和错误状态检测。
字符串IO流,特别是stringstream类,解决了C语言中数值与字符串转换的安全性问题。它通过内存中的字符串缓冲区,提供了类型安全的格式化能力,避免了传统sprintf函数可能引发的缓冲区溢出风险,同时简化了复杂数据结构的序列化和反序列化过程。
3. C++IO流
3.1 IO流类库完整体系
C++通过面向对象的方式实现了一套完整的io体系,类图如下(图片来自网站:cpluscplus.com):

对于上图:
istream,ifstream,istringstream都是输入流
ostream,ofstream,ostringstream都是输出流
iostream,fstream,stringstream都是输入输出流,即具有输出流功能,也有输入流功能
输入输出状态表示

在ios_base类中设计了流状态,类似于位图结构,使用其中特定的比特位表示某一个状态,如上图所示。
| 表示 | good() | eof() | fail() | bad() | rdstate() |
|---|---|---|---|---|---|
| 无错误(零值iostate) | true | false | false | false | goodbit |
| 输入操作达到文件末尾 | false | true | false | false | eofbit |
| I/O操作中的逻辑错误 | false | false | true | false | failbit |
| I/O操作中的读/写错误 | false | false | true | truet | badbit |
failbit与badbit区别为,faibit发生的错误可以挽回,该流对象清空流状态还可以继续使用,badbit即致命错误,不可挽回
rdstate()表示获取对象错误标志,没有错误就返回goodbit
good()定义类似以下代码
bool basic_ios::good() const {
return rdstate() == goodbit;
}
输入Ctrl+z,是文件结束的标志符,流对象读到之后会讲eofbit设置为true,表示读到了文件结尾
当使用流对象进行一次IO后,该对象流状态被设置成相应的状态。
int main() {
int number;
int cnt = 10;
while(true)
{
std::cout << "请输入一个整数: ";
std::cin >> number;
if (std::cin.good()) {
std::cout << "流状态正常" << std::endl;
}
if (std::cin.eof()) {
std::cout << "到达文件末尾,退出程序" << std::endl;
exit(0);
}
if (std::cin.fail()) {
std::cout << "非致命错误发生" << std::endl;
std::cin.clear(); // 清除错误状态
std::cin.ignore(1000, '\n'); // 忽略错误输入
std::cout << "输入无效,已忽略" << std::endl;
}
if (std::cin.bad()) {
std::cout << "致命错误发生,退出程序" << std::endl;
exit(-1);
}
std::cout << std::endl;
}
return 0;
}
在上面的程序中依次输入
1 2 3 a b x ctrl+z
运行结果如下:

当输入为整形的时候则正常,输入为其他类型的时候就会错误。
全局流对象详细说明
cin (标准输入)
#include <iostream>
#include <limits>
int main() {
int age;
std::string name;
// 基本的输入
std::cout << "请输入姓名和年龄: ";
std::cin >> name >> age;
// 错误处理
if(std::cin.fail()) {
std::cin.clear(); // 清除错误状态
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 清空缓冲区
std::cout << "输入错误,请重新输入: ";
std::cin >> name >> age;
}
return 0;
}
cout (标准输出)
#include <iostream>
#include <iomanip>
int main() {
int num = 255;
double pi = 3.1415926535;
// 格式化输出
std::cout << "十进制: " << num << std::endl;
std::cout << "十六进制: " << std::hex << num << std::endl;
std::cout << "八进制: " << std::oct << num << std::endl;
// 控制精度
std::cout << "PI: " << std::setprecision(4) << pi << std::endl;
// 控制宽度和对齐
std::cout << std::setw(10) << std::left << "姓名"
<< std::setw(5) << std::right << "年龄" << std::endl;
std::cout << std::setw(10) << std::left << "张三"
<< std::setw(5) << std::right << 25 << std::endl;
return 0;
}
cerr 和 clog 的区别
#include <iostream>
#include <fstream>
int main() {
// cerr - 无缓冲,立即输出,用于错误信息
std::cerr << "这是一个错误信息" << std::endl;
// clog - 有缓冲,用于日志信息
std::clog << "这是一个日志信息" << std::endl;
// 重定向示例
std::ofstream logFile("log.txt");
std::streambuf* originalClogBuffer = std::clog.rdbuf();
std::clog.rdbuf(logFile.rdbuf()); // 重定向clog到文件
std::clog << "这条日志会写入文件" << std::endl;
// 恢复原来的缓冲区
std::clog.rdbuf(originalClogBuffer);
return 0;
}
3.2 自定义类型重载流插入/提取
重载流插入,作为成员函数重载时,this占据第一个参数,对象就会成为第一个操作数,d << cout ,所以不可以重载为成员函数,只能重载为全局的,达到 cout<<d的效果,但是访问不了私有成员,可以用友元声明。
要支持连续插入 cout<<d1<<d2,就需要增加返回值 (cout<<d1)<<d2, cout<<d1返回一个cout
cin与cout同理
#include <iostream>
using namespace std;
class Date
{
friend bool operator==(const Date& d1, const Date& d2); //声明该函数为Date的友元函数,负责在全局无法访问Date的private私有成员
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)
{
}
// >运算符重载
bool operator>(const Date& d)
{
if (_year > d._year) return true;
else if (_year == d._year)
{
if (_month > d._month)
return true;
if (_month == d._month && _day > d._day)
return true;
}
return false;
}
private:
//声明成员变量
int _year;
int _month;
int _day;
};
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
int main()
{
Date d1;
cout << "请输入一个日期, 空格隔开:" << endl;
cin >> d1; //流插入调用
cout << d1 << endl; //流提取调用
return 0;
}

3.3 文件IO流fstream应用
文件IO的时候,与标准IO基本相同,只不过需要我们带上文件路径和打开方式来进行打开文件
定义文件流对象
ifstream fin(“filename”, mod);
ofstream fout(“filename”, mod);
这里的mod对应文件流对象的几种模式,与C语言类似,下列表格详细列出
| 模式标志 | 适用流类型 | 作用描述 | 文件不存在时 | 文件存在时 | 常见组合与备注 |
|---|---|---|---|---|---|
| std::ios::in | ifstream, fstream | 以读取方式打开文件。 | 打开失败 | 打开文件,读指针在开头。 | ifstream 的默认模式。 |
| std::ios::out | ofstream, fstream | 以写入方式打开文件。 | 创建新文件 | 清空文件内容。 | ofstream 的默认模式。常与 trunc 组合。 |
| std::ios::app | ofstream, fstream | 追加模式。所有写入都追加到文件末尾。 | 创建新文件 | 打开文件,不清空,写指针在末尾。 | 与 out 同时使用。ate 和 app 的区别在于 app 每次写操作前都会定位到末尾。 |
| std::ios::ate | ifstream, ofstream, fstream | 打开文件后,立即将读/写指针定位到文件末尾。 | 创建新文件 | 打开文件,不清空,指针移到末尾。 | 初始位置在末尾,但之后可以自由移动指针进行读写。 |
| std::ios::trunc | ofstream, fstream | 如果文件已存在,则截断它(清空所有内容)。 | 创建新文件 | 清空文件内容。 | 如果只指定 out 而未指定 app/ate/in,则默认包含 trunc。 |
| std::ios::binary | 所有流类型 | 以二进制模式打开文件,禁止字符转换。 | - | - | 与任何模式组合使用。在Windows上尤为重要,可避免 \n 被转换为 \r\n。 |
这些模式标志定义在 std::ios_base 类中,可以在创建文件流对象(ifstream, ofstream, fstream)时,通过构造函数或调用 open() 成员函数时使用。它们可以通过位或操作 | 进行组合。
常用组合示例
以下是一些常见的模式组合及其实际效果:
| 组合方式 | 等效简写/说明 | 行为 |
|---|---|---|
| std::ios::in | std::ios::out | fstream 的常见默认(需手动指定) | 打开文件进行读写。文件必须存在,否则失败。不会清空。 |
| std::ios::out | std::ios::trunc | 仅 std::ios::out | 创建新文件或清空现有文件以进行写入。 |
| std::ios::out | std::ios::app | - | 创建新文件或打开现有文件,所有写入都追加到末尾。 |
| std::ios::in | std::ios::out | std::ios::trunc | - | 创建新文件或清空现有文件以进行读写。 |
| std::ios::in | std::ios::out | std::ios::app | - | 打开文件进行读写。读指针在开头,但每次写入前都会将写指针移到末尾。 |
| std::ios::in | std::ios::binary | - | 以二进制模式打开文件进行读取。 |
一些文件对象定义的时候会有默认的参数,默认行为如下:
ifstream: 默认为 in。
ofstream: 默认为 out(隐含 trunc,即会清空文件)。
fstream: 无默认模式,必须显式指定。
app vs ate:
app(Append): 是一种操作约束,所有写入必须在末尾进行。
ate(At End): 是一个初始位置,打开文件后指针在末尾,但之后可以随意移动指针到文件任何位置进行读写。
二进制模式: 在处理非文本文件(如图片、视频)或需要精确控制换行符时,务必使用 binary 模式。
代码示例:
#include <iostream>
#include <fstream>
using namespace std;
int main()
{
ofstream fcout("data.txt", ios::out); //定义ofstream对象,与cout相同,只不过cout是库里定义好
fcout << "this is a file message, three number list: " << endl; //
fcout << 20 <<" " << 22 << " " << 24 << endl;
fcout.close();//文件流对象在销毁的时候也会默认关闭文件,一些场景下也可以不手动关闭文件
char message[64];
int num1, num2, num3;
ifstream fcin("data.txt",ios::in);
fcin.getline(message, sizeof(message));
fcin >> num1 >> num2 >> num3;
fcin.close();
cout << message << num1 << " " << num2 << " " << num3 << endl;
ofstream fcout("data.txt", ios::out | ios::app); //追加形式打开
fcout << "this is second file message, three number list: " << endl; //
fcout << 30 <<" " << 40 << " " << 50 << endl;
fcout.close();
return 0;
}

文件data.txt内容:

3.4 字符串IOstringstream 应用
与文件IO流基本相同,但是在定义stringIO对象的时候,传入的是string对象(ostream会默认生成一个string对象,不传入参数也可以后续获取默认生成的),并非文件路径,之前从文件中进行读取/输出数据,现在在字符串中。
#include <sstream> // 包含字符串流头文件
#include <string>
using namespace std;
int main() {
// 将各种数据类型转换为字符串
cout << "数据类型转字符串" << endl;
ostringstream oss; // 输出字符串流,将数据输出到字符串,用于构建字符串
int age = 25;
double salary = 75000.50;
string name = "张三";
// 像使用 cout 一样将数据写入字符串流
oss << "姓名: " << name << ", 年龄: " << age << ", 薪资: " << salary;
// 获取构建好的完整字符串
string result = oss.str();
cout << result << endl << endl;
//从字符串解析数据
cout << "字符串解析数据" << endl;
string data = "100 98.5 你好";
istringstream iss(data); // 输入字符串流,用于解析字符串
int score1;
double score2;
string text;
// 像使用 cin 一样从字符串流读取数据
iss >> score1 >> score2 >> text;
cout << "整数: " << score1 << endl;
cout << "浮点数: " << score2 << endl;
cout << "字符串: " << text << endl << endl;
return 0;
}
运行结果:
4、C++IO接口汇总
4.1 通用 I/O 接口(适用于所有流类型)
基本状态检查接口
| 接口 | 说明 |
|---|---|
| good() | 流状态正常,可进行I/O操作 |
| eof() | 已到达文件/流末尾 |
| fail() | 最近的操作失败(但流未完全损坏) |
| bad() | 流已损坏,无法继续使用 |
| clear() | 清除错误状态标志 |
| rdstate() | 返回当前流状态 |
格式化输入/输出接口
| 接口 | 说明 |
|---|---|
| << 操作符 | 格式化输出(插入器) |
| >> 操作符 | 格式化输入(提取器) |
| setf(), unsetf() | 设置/清除格式标志 |
| precision() | 设置浮点数精度 |
| width() | 设置字段宽度 |
| fill() | 设置填充字符 |
定位接口
| 接口 | 说明 |
|---|---|
| tellg() | 获取输入流当前位置 |
| tellp() | 获取输出流当前位置 |
| seekg(pos) | 设置输入流位置 |
| seekp(pos) | 设置输出流位置 |
| seekg(off, dir) | 设置输入流相对位置 |
| seekp(off, dir) | 设置输出流相对位置 |
4.2 文件 I/O 特殊接口
文件流特有接口(fstream, ifstream, ofstream)
| 接口 | 说明 |
|---|---|
| open(filename, mode) | 打开文件 |
| close() | 关闭文件 |
| is_open() | 检查文件是否成功打开 |
| 构造函数接受文件名 | 创建时直接打开文件 |
文件打开模式
| 模式 | 说明 |
|---|---|
| ios::in | 以读方式打开 |
| ios::out | 以写方式打开 |
| ios::app | 追加模式 |
| ios::ate | 打开后定位到末尾 |
| ios::trunc | 截断文件 |
| ios::binary | 二进制模式 |
4.3 字符串 I/O 特殊接口
| 接口 | 说明 |
|---|---|
| str() | 获取底层字符串 |
| str(string) | 设置底层字符串内容 |
| 构造函数接受字符串 | 创建时初始化内容 |
4.4 特殊 I/O 接口
非格式化 I/O 接口
| 接口 | 说明 | 适用流 |
|---|---|---|
| get() | 读取单个字符 | 输入流 |
| getline() | 读取一行 | 输入流 |
| put(char) | 写入单个字符 | 输出流 |
| read(char*, size) | 读取二进制数据 | 输入流 |
| write(char*, size) | 写入二进制数据 | 输出流 |
| gcount() | 返回上次读取的字符数 | 输入流 |
缓冲区操作接口
| 接口 | 说明 |
|---|---|
| flush() | 刷新输出缓冲区 |
| sync() | 同步输入缓冲区 |
| peek() | 查看下一个字符但不提取 |
| putback(char) | 将字符放回输入流 |
| unget() | 回退一个字符 |
更多推荐

所有评论(0)