C++的IO流

文章目录

C++的IO流

C语言的输入输出与缓冲区

C语言中最常见的输入输出方式是 scanf() 与 printf()。scanf() 从标准输入设备(通常是键盘)读取数据,并按格式说明符把值存入变量;printf() 把指定文字或格式化数据输出到标准输出设备(通常是屏幕)。C语言输入输出并不是直接和硬件交互,而是借助输入缓冲区、输出缓冲区完成数据中转。缓冲区的存在可以屏蔽底层低级 I/O 的实现差异,也能支持"按行读取"等更符合人类习惯的行为。

流是什么

"流"可以理解为数据从一处流向另一处的抽象过程。C++ 中的流是对"有序、连续、具有方向性的数据"的抽象描述,数据单位可以是 bit、byte、packet 等。输入流表示数据从外部设备流向内存,输出流表示数据从内存流向外部设备。C++ 通过流类库把这种输入输出过程封装成对象,使程序员可以用统一方式操作键盘、屏幕、文件和字符串。

流的核心特性:是"有序连续"和"具有方向性"。有序连续意味着数据按照一定顺序到达或写出,不能随意跳跃;方向性意味着输入流和输出流职责不同,输入流用于提取数据,输出流用于插入数据。

cpp 复制代码
#include <iostream>

int main() 
{
    int x = 0;
    std::cout << "请输入一个整数:";
    std::cin >> x;              // 数据从键盘流向内存
    std::cout << "x = " << x << std::endl; // 数据从内存流向屏幕
    return 0;
}

C++ IO流

C++ 系统实现了一个庞大的 IO 类库,其中 ios 是基类,其他流类大多直接或间接派生自 ios。标准输入输出流、文件流、字符串流都建立在这套继承体系之上。

C++ 标准IO流

C++ 标准库提供了四个全局标准流对象:cin、cout、cerr、clog。cin 是 istream 类型对象,用于标准输入;cout、cerr、clog 是 ostream 类型对象,用于标准输出、错误输出和日志输出。它们的基本插入/提取方式相似,但应用场景和缓冲策略不同:cerr 通常用于立即显示错误信息,clog 通常用于带缓冲的日志输出。使用这些对象需要包含 <iostream> 并引入 std 命名空间。

cpp 复制代码
int main() 
{
    int x = 10;

    std::cout << "标准输出:x = " << x << std::endl;
    std::cerr << "标准错误:用于立即提示错误" << std::endl;
    std::clog << "日志输出:用于记录日志" << std::endl;

    std::cin >> x;
    std::cout << "重新输入后 x = " << x << std::endl;
    return 0;
}

注意:

  • cout、cerr、clog 都是 ostream 对象,但 cerr 通常不带缓冲或立即刷新,适合错误提示;clog 带缓冲,适合日志。不要简单认为三者完全等价。
  • 标准流对象是全局对象,使用时必须确保包含正确头文件并正确使用命名空间。
  • endl不仅输出换行,还会刷新缓冲区;频繁使用可能影响性能,普通换行可用 \n。
cin 缓冲与输入规则

cin 是缓冲流:输入先进入输入缓冲区,再由 cin 提取;一次输入过多,剩余数据留在缓冲区供后续读取。回车前可修改,回车后通常无法撤回。提取类型必须匹配,否则会置流状态位,程序可继续但后续提取可能失败。空格和回车可作分隔符,多数据可一行或分行输入;但用 >> 读字符/字符串时不能读空格,也不能直接读回车。

cpp 复制代码
int main() 
{
    int id = 0;
    std::string line;

    std::cout << "请输入编号:";
    std::cin >> id;

    // 清除输入缓冲区中残留的换行符,否则 getline 可能直接读到空行
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');

    std::cout << "请输入一整行内容:";
    std::getline(std::cin, line);

    std::cout << "id=" << id << ", line=" << line << std::endl;
    return 0;
}
循环输入与 operator bool

在线编程题中经常需要循环输入。while (cin >> a) 能成立的原因是:operator>> 返回 istream&,而 istream 提供了 explicit operator bool() const,当流读取成功时转换为 true,当读取失败或到达文件末尾时转换为 false。因此 while (cin >> a) 既完成了输入,又完成了循环条件判断。类似地,可以写 while (cin >> a >> b >> c) 连续读取多个元素。

cpp 复制代码
int main() 
{
    int x = 0;

    // 单个元素循环输入,Windows 下 Ctrl+Z 结束,Linux 下 Ctrl+D 结束
    while (std::cin >> x) {
        std::cout << "x * 2 = " << x * 2 << std::endl;
    }

    std::cout << "输入结束" << std::endl;
    return 0;
}
自定义类型重载流插入和流提取

C++ 标准库已经为所有内置类型重载了<<和 >>,所以 cin、cout 可以直接输入输出 int、double 等类型。对于自定义类型,如果要支持标准输入输出,需要自己重载流插入运算符 << 和流提取运算符 >>。通常把它们写成全局函数,并声明为类的友元,以便访问私有成员;返回值必须是流引用,才能支持链式操作和 while (cin >> d) 这类写法。

cpp 复制代码
class Date {
    friend std::ostream& operator<<(std::ostream& out, const Date& d);
    friend std::istream& operator>>(std::istream& in, Date& d);
public:
    Date(int year = 1, int month = 1, int day = 1)
        : _year(year), _month(month), _day(day) {}

    explicit operator bool() const {
        // 示例规则:年份为 0 时认为无效,可用于结束循环
        return _year != 0;
    }

private:
    int _year;
    int _month;
    int _day;
};

std::istream& operator>>(std::istream& in, Date& d) {
    in >> d._year >> d._month >> d._day;
    return in;
}

std::ostream& operator<<(std::ostream& out, const Date& d) {
    out << d._year << "-" << d._month << "-" << d._day;
    return out;
}

int main() {
    Date d(2022, 4, 10);
    std::cout << d << std::endl;

    while (std::cin >> d) { // 这里调用的是istream库
        if (!d) { // 这里调用 Date::operator bool()
            break;
        }
        std::cout << d << std::endl;
    }
    return 0;
}

C++文件IO流

C++ 根据文件内容的数据格式,通常分为二进制文件和文本文件。文件流操作的一般步骤是:定义文件流对象、打开磁盘文件建立联系、使用提取/插入运算符或成员函数读写、关闭文件。ifstream 用于只输入,ofstream 用于只输出,fstream 用于既输入又输出。

文件流对象与打开模式

文件流对象把磁盘文件抽象成流,使我们可以像操作cin、cout 一样操作文件。打开文件时可以指定模式,例如 ios::in 表示读取,ios::out 表示写出,ios::binary 表示二进制模式,ios::trunc 表示截断文件,ios::app 表示追加。ifstream 默认包含输入模式,ofstream 默认包含输出模式并通常会截断已有文件。工程代码中必须检查文件是否成功打开。

cpp 复制代码
#include <fstream>
#include <iostream>
#include <string>

int main() {
    std::ofstream ofs("demo.txt", std::ios::out | std::ios::trunc);
    if (!ofs) {
        std::cerr << "打开输出文件失败\n";
        return 1;
    }
    ofs << "hello\n" << "world\n";
    ofs.close();

    std::ifstream ifs("demo.txt");
    if (!ifs) {
        std::cerr << "打开输入文件失败\n";
        return 1;
    }

    std::string line;
    while (std::getline(ifs, line)) {
        std::cout << line << std::endl;
    }
    return 0;
}

注意:

  • 使用文件流后应检查 is_open() 或直接判断流对象,否则文件不存在、权限不足时后续读写会失败。
  • ofstream 默认可能截断文件,如果希望追加内容,应使用 std::ios::app。
  • 文件使用完毕后应 close(),虽然析构时通常也会关闭,但显式关闭可以更早释放资源并便于检查错误。
二进制文件读写

二进制文件按内存字节形式读写,常用 write() / read()。适合保存结构体、数组等"按内存布局"的数据,效率高,但可移植性差:不同编译器/平台在结构体填充、大小端、类型长度上可能不同。用二进制保存自定义类型时,该类型应尽量简单可拷贝,不能包含指针、虚函数等依赖进程地址的内容。

cpp 复制代码
#include <stdexcept>

struct ServerInfo {
    char _address[32];
    int _port;
    Date _date;
};

class ConfigManagerBin {
public:
    explicit ConfigManagerBin(const char* filename) : _filename(filename) {}

    void writeBin(const ServerInfo& info) {
        std::ofstream ofs(_filename, std::ios::out | std::ios::binary | std::ios::trunc);
        if (!ofs) {
            throw std::runtime_error("打开二进制文件失败");
        }
        ofs.write(reinterpret_cast<const char*>(&info), sizeof(info));
        if (!ofs) {
            throw std::runtime_error("二进制写入失败");
        }
    }

    void readBin(ServerInfo& info) {
        std::ifstream ifs(_filename, std::ios::in | std::ios::binary);
        if (!ifs) {
            throw std::runtime_error("打开二进制文件失败");
        }
        ifs.read(reinterpret_cast<char*>(&info), sizeof(info));
        if (!ifs) {
            throw std::runtime_error("二进制读取失败");
        }
    }

private:
    std::string _filename;
};

int main() {
    ServerInfo winfo = {"192.0.0.1", 80, Date(2022, 4, 10)};

    ConfigManagerBin cf("test.bin");
    cf.writeBin(winfo);

    ServerInfo rbinfo;
    cf.readBin(rbinfo);

    std::cout << rbinfo._address << " "
              << rbinfo._port << " "
              << rbinfo._date << std::endl;
    return 0;
}

注意:直接 write((char*)&info, sizeof(info)) 会把结构体填充字节也写入,不同编译器下sizeof(ServerInfo) 可能不同,导致文件格式不兼容。

文本文件读写

文本文件读写用流插入/提取运算符(<< / >>),把数据格式化成可读文本。优点是直观、易调试、跨平台处理换行较方便;缺点是要解析、效率通常低于二进制、字段中不能随意含分隔符。自定义类型的文本读写同样靠重载 << / >>。若字符串字段可能含空格,不能简单用 >> 按空白分隔读取,应设计明确的分隔格式或改用 JSON/XML。

cpp 复制代码
void writeText(const char* filename, const ServerInfo& info) {
    std::ofstream ofs(filename, std::ios::out | std::ios::trunc);
    if (!ofs) {
        throw std::runtime_error("打开文本文件失败");
    }
    ofs << info._address << " "
        << info._port << " "
        << info._date << "\n";
}

void readText(const char* filename, ServerInfo& info) {
    std::ifstream ifs(filename);
    if (!ifs) {
        throw std::runtime_error("打开文本文件失败");
    }
    ifs >> info._address >> info._port >> info._date;
    if (!ifs) {
        throw std::runtime_error("文本读取失败");
    }
}

int main() {
    ServerInfo winfo = {"192.0.0.1", 80, Date(2022, 4, 10)};
    writeText("test.txt", winfo);

    ServerInfo rtinfo;
    readText("test.txt", rtinfo);

    std::cout << rtinfo._address << " "
              << rtinfo._port << " "
              << rtinfo._date << std::endl;
    return 0;
}

注意:

  • 文本读写依赖字段分隔符,若字符串字段包含空格,ifs >> info._address 只能读到第一个单词,后续字段会错位。
  • 读取后应检查流状态,文件格式不符合预期时不能继续使用未完整读取的对象。
  • 文本模式在不同平台上的换行符处理可能不同;对格式要求严格的场景应明确使用二进制模式或统一换行约定。

stringstream

在C语言中,如果想要将一个整形变量的数据转化为字符串格式,如何去做?

  1. 使用itoa()函数
  2. 使用sprintf()函数

但是两个函数在转化时,都得需要先给出保存结果的空间,那空间要给多大呢,就不太好界定,而且转化格式不匹配时,可能还会得到错误的结果甚至程序崩溃。

stringstream 是 C++ 字符串流类,定义在 <sstream> 中。该头文件下主要有 istringstream、ostringstream 和 stringstream,分别用于字符串输入、字符串输出和双向输入输出。stringstream 底层维护一个 string 对象来保存结果,可以避免 C 语言 itoa()、sprintf() 需要预先分配字符数组、格式化不匹配可能导致崩溃等问题。它常用于数值与字符串互转、字符串拼接、结构数据序列化和反序列化。

stringstream 基本概念

stringstream 把字符串当流用,可像 cin/cout 一样使用 << 和 >>。它底层管理一个 string,str() 取出底层字符串,str("") 清空。相比字符数组,无需预估缓冲区大小,且会推导参数类型,减少格式化错误。多次转换时要注意流状态和底层字符串内容两个层面。

cpp 复制代码
#include <sstream>

int main() {
    std::stringstream ss;
    ss << "abc" << 123;

    std::cout << ss.str() << std::endl;
    return 0;
}

stringstream的使用场景

  1. 数值类型与字符串互转
    C 语言中,整数转字符串可用 itoa() 或 sprintf(),但都需预先分配空间,大小难界定;格式不匹配时可能出错甚至崩溃。stringstream 用 string 管理结果,无需预估字符数组大小,类型转换也更安全。使用时先写入数值,再提取到字符串,或直接用 str() 取底层字符串。多次转换时须同时处理"流状态"和"底层字符串内容"。
cpp 复制代码
int main() {
    int a = 12345678;
    std::string sa;
    std::stringstream s;

    s << a;
    s >> sa;
    std::cout << "第一次转换: " << sa << std::endl;

    // 清空底层字符串内容
    s.str("");
    // 清除上一次提取后可能产生的 eof/fail 状态
    s.clear();

    double d = 12.34;
    s << d;
    s >> sa;
    std::cout << "第二次转换: " << sa << std::endl;

    std::string svalue = s.str();
    std::cout << "底层字符串: " << svalue << std::endl;
    return 0;
}

注意:复用 stringstream 时 clear() 和 str("") 都要调用:只 clear() 不清内容会累积(如 "1234567812.34");只 str("") 不清状态,残留的 eofbit/failbit 会导致下次转换失败。提取到末尾可能置失败/EOF 状态,复用前必须重置。通常先 str("") 再 clear(),顺序可反,但两者都不可少。

  1. 字符串拼接
    stringstream 可以像输出流一样把多个字符串、数值拼接成一个字符串。与直接用 + 拼接相比,它尤其适合混合多种类型的数据,例如把字符串、整数、浮点数组合成一条日志或配置文本。拼接完成后通过 str() 取出结果。若需要复用同一个流,应先清空底层字符串。
cpp 复制代码
int main() {
    std::stringstream sstream;

    sstream << "first" << " " << "string,";
    sstream << " second string";
    std::cout << "strResult is: " << sstream.str() << std::endl;

    sstream.str("");
    sstream << "third string";
    std::cout << "After clear, strResult is: " << sstream.str() << std::endl;
    return 0;
}
  1. 结构数据序列化与反序列化
    • 序列化是把内存中的结构数据转换为可存储或可传输的字符串;
    • 反序列化是把字符串解析回结构数据。
    • ostringstream 适合把结构体字段按约定格式写入字符串,istringstream 适合按相同格式提取字段。
cpp 复制代码
struct ChatInfo {
    std::string _name;
    int _id;
    Date _date;
    std::string _msg;
};

int main() {
    ChatInfo winfo = {"张三", 135246, Date(2022, 4, 10), "晚上一起看电影吧"};

    std::ostringstream oss;
    oss << winfo._name << " "
        << winfo._id << " "
        << winfo._date << " "
        << winfo._msg;

    std::string str = oss.str();
    std::cout << "序列化结果: " << str << std::endl;

    ChatInfo rInfo;
    std::istringstream iss(str);
    iss >> rInfo._name >> rInfo._id >> rInfo._date >> rInfo._msg;

    if (!iss) {
        std::cerr << "反序列化失败" << std::endl;
        return 1;
    }

    std::cout << "姓名: " << rInfo._name << "(" << rInfo._id << ")\n";
    std::cout << "时间: " << rInfo._date << "\n";
    std::cout << "消息: " << rInfo._msg << std::endl;
    return 0;
}

注意:用空格分隔字段时,消息内容不能包含空格,否则 iss >> rInfo._msg 只能读到第一个单词,后续解析会错位。

完

相关推荐
小超人诞生日记1 小时前
C++ 异常机制详解:从抛出捕获到异常安全
c++·算法
骑着蜗牛撵大象3271 小时前
AI Agent 工程化开发实战(三):用 StateGraph 给智能体搭一条可回放的状态流水线
java·开发语言·jvm·ai·状态管理·langgraph·stategraph
itmigrate1 小时前
Rust 里的所有权误区:不是不写 clone 就叫高性能
开发语言·后端·rust
Brilliantwxx1 小时前
【Linux】 文件访问与路径解析:目录、文件名、dentry、路径缓冲与 dcache 全解析
linux·运维·服务器·开发语言·spring
摇滚侠2 小时前
《On Java 中文版 基础卷》阅读笔记 初始化和清理 08
java·开发语言·笔记
xiangyun612 小时前
C++ 手撕 shared_ptr:引用计数与控制块到底长什么样?
开发语言·c++·算法·面试·stl
XiYang-DING2 小时前
P4009.和为K的子数组个数 & P5522.异或和为 k 的子数组个数
java·开发语言
SunkingYang2 小时前
C++14 vs C++17:新特性对比与用法指南
c++·新特性·用法·c++14·示例·c++17
jaysee-sjc2 小时前
【苍穹外卖】Day12:工作台数据看板与 Excel 报表导出
java·开发语言·数据库·spring boot·spring·微信·excel