C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C++ IO流

C++ IO流详解之标准IO、文件IO与字符串IO实战

作者:星河耀银海

本文详细介绍了C++中的IO流,包括标准IO流、文件IO流和字符串IO流,文章介绍了如何重载运算符以支持自定义类型的IO流操作,并提供了一些实战案例来说明如何使用IO流解决实际问题,感兴趣的朋友跟随小编一起看看吧

一、学习目标与重点

💡 核心重点:IO流类体系的继承关系、文件IO的打开模式与权限控制、格式控制符的灵活运用、字符串IO的数据解析技巧

二、C++ IO流概述

2.1 什么是IO流

IO(Input/Output)流是C++中处理输入输出的核心机制,核心思想是“将数据的传输抽象为流”——数据从源端(如键盘、文件、内存)流向目标端(如屏幕、文件、内存)的过程,称为“流”。

🗄️ 生活中的IO流类比:

C++ IO流的核心优势:

  1. 统一的接口:无论是控制台、文件还是内存,均通过相同的流操作(如>>输入、<<输出)实现,降低学习成本
  2. 灵活的格式控制:支持整数、浮点数、字符串等各种数据类型的格式化输入输出
  3. 可扩展性:支持自定义类型的IO流操作(重载>><<运算符)

2.2 C++ IO流类体系

C++标准库通过一系列类实现IO流功能,这些类主要定义在<iostream>(标准IO)、<fstream>(文件IO)、<sstream>(字符串IO)头文件中,核心类体系如下:

2.2.1 基类(抽象类)

2.2.2 核心流类

流类功能描述头文件常用对象/实例化方式
istream输入流基类,提供输入操作(>>运算符)<iostream>cin(标准输入流,对应键盘)
ostream输出流基类,提供输出操作(<<运算符)<iostream>cout(标准输出流,对应屏幕)、cerr(标准错误流)、clog(标准日志流)
iostream输入输出流类,继承自istreamostream<iostream>用于双向流操作(如文件读写)
ifstream文件输入流类,继承自istream<fstream>实例化对象读取文件
ofstream文件输出流类,继承自ostream<fstream>实例化对象写入文件
fstream文件输入输出流类,继承自iostream<fstream>实例化对象读写文件
istringstream字符串输入流类,继承自istream<sstream>从字符串中读取数据
ostringstream字符串输出流类,继承自ostream<sstream>向字符串中写入数据(格式化)
stringstream字符串输入输出流类,继承自iostream<sstream>对字符串进行读写操作

💡 类体系核心关系:

ios_base ← ios ← istream ← ifstream / istringstream
ios_base ← ios ← ostream ← ofstream / ostringstream
istream + ostream ← iostream ← fstream / stringstream

2.3 IO流的状态标志

IO流的操作结果(成功、失败、文件结束)通过“状态标志”标识,定义在ios_base中,核心状态标志如下:

状态标志描述检查方法
goodbit流状态正常(无错误、未到文件结束)stream.good()
eofbit到达文件结束(如读取文件到末尾)stream.eof()
failbit操作失败(如输入非预期类型、文件打开失败)stream.fail()
badbit严重错误(如内存访问错误、文件损坏)stream.bad()

💡 状态检查与重置:

⚠️ 警告:流操作失败后(如cin输入非数字),流会设置failbit,后续所有IO操作都会被忽略,需调用clear()重置状态,并处理错误数据。

三、标准IO流:控制台输入输出

标准IO流是指与控制台(键盘、屏幕)交互的流,核心对象包括cin(输入)、cout(输出)、cerr(错误输出)、clog(日志输出),定义在<iostream>头文件中。

3.1 标准输出流:cout、cerr、clog

3.1.1 核心区别

💡 示例:标准输出流对比

#include <iostream>
using namespace std;
int main() {
    cout << "cout输出:这是普通信息" << endl; // 换行并刷新缓冲区
    cerr << "cerr输出:这是错误信息" << endl; // 立即输出,不受缓冲区影响
    clog << "clog输出:这是日志信息" << endl; // 缓冲输出,与cout类似
    // 测试缓冲区:cout未刷新时,数据可能延迟输出
    cout << "cout无endl:"; 
    // 此时数据在缓冲区,未输出到屏幕
    cerr << "cerr插入:验证立即输出" << endl; 
    // 输出后,cout的缓冲区仍未刷新,直到程序结束或遇到endl
    return 0;
}

✅ 运行结果:

cout输出:这是普通信息
cerr输出:这是错误信息
clog输出:这是日志信息
cerr插入:验证立即输出
cout无endl:

3.1.2 格式控制

C++提供两种格式控制方式:格式控制符(如setwhex)和成员函数(如setfprecision),需包含<<iomanip>头文件(格式控制符专用)。

💡 常用格式控制(以cout为例):

#include <iostream>
#include <<iomanip> // 包含格式控制符
using namespace std;
int main() {
    int num = 255;
    double pi = 3.1415926535;
    string str = "C++ IO";
    // 1. 整数格式:十进制、八进制、十六进制
    cout << "十进制:" << dec << num << endl; // 255(默认)
    cout << "八进制:" << oct << num << endl; // 377(前缀无0)
    cout << "十六进制(小写):" << hex << num << endl; // ff
    cout << "十六进制(大写):" << hex << uppercase << num << endl; // FF
    cout << resetiosflags(ios::uppercase); // 重置大写格式
    // 2. 浮点数精度控制
    cout << "\n浮点数默认精度(6位有效数字):" << pi << endl; // 3.14159
    cout << "保留2位小数:" << fixed << setprecision(2) << pi << endl; // 3.14
    cout << "保留4位有效数字:" << resetiosflags(ios::fixed) << setprecision(4) << pi << endl; // 3.142
    // 3. 宽度与对齐
    cout << "\n左对齐(宽度10):" << left << setw(10) << str << "后续内容" << endl; // "C++ IO     后续内容"
    cout << "右对齐(宽度10):" << right << setw(10) << str << "后续内容" << endl; // "     C++ IO后续内容"
    cout << "填充字符(宽度10,填充*):" << setfill('*') << setw(10) << str << endl; // "*****C++ IO"
    // 4. 布尔值格式
    bool flag = true;
    cout << "\n布尔值默认:" << flag << endl; // 1
    cout << "布尔值文字(true/false):" << boolalpha << flag << endl; // true
    cout << resetiosflags(ios::boolalpha); // 重置布尔值格式
    return 0;
}

✅ 运行结果:

十进制:255
八进制:377
十六进制(小写):ff
十六进制(大写):FF

浮点数默认精度(6位有效数字):3.14159
保留2位小数:3.14
保留4位有效数字:3.142

左对齐(宽度10):C++ IO     后续内容
右对齐(宽度10):     C++ IO后续内容
填充字符(宽度10,填充*):*****C++ IO

布尔值默认:1
布尔值文字(true/false):true

3.2 标准输入流:cin

cinistream类的实例,用于从键盘读取数据,核心操作是>>运算符(提取运算符),支持各种基本数据类型的输入。

3.2.1 基本输入用法

#include <iostream>
#include <string>
using namespace std;
int main() {
    int a;
    double b;
    string s;
    cout << "请输入一个整数、一个浮点数和一个字符串(空格分隔):";
    cin >> a >> b >> s; // 按顺序读取,空格/回车作为分隔符
    cout << "\n你输入的整数:" << a << endl;
    cout << "你输入的浮点数:" << b << endl;
    cout << "你输入的字符串:" << s << endl;
    return 0;
}

✅ 运行示例:

请输入一个整数、一个浮点数和一个字符串(空格分隔):10 3.14 Hello
你输入的整数:10
你输入的浮点数:3.14
你输入的字符串:Hello

3.2.2 常见问题与解决方案

问题1:输入数据类型不匹配

当输入的数据类型与变量类型不一致时(如int变量输入字符串),cin会设置failbit,后续输入操作失效。

💡 解决方案:检查状态并重置

#include <iostream>
#include <limits> // 包含numeric_limits
using namespace std;
int main() {
    int num;
    cout << "请输入一个整数:";
    cin >> num;
    if (!cin) { // 等价于cin.fail(),输入失败
        cout << "输入错误!请输入整数类型。" << endl;
        cin.clear(); // 重置状态标志
        // 忽略缓冲区中所有错误数据(直到换行)
        cin.ignore(numeric_limits<streamsize>::max(), '\n');
    } else {
        cout << "你输入的整数:" << num << endl;
    }
    // 重新输入
    cout << "请重新输入一个整数:";
    cin >> num;
    cout << "你重新输入的整数:" << num << endl;
    return 0;
}

问题2:读取带空格的字符串

>>运算符默认以空格、回车、制表符为分隔符,无法读取带空格的字符串(如“Hello World”)。

💡 解决方案:使用getline()函数

#include <iostream>
#include <string>
using namespace std;
int main() {
    string name;
    cout << "请输入你的姓名(可带空格):";
    // 注意:若之前有cin >> 操作,需先忽略缓冲区中的换行符
    cin.ignore(numeric_limits<streamsize>::max(), '\n'); 
    getline(cin, name); // 读取整行数据,包括空格
    cout << "你好," << name << "!" << endl;
    return 0;
}

⚠️ 注意:cin >> 之后调用 getline() 时,cin >> 会将换行符留在缓冲区,getline() 会直接读取空字符串,因此需先用 cin.ignore() 忽略换行符。

问题3:读取指定长度的字符

使用cin.get(char* buf, int n)读取最多n-1个字符(最后一个字符为’\0’),或直到遇到指定分隔符。

💡 示例:读取指定长度的字符串

#include <iostream>
using namespace std;
int main() {
    char buf[10]; // 最多存储9个字符+1个'\0'
    cout << "请输入字符串(最多9个字符):";
    cin.get(buf, 10); // 读取最多9个字符,遇到换行或满9个字符停止
    cout << "你输入的字符串:" << buf << endl;
    return 0;
}

四、文件IO流:文件读写操作

文件IO流用于与磁盘文件进行交互,核心类是ifstream(读文件)、ofstream(写文件)、fstream(读写文件),需包含<fstream>头文件。

4.1 文件打开与关闭

4.1.1 打开文件的两种方式

  1. 构造函数打开:实例化流对象时,通过构造函数指定文件名和打开模式
  2. open()方法打开:先实例化流对象,再调用open(const string& filename, ios::openmode mode)方法

4.1.2 核心打开模式(ios::openmode)

文件打开模式用于指定文件的操作类型(读/写)、文件存在时的处理方式(覆盖/追加)等,可通过位或(|)组合使用:

打开模式描述适用类
ios::in读模式(文件必须存在,否则打开失败)ifstream、fstream
ios::out写模式(文件不存在则创建,存在则覆盖)ofstream、fstream
ios::app追加模式(文件不存在则创建,写入数据追加到文件末尾)ofstream、fstream
ios::trunc截断模式(文件存在则清空内容,默认与ios::out结合)ofstream、fstream
ios::binary二进制模式(默认是文本模式)所有文件流类
ios::ate打开文件后定位到文件末尾所有文件流类

💡 常用模式组合:

4.1.3 文件关闭

文件使用完毕后,需调用close()方法关闭文件,释放文件资源(如文件句柄)。即使不手动调用close(),流对象析构时也会自动关闭文件,但建议手动关闭(避免异常导致资源泄漏)。

⚠️ 警告:打开文件后必须检查是否打开成功,否则后续操作会失败。

4.2 文本文件读写

文本文件存储的是人类可读的字符(如ASCII码、UTF-8编码),读写操作与标准IO流类似(使用>><<getline())。

4.2.1 文本文件写入(ofstream)

💡 示例:创建文本文件并写入内容

#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
    // 1. 构造函数打开文件(写模式,覆盖原有内容)
    ofstream out_file("test.txt", ios::out | ios::trunc);
    // 检查文件是否打开成功
    if (!out_file.is_open()) {
        cerr << "文件打开失败!" << endl;
        return 1;
    }
    // 2. 写入数据(使用<<运算符)
    out_file << "Hello, File IO!" << endl;
    out_file << "这是C++文本文件写入示例" << endl;
    out_file << "整数:" << 100 << ",浮点数:" << 3.14 << endl;
    // 3. 写入字符串(使用write()方法,指定长度)
    string str = "使用write()方法写入的内容";
    out_file.write(str.c_str(), str.length());
    out_file << endl;
    // 4. 关闭文件
    out_file.close();
    cout << "文件写入完成!" << endl;
    return 0;
}

✅ 运行结果:生成test.txt文件,内容如下:

Hello, File IO!
这是C++文本文件写入示例
整数:100,浮点数:3.14
使用write()方法写入的内容

4.2.2 文本文件读取(ifstream)

文本文件读取有三种常用方式:>>运算符、getline()函数、read()方法。

💡 示例:读取文本文件内容

#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
    // 1. 打开文件(读模式)
    ifstream in_file("test.txt", ios::in);
    if (!in_file.is_open()) {
        cerr << "文件打开失败!" << endl;
        return 1;
    }
    cout << "=== 方式1:使用>>运算符读取(忽略空格和换行) ===" << endl;
    string word;
    while (in_file >> word) { // 直到文件结束(eofbit)
        cout << word << " ";
    }
    cout << endl;
    // 重置文件指针到开头(否则后续读取会从文件末尾开始)
    in_file.clear(); // 重置状态标志(因eofbit已设置)
    in_file.seekg(0, ios::beg); // 移动文件指针到开头(ios::beg=文件开头)
    cout << "\n=== 方式2:使用getline()读取整行 ===" << endl;
    string line;
    while (getline(in_file, line)) { // 逐行读取
        cout << line << endl;
    }
    in_file.clear();
    in_file.seekg(0, ios::beg);
    cout << "\n=== 方式3:使用read()读取指定字节数 ===" << endl;
    char buf[1024];
    in_file.read(buf, sizeof(buf)); // 读取最多1023个字符
    buf[in_file.gcount()] = '\0'; // gcount()返回实际读取的字节数,添加字符串结束符
    cout << buf << endl;
    // 关闭文件
    in_file.close();
    cout << "\n文件读取完成!" << endl;
    return 0;
}

✅ 运行结果:

=== 方式1:使用>>运算符读取(忽略空格和换行) ===
Hello, File IO! 这是C++文本文件写入示例 整数:100,浮点数:3.14 使用write()方法写入的内容 
=== 方式2:使用getline()读取整行 ===
Hello, File IO!
这是C++文本文件写入示例
整数:100,浮点数:3.14
使用write()方法写入的内容
=== 方式3:使用read()读取指定字节数 ===
Hello, File IO!
这是C++文本文件写入示例
整数:100,浮点数:3.14
使用write()方法写入的内容
文件读取完成!

4.3 二进制文件读写

二进制文件存储的是字节流(如图片、视频、可执行文件),读写时需指定ios::binary模式,使用read()write()方法(按字节操作)。

4.3.1 二进制文件写入

💡 示例:将结构体数据写入二进制文件

#include <iostream>
#include <fstream>
using namespace std;
// 定义结构体(存储用户信息)
struct User {
    char name[20]; // 用户名
    int age;       // 年龄
    double score;  // 分数
};
int main() {
    // 1. 打开二进制文件(写模式)
    ofstream out_file("users.bin", ios::out | ios::binary | ios::trunc);
    if (!out_file.is_open()) {
        cerr << "文件打开失败!" << endl;
        return 1;
    }
    // 2. 准备数据
    User user1 = {"张三", 20, 95.5};
    User user2 = {"李四", 22, 88.0};
    // 3. 写入数据(write()参数:数据地址、字节数)
    out_file.write(reinterpret_cast<const char*>(&user1), sizeof(User));
    out_file.write(reinterpret_cast<const char*>(&user2), sizeof(User));
    // 4. 关闭文件
    out_file.close();
    cout << "二进制文件写入完成!" << endl;
    return 0;
}

4.3.2 二进制文件读取

💡 示例:从二进制文件读取结构体数据

#include <iostream>
#include <fstream>
using namespace std;
struct User {
    char name[20];
    int age;
    double score;
};
int main() {
    // 1. 打开二进制文件(读模式)
    ifstream in_file("users.bin", ios::in | ios::binary);
    if (!in_file.is_open()) {
        cerr << "文件打开失败!" << endl;
        return 1;
    }
    // 2. 读取数据
    User user;
    cout << "=== 读取二进制文件内容 ===" << endl;
    // 循环读取,直到文件结束
    while (in_file.read(reinterpret_cast<char*>(&user), sizeof(User))) {
        cout << "用户名:" << user.name 
             << ",年龄:" << user.age 
             << ",分数:" << user.score << endl;
    }
    // 检查读取是否因文件结束而停止
    if (in_file.eof()) {
        cout << "读取完成(已到文件末尾)" << endl;
    } else if (in_file.fail()) {
        cerr << "读取失败!" << endl;
    }
    // 3. 关闭文件
    in_file.close();
    return 0;
}

✅ 运行结果:

=== 读取二进制文件内容 ===
用户名:张三,年龄:20,分数:95.5
用户名:李四,年龄:22,分数:88
读取完成(已到文件末尾)

⚠️ 注意事项:

4.4 文件指针操作

文件指针用于定位文件中的读写位置,分为输入指针(gptr,用于读操作)和输出指针(pptr,用于写操作),通过以下成员函数操作:

函数名功能描述参数说明
seekg(pos, mode)移动输入指针到指定位置pos:偏移量;mode:基准位置(ios::beg=开头、ios::cur=当前、ios::end=末尾)
seekp(pos, mode)移动输出指针到指定位置同seekg
tellg()获取输入指针当前位置(相对于文件开头的字节数)返回值:streampos类型(可转换为int)
tellp()获取输出指针当前位置同tellg

💡 示例:文件指针操作(读取文件中间内容)

#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
    ifstream in_file("test.txt", ios::in);
    if (!in_file.is_open()) {
        cerr << "文件打开失败!" << endl;
        return 1;
    }
    // 1. 获取文件大小(移动指针到末尾,获取位置)
    in_file.seekg(0, ios::end);
    int file_size = in_file.tellg();
    cout << "文件大小:" << file_size << "字节" << endl;
    // 2. 移动指针到文件中间(第20字节处)
    in_file.seekg(20, ios::beg);
    cout << "指针位置(第20字节):" << in_file.tellg() << endl;
    // 3. 读取从第20字节开始的内容
    string content;
    getline(in_file, content);
    cout << "从第20字节开始的内容:" << content << endl;
    in_file.close();
    return 0;
}

✅ 运行结果(假设test.txt第20字节后内容为“这是C++文本文件写入示例”):

文件大小:85字节
指针位置(第20字节):20
从第20字节开始的内容:这是C++文本文件写入示例

五、字符串IO流:内存数据格式化与解析

字符串IO流用于在内存中对字符串进行格式化输入输出,核心类是istringstream(从字符串读)、ostringstream(向字符串写)、stringstream(读写字符串),需包含<sstream>头文件。

字符串IO流的核心应用场景:

  1. 数据格式化:将多个基本类型数据拼接为字符串(如日志记录)
  2. 数据解析:将字符串按指定格式拆分为多个基本类型数据(如解析配置文件、网络数据)

5.1 ostringstream:字符串格式化输出

ostringstream用于将各种类型数据写入字符串(内存缓冲区),支持格式控制,最终通过str()方法获取格式化后的字符串。

💡 示例:格式化日志字符串

#include <iostream>
#include <sstream>
#include <<iomanip>
#include <string>
using namespace std;
int main() {
    // 1. 创建ostringstream对象
    ostringstream oss;
    // 2. 格式化写入数据(支持格式控制)
    string level = "INFO";
    string module = "FileHandler";
    int line = 100;
    double cost = 0.023;
    oss << "[" << level << "] "
        << "模块:" << module << ","
        << "行号:" << line << ","
        << "耗时:" << fixed << setprecision(4) << cost << "秒";
    // 3. 获取格式化后的字符串
    string log = oss.str();
    cout << "格式化日志:" << log << endl;
    // 4. 追加内容
    oss << ",状态:成功";
    cout << "追加后日志:" << oss.str() << endl;
    // 5. 清空缓冲区(重新使用)
    oss.str(""); // 清空字符串内容
    oss.clear(); // 重置状态标志
    oss << "[" << "ERROR" << "] " << "文件打开失败,路径:test.txt";
    cout << "新日志:" << oss.str() << endl;
    return 0;
}

✅ 运行结果:

格式化日志:[INFO] 模块:FileHandler,行号:100,耗时:0.0230秒
追加后日志:[INFO] 模块:FileHandler,行号:100,耗时:0.0230秒,状态:成功
新日志:[ERROR] 文件打开失败,路径:test.txt

5.2 istringstream:字符串解析输入

istringstream用于从字符串中读取数据,按指定格式拆分字符串为多个基本类型数据,支持>>运算符和getline()函数。

💡 示例1:解析空格分隔的字符串

#include <iostream>
#include <sstream>
#include <string>
using namespace std;
int main() {
    string data = "10 3.14 Hello C++";
    istringstream iss(data);
    // 解析数据
    int num;
    double pi;
    string str1, str2;
    iss >> num >> pi >> str1 >> str2;
    cout << "解析结果:" << endl;
    cout << "整数:" << num << endl;
    cout << "浮点数:" << pi << endl;
    cout << "字符串1:" << str1 << endl;
    cout << "字符串2:" << str2 << endl;
    return 0;
}

✅ 运行结果:

解析结果:
整数:10
浮点数:3.14
字符串1:Hello
字符串2:C++

💡 示例2:解析逗号分隔的配置字符串(如CSV数据)

#include <iostream>
#include <sstream>
#include <string>
using namespace std;
int main() {
    // 模拟CSV格式的配置数据:姓名,年龄,分数
    string config = "张三,20,95.5";
    istringstream iss(config);
    string field;
    // 按逗号分隔解析
    getline(iss, field, ','); // 读取第一个字段(姓名)
    string name = field;
    getline(iss, field, ','); // 读取第二个字段(年龄)
    int age = stoi(field); // 字符串转整数
    getline(iss, field, ','); // 读取第三个字段(分数)
    double score = stod(field); // 字符串转浮点数
    cout << "配置解析结果:" << endl;
    cout << "姓名:" << name << endl;
    cout << "年龄:" << age << endl;
    cout << "分数:" << score << endl;
    return 0;
}

✅ 运行结果:

配置解析结果:
姓名:张三
年龄:20
分数:95.5

5.3 stringstream:字符串读写双向操作

stringstream兼具istringstreamostringstream的功能,可对字符串进行读写操作,适用于需要先格式化再解析的场景。

💡 示例:字符串读写双向操作

#include <iostream>
#include <sstream>
#include <string>
using namespace std;
int main() {
    stringstream ss;
    // 1. 写入数据(格式化)
    ss << "姓名:" << "李四" << ",年龄:" << 22 << ",分数:" << 88.0;
    cout << "格式化字符串:" << ss.str() << endl;
    // 2. 读取解析(忽略非数字字符,提取年龄和分数)
    ss.clear(); // 重置状态标志
    ss.seekg(0, ios::beg); // 移动指针到开头
    string temp;
    int age;
    double score;
    // 跳过"姓名:李四,年龄:"等非数字内容
    while (ss >> temp) {
        if (temp == "年龄:") {
            ss >> age; // 提取年龄
        } else if (temp == "分数:") {
            ss >> score; // 提取分数
        }
    }
    cout << "解析结果:" << endl;
    cout << "年龄:" << age << endl;
    cout << "分数:" << score << endl;
    return 0;
}

✅ 运行结果:

格式化字符串:姓名:李四,年龄:22,分数:88
解析结果:
年龄:22
分数:88

六、IO流的高级应用:自定义类型的IO流

C++支持为自定义类型(如类、结构体)重载>>(输入)和<<(输出)运算符,使自定义类型可以像基本类型一样使用IO流操作。

6.1 重载<<运算符(输出)

重载<<运算符需定义为全局函数(或类的友元函数),参数为ostream&和自定义类型的引用,返回ostream&(支持链式调用)。

6.2 重载>>运算符(输入)

重载>>运算符同样定义为全局函数,参数为istream&和自定义类型的引用,返回istream&

💡 示例:自定义Person类的IO流重载

#include <iostream>
#include <string>
#include <<iomanip>
using namespace std;
class Person {
private:
    string name;
    int age;
    double height;
public:
    // 构造函数
    Person(string n = "", int a = 0, double h = 0.0) 
        : name(n), age(a), height(h) {}
    // 友元函数:重载<<运算符(输出)
    friend ostream& operator<<(ostream& os, const Person& p);
    // 友元函数:重载>>运算符(输入)
    friend istream& operator>>(istream& is, Person& p);
};
// 重载<<运算符:输出Person对象
ostream& operator<<(ostream& os, const Person& p) {
    os << "姓名:" << setw(8) << left << p.name
       << ",年龄:" << setw(3) << p.age
       << ",身高:" << fixed << setprecision(2) << p.height << "m";
    return os; // 支持链式调用(如cout << p1 << p2)
}
// 重载>>运算符:输入Person对象
istream& operator>>(istream& is, Person& p) {
    cout << "请输入姓名、年龄、身高(空格分隔):";
    is >> p.name >> p.age >> p.height;
    // 可选:输入验证
    if (p.age < 0 || p.age > 150) {
        is.setstate(ios::failbit); // 设置失败状态
        cerr << "输入错误:年龄非法!" << endl;
    }
    return is;
}
int main() {
    Person p1("张三", 20, 1.75);
    Person p2;
    // 输出自定义类型(使用重载的<<)
    cout << "p1信息:" << p1 << endl;
    // 输入自定义类型(使用重载的>>)
    if (cin >> p2) {
        cout << "p2信息:" << p2 << endl;
    }
    // 链式输出
    Person p3("李四", 22, 1.80);
    cout << "\n链式输出:" << p1 << " | " << p3 << endl;
    return 0;
}

✅ 运行结果:

p1信息:姓名:张三      ,年龄: 20,身高:1.75m
请输入姓名、年龄、身高(空格分隔):王五 25 1.85
p2信息:姓名:王五      ,年龄: 25,身高:1.85m

链式输出:姓名:张三      ,年龄: 20,身高:1.75m | 姓名:李四      ,年龄: 22,身高:1.80m

七、IO流的常见错误与最佳实践

7.1 常见错误

错误1:文件打开失败未检查

未检查文件是否打开成功,直接进行读写操作,导致程序崩溃或产生无效数据。

错误2:流状态异常未处理

cin输入类型不匹配、文件读取到末尾后继续读取,未重置流状态,导致后续IO操作失效。

错误3:二进制文件与文本文件模式混用

用文本模式读取二进制文件(如图片、视频),会导致数据解析错误(如换行符转换)。

错误4:字符串IO流未清空状态

重复使用stringstream时,未调用clear()重置状态标志,导致后续读取失败。

错误5:自定义类型IO流重载错误

重载<<>>时未返回ostream&istream&,导致链式调用失效。

7.2 最佳实践

实践1:始终检查文件打开状态

打开文件后,通过is_open()或流对象的布尔值判断是否打开成功。

实践2:及时处理流状态异常

IO操作后检查流状态(如if (!cin)),失败时调用clear()重置状态,并处理错误数据。

实践3:明确文件操作模式

实践4:合理使用缓冲区

实践5:资源管理

实践6:格式化输出优先使用ostringstream

拼接复杂字符串(如日志、配置)时,优先使用ostringstream,避免多次string拼接(效率低)。

八、实战案例:配置文件读写工具

8.1 问题描述

实现一个配置文件读写工具,支持:

  1. 读取INI格式的配置文件(键值对形式,如name=张三
  2. 解析配置项为字符串、整数、浮点数类型
  3. 修改配置项并写入文件(覆盖原有内容)
  4. 处理配置文件不存在、格式错误等异常场景

8.2 实现思路

  1. 定义ConfigReader类,封装配置文件的读取、解析、写入操作
  2. 使用map<string, string>存储配置项(键-值对)
  3. 读取配置文件:使用ifstream读取文件,istringstream解析每行的键值对
  4. 写入配置文件:使用ofstreammap中的键值对写入文件
  5. 提供getString()getInt()getDouble()方法获取不同类型的配置值
  6. 提供setConfig()方法修改配置项

8.3 代码实现

#include <iostream>
#include <fstream>
#include <sstream>
#include <map>
#include <string>
#include <stdexcept> // 包含异常类
using namespace std;
class ConfigReader {
private:
    string filename; // 配置文件名
    map<string, string> config_map; // 存储键值对
public:
    // 构造函数:指定配置文件名并读取配置
    ConfigReader(const string& fname) : filename(fname) {
        readConfig();
    }
    // 读取配置文件
    void readConfig() {
        ifstream in_file(filename, ios::in);
        if (!in_file.is_open()) {
            // 配置文件不存在,创建空配置(不抛出异常,允许后续写入)
            cout << "配置文件" << filename << "不存在,将创建新文件" << endl;
            return;
        }
        string line;
        int line_num = 0;
        while (getline(in_file, line)) {
            line_num++;
            // 跳过空行和注释行(以#开头)
            if (line.empty() || line[0] == '#') {
                continue;
            }
            // 解析键值对(格式:key=value)
            size_t eq_pos = line.find('=');
            if (eq_pos == string::npos) {
                cerr << "警告:第" << line_num << "行格式错误(无=),跳过该行" << endl;
                continue;
            }
            string key = line.substr(0, eq_pos);
            string value = line.substr(eq_pos + 1);
            // 去除键和值的前后空格
            key = trim(key);
            value = trim(value);
            if (!key.empty()) {
                config_map[key] = value;
            }
        }
        in_file.close();
        cout << "配置文件读取完成,共" << config_map.size() << "个配置项" << endl;
    }
    // 写入配置文件(覆盖原有内容)
    void writeConfig() {
        ofstream out_file(filename, ios::out | ios::trunc);
        if (!out_file.is_open()) {
            throw runtime_error("配置文件" + filename + "写入失败(权限不足或路径错误)");
        }
        // 写入注释
        out_file << "# 配置文件:" << filename << endl;
        out_file << "# 格式:key=value(#开头为注释,空行忽略)" << endl << endl;
        // 写入键值对
        for (const auto& pair : config_map) {
            out_file << pair.first << "=" << pair.second << endl;
        }
        out_file.close();
        cout << "配置文件写入完成!" << endl;
    }
    // 获取字符串类型配置
    string getString(const string& key, const string& default_val = "") const {
        auto it = config_map.find(key);
        return (it != config_map.end()) ? it->second : default_val;
    }
    // 获取整数类型配置
    int getInt(const string& key, int default_val = 0) const {
        auto it = config_map.find(key);
        if (it == config_map.end()) {
            return default_val;
        }
        // 字符串转整数
        istringstream iss(it->second);
        int val;
        if (!(iss >> val)) {
            cerr << "警告:配置项" << key << "的值" << it->second << "不是整数,返回默认值" << default_val << endl;
            return default_val;
        }
        return val;
    }
    // 获取浮点数类型配置
    double getDouble(const string& key, double default_val = 0.0) const {
        auto it = config_map.find(key);
        if (it == config_map.end()) {
            return default_val;
        }
        istringstream iss(it->second);
        double val;
        if (!(iss >> val)) {
            cerr << "警告:配置项" << key << "的值" << it->second << "不是浮点数,返回默认值" << default_val << endl;
            return default_val;
        }
        return val;
    }
    // 修改配置项
    void setConfig(const string& key, const string& value) {
        config_map[key] = value;
    }
private:
    // 辅助函数:去除字符串前后空格
    string trim(const string& str) {
        size_t start = str.find_first_not_of(" \t\n\r");
        size_t end = str.find_last_not_of(" \t\n\r");
        return (start == string::npos || end == string::npos) ? "" : str.substr(start, end - start + 1);
    }
};
int main() {
    try {
        // 1. 创建配置读取器(读取config.ini)
        ConfigReader config("config.ini");
        // 2. 获取配置项(带默认值)
        cout << "\n=== 读取配置项 ===" << endl;
        string name = config.getString("name", "未知");
        int age = config.getInt("age", 18);
        double score = config.getDouble("score", 60.0);
        string address = config.getString("address", "未设置");
        cout << "姓名:" << name << endl;
        cout << "年龄:" << age << endl;
        cout << "分数:" << score << endl;
        cout << "地址:" << address << endl;
        // 3. 修改配置项
        cout << "\n=== 修改配置项 ===" << endl;
        config.setConfig("name", "李四");
        config.setConfig("age", "22");
        config.setConfig("score", "95.5");
        config.setConfig("address", "北京市");
        // 4. 写入配置文件
        config.writeConfig();
        // 5. 重新读取验证
        cout << "\n=== 重新读取配置项 ===" << endl;
        config.readConfig(); // 重新读取文件
        cout << "姓名:" << config.getString("name") << endl;
        cout << "年龄:" << config.getInt("age") << endl;
        cout << "地址:" << config.getString("address") << endl;
    } catch (const exception& e) {
        cerr << "错误:" << e.what() << endl;
        return 1;
    }
    return 0;
}

8.4 运行结果

首次运行(config.ini不存在):

配置文件config.ini不存在,将创建新文件
配置文件读取完成,共0个配置项
=== 读取配置项 ===
姓名:未知
年龄:18
分数:60
地址:未设置
=== 修改配置项 ===
配置文件写入完成!
=== 重新读取配置项 ===
配置文件读取完成,共4个配置项
姓名:李四
年龄:22
地址:北京市

生成的config.ini文件内容:

# 配置文件:config.ini
# 格式:key=value(#开头为注释,空行忽略)
address=北京市
age=22
name=李四
score=95.5

再次运行(config.ini已存在):

配置文件读取完成,共4个配置项
=== 读取配置项 ===
姓名:李四
年龄:22
分数:95.5
地址:北京市
=== 修改配置项 ===
配置文件写入完成!
=== 重新读取配置项 ===
配置文件读取完成,共4个配置项
姓名:李四
年龄:22
地址:北京市

✅ 结论:该配置文件工具通过文件IO流实现了INI文件的读写,通过字符串IO流实现了配置项的解析和格式化,支持多种数据类型的配置获取,处理了文件不存在、格式错误等异常场景,符合IO流的最佳实践。

九、总结

  1. C++ IO流将输入输出抽象为“流”,通过统一的接口(>><<)实现控制台、文件、内存的交互,核心类体系基于ios_baseios派生。
  2. 标准IO流(cin、cout、cerr、clog)用于控制台交互,支持丰富的格式控制(如精度、对齐、进制)。
  3. 文件IO流(ifstream、ofstream、fstream)用于文件读写,文本文件使用>>/<</getline(),二进制文件使用read()/write(),需指定ios::binary模式。
  4. 字符串IO流(istringstream、ostringstream、stringstream)用于内存数据的格式化与解析,适用于日志拼接、配置解析等场景。
  5. 自定义类型可通过重载>><<运算符支持IO流操作,提升代码灵活性。
  6. 最佳实践:检查文件打开状态、处理流状态异常、明确文件模式、合理管理资源,避免常见错误。

通过本文学习,你应能熟练运用C++ IO流处理各种输入输出场景,解决实际开发中的文件读写、数据格式化、配置解析等问题。下一篇将深入探讨C++的多线程编程基础,开启并发编程的大门!

到此这篇关于C++ IO流详解:标准IO、文件IO与字符串IO实战的文章就介绍到这了,更多相关C++ IO流内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文