C++ Primer 第8章:IO 库

C++ Primer 第8章:IO 库


8.1 IO 类

8.1.1 IO 类层次结构

复制代码
IO 类层次结构:
┌─────────────────────────────────────────────────────┐
│                    ios_base                          │
│                       │                             │
│                      ios                            │
│                    /     \                          │
│               istream   ostream                     │
│              /    \       /    \                    │
│         ifstream  │  ofstream  │                    │
│                   │            │                    │
│              istringstream  ostringstream            │
│                    \       /                        │
│                   iostream                          │
│                  /         \                        │
│              fstream    stringstream                 │
└─────────────────────────────────────────────────────┘

标准流对象:
  cin   → istream(标准输入)
  cout  → ostream(标准输出)
  cerr  → ostream(标准错误,不缓冲)
  clog  → ostream(标准日志,缓冲)

8.1.2 IO 对象不能拷贝或赋值

cpp 复制代码
// io_no_copy.cpp -- IO对象不能拷贝
#include <iostream>
#include <fstream>

// IO 对象不能拷贝,因此:
// 1. 不能将形参或返回类型设为流类型
// 2. 传递和返回流时必须使用引用
// 3. 读写流会改变其状态,因此引用不能是 const

// ✅ 正确:通过引用传递
void readData(std::istream& is)
{
    int val;
    while (is >> val)
        std::cout << val << " ";
    std::cout << std::endl;
}

// ✅ 正确:通过引用返回
std::ostream& printHeader(std::ostream& os)
{
    os << "===== 报告 =====" << std::endl;
    return os;
}

// ❌ 错误:不能拷贝IO对象
// void badFunc(std::istream is) {}   // 错误!
// std::ostream badReturn() {}        // 错误!

int main()
{
    printHeader(std::cout) << "内容" << std::endl;
    return 0;
}

8.1.3 条件状态

cpp 复制代码
// io_state.cpp -- IO条件状态
#include <iostream>
#include <string>

int main()
{
    using namespace std;

    // IO 流的条件状态标志:
    // strm::badbit  :流已崩溃(不可恢复)
    // strm::failbit :IO操作失败(可恢复)
    // strm::eofbit  :到达文件末尾
    // strm::goodbit :流处于有效状态(值为0)

    // ===== 检测流状态 =====
    int val;
    cout << "输入一个整数:";
    cin >> val;

    if (cin.good())   cout << "流状态正常" << endl;
    if (cin.fail())   cout << "IO操作失败" << endl;
    if (cin.bad())    cout << "流已崩溃" << endl;
    if (cin.eof())    cout << "到达文件末尾" << endl;

    // 最常用的检测方式
    if (cin)   cout << "流有效" << endl;
    else       cout << "流无效" << endl;

    // ===== 管理条件状态 =====
    // rdstate():返回当前状态
    auto old_state = cin.rdstate();

    // clear():将所有状态位复位(恢复到goodbit)
    cin.clear();
    cout << "clear后,cin有效:" << boolalpha << (bool)cin << endl;

    // setstate():设置特定状态位
    cin.setstate(ios::failbit);
    cout << "setstate(failbit)后,cin有效:" << (bool)cin << endl;

    // 恢复到之前的状态
    cin.clear(old_state);

    // ===== 实际应用:读取整数直到失败 =====
    cout << "\n输入整数(输入非数字结束):" << endl;
    int sum = 0, count = 0;
    while (cin >> val)
    {
        sum += val;
        count++;
    }

    // 清除错误状态,继续使用cin
    cin.clear();
    cin.ignore(1000, '\n');   // 清除缓冲区中的错误输入

    if (count > 0)
        cout << "共" << count << "个数,平均值:"
             << (double)sum / count << endl;

    return 0;
}

8.1.4 管理输出缓冲

cpp 复制代码
// output_buffer.cpp -- 输出缓冲管理
#include <iostream>

int main()
{
    using namespace std;

    // 输出缓冲区刷新的情况:
    // 1. 程序正常结束
    // 2. 缓冲区满
    // 3. 使用 endl、flush、ends 操纵符
    // 4. 使用 unitbuf 设置每次写操作后刷新
    // 5. 关联流被读写(cin 和 cout 关联)

    // ===== 刷新缓冲区的方式 =====
    cout << "Hello" << endl;    // 输出 + 换行 + 刷新
    cout << "World" << flush;   // 输出 + 刷新(不换行)
    cout << "!" << ends;        // 输出 + 空字符 + 刷新
    cout << endl;

    // ===== unitbuf:每次写操作后刷新 =====
    cout << unitbuf;   // 设置每次写后刷新
    cout << "每次写后都刷新" << endl;
    cout << nounitbuf; // 恢复正常缓冲

    // ===== 关联流 =====
    // cin 和 cout 默认关联
    // 读 cin 之前会刷新 cout 的缓冲区
    // 这就是为什么 cout << "提示:" 后不需要 flush 就能显示

    // 手动关联流
    // cin.tie(&cout);   // 默认已关联
    // ostream* old = cin.tie(nullptr);   // 解除关联

    // ⚠️ 如果程序崩溃,缓冲区可能不会刷新
    // 调试时使用 cerr(不缓冲)或 endl(刷新)

    return 0;
}

8.2 文件输入输出

8.2.1 使用文件流

cpp 复制代码
// file_io.cpp -- 文件输入输出
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <stdexcept>

int main()
{
    using namespace std;

    // ===== 写入文件 =====
    {
        ofstream outFile("data.txt");   // 创建并打开文件

        // 检查是否成功打开
        if (!outFile)
            throw runtime_error("无法打开文件 data.txt");

        // 写入内容(与 cout 用法相同)
        outFile << "张三 85 92 78" << endl;
        outFile << "李四 90 88 95" << endl;
        outFile << "王五 72 85 80" << endl;

        // outFile 离开作用域时自动关闭
    }
    cout << "写入完成" << endl;

    // ===== 读取文件 =====
    {
        ifstream inFile("data.txt");

        if (!inFile)
            throw runtime_error("无法打开文件 data.txt");

        string name;
        int s1, s2, s3;

        cout << "\n读取内容:" << endl;
        while (inFile >> name >> s1 >> s2 >> s3)
        {
            double avg = (s1 + s2 + s3) / 3.0;
            cout << name << " 平均分:" << avg << endl;
        }
    }

    // ===== 逐行读取 =====
    {
        ifstream inFile("data.txt");
        string line;
        int lineNum = 0;

        cout << "\n逐行读取:" << endl;
        while (getline(inFile, line))
        {
            cout << ++lineNum << ": " << line << endl;
        }
    }

    return 0;
}

8.2.2 文件模式

cpp 复制代码
// file_modes.cpp -- 文件打开模式
#include <iostream>
#include <fstream>
#include <string>

int main()
{
    using namespace std;

    /*
    文件模式标志:
    ios::in     以读方式打开
    ios::out    以写方式打开(默认截断)
    ios::app    追加:每次写操作前定位到文件末尾
    ios::ate    打开文件后立即定位到文件末尾
    ios::trunc  截断文件(清空内容)
    ios::binary 以二进制方式进行IO

    组合规则:
    - out 默认隐含 trunc
    - 要保留文件内容,必须显式指定 app 或 in
    */

    // ===== 各种打开模式 =====

    // 1. 只读(默认)
    ifstream f1("file.txt");
    // 等价于:ifstream f1("file.txt", ios::in);

    // 2. 只写(截断)
    ofstream f2("file.txt");
    // 等价于:ofstream f2("file.txt", ios::out | ios::trunc);

    // 3. 追加写入(不截断)
    ofstream f3("file.txt", ios::app);
    // 等价于:ofstream f3("file.txt", ios::out | ios::app);

    // 4. 读写(不截断)
    fstream f4("file.txt", ios::in | ios::out);

    // 5. 读写(截断)
    fstream f5("file.txt", ios::in | ios::out | ios::trunc);

    // 6. 二进制读写
    fstream f6("file.bin", ios::in | ios::out | ios::binary);

    // ===== 实际应用:日志文件 =====
    auto writeLog = [](const string& msg) {
        ofstream log("app.log", ios::app);   // 追加模式
        if (log)
            log << msg << endl;
    };

    writeLog("程序启动");
    writeLog("执行操作1");
    writeLog("执行操作2");
    writeLog("程序结束");

    // 读取日志
    ifstream logFile("app.log");
    string line;
    cout << "日志内容:" << endl;
    while (getline(logFile, line))
        cout << "  " << line << endl;

    return 0;
}

8.2.3 文件流的成员函数

cpp 复制代码
// fstream_members.cpp -- 文件流成员函数
#include <iostream>
#include <fstream>
#include <string>

int main()
{
    using namespace std;

    // ===== open() 和 close() =====
    ifstream inFile;   // 未关联任何文件

    inFile.open("data.txt");   // 打开文件
    if (!inFile.is_open())
    {
        cerr << "无法打开文件" << endl;
        return 1;
    }

    string line;
    while (getline(inFile, line))
        cout << line << endl;

    inFile.close();   // 关闭文件

    // 重新打开另一个文件
    inFile.open("other.txt");
    if (inFile.is_open())
    {
        // 处理 other.txt
        inFile.close();
    }

    // ===== 处理多个文件 =====
    vector<string> files = {"file1.txt", "file2.txt", "file3.txt"};

    for (const auto& filename : files)
    {
        ifstream f(filename);
        if (!f)
        {
            cerr << "无法打开:" << filename << endl;
            continue;
        }

        cout << "处理文件:" << filename << endl;
        string content;
        while (getline(f, content))
            cout << "  " << content << endl;
        // f 离开作用域时自动关闭
    }

    return 0;
}

8.3 string 流

8.3.1 istringstream

cpp 复制代码
// istringstream_demo.cpp -- istringstream详解
#include <iostream>
#include <sstream>
#include <string>
#include <vector>

struct PersonInfo
{
    std::string name;
    std::vector<std::string> phones;
};

int main()
{
    using namespace std;

    // ===== 基本用法 =====
    string line = "张三 138-1234-5678 010-12345678";
    istringstream iss(line);

    string name;
    iss >> name;   // 读取姓名

    string phone;
    vector<string> phones;
    while (iss >> phone)   // 读取所有电话号码
        phones.push_back(phone);

    cout << "姓名:" << name << endl;
    cout << "电话:";
    for (const auto& p : phones) cout << p << " ";
    cout << endl;

    // ===== 解析结构化数据 =====
    vector<string> lines = {
        "张三 138-1234-5678 010-12345678",
        "李四 139-8765-4321",
        "王五 021-87654321 400-123-4567 010-98765432"
    };

    vector<PersonInfo> people;
    for (const auto& l : lines)
    {
        PersonInfo info;
        istringstream record(l);
        record >> info.name;

        string p;
        while (record >> p)
            info.phones.push_back(p);

        people.push_back(info);
    }

    cout << "\n联系人列表:" << endl;
    for (const auto& person : people)
    {
        cout << person.name << ":";
        for (const auto& phone : person.phones)
            cout << phone << " ";
        cout << endl;
    }

    // ===== 类型转换 =====
    string numStr = "42";
    int num;
    istringstream(numStr) >> num;
    cout << "\n字符串转整数:" << num << endl;

    string floatStr = "3.14";
    double d;
    istringstream(floatStr) >> d;
    cout << "字符串转浮点:" << d << endl;

    return 0;
}

8.3.2 ostringstream

cpp 复制代码
// ostringstream_demo.cpp -- ostringstream详解
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
#include <iomanip>

int main()
{
    using namespace std;

    // ===== 基本用法:构建字符串 =====
    ostringstream oss;
    oss << "姓名:" << "张三" << endl;
    oss << "年龄:" << 25 << endl;
    oss << "成绩:" << fixed << setprecision(1) << 92.5 << endl;

    string result = oss.str();   // 获取构建的字符串
    cout << result;

    // ===== 格式化数字 =====
    auto formatNumber = [](double val, int precision = 2) -> string {
        ostringstream os;
        os << fixed << setprecision(precision) << val;
        return os.str();
    };

    cout << "格式化:" << formatNumber(3.14159, 3) << endl;
    cout << "格式化:" << formatNumber(1234567.89) << endl;

    // ===== 构建 CSV 行 =====
    auto makeCSVRow = [](const vector<string>& fields) -> string {
        ostringstream os;
        for (size_t i = 0; i < fields.size(); i++)
        {
            if (i > 0) os << ",";
            os << fields[i];
        }
        return os.str();
    };

    vector<string> row = {"张三", "25", "北京", "92.5"};
    cout << "CSV行:" << makeCSVRow(row) << endl;

    // ===== 数值转字符串 =====
    auto toString = [](auto val) -> string {
        ostringstream os;
        os << val;
        return os.str();
    };

    cout << "int转string:" << toString(42) << endl;
    cout << "double转string:" << toString(3.14) << endl;
    cout << "bool转string:" << toString(true) << endl;

    // ===== 延迟输出(先构建,后决定是否输出)=====
    vector<string> errors;
    vector<int> data = {1, -2, 3, -4, 5};

    for (int val : data)
    {
        if (val < 0)
        {
            ostringstream err;
            err << "发现负数:" << val;
            errors.push_back(err.str());
        }
    }

    if (!errors.empty())
    {
        cout << "\n错误列表:" << endl;
        for (const auto& e : errors)
            cout << "  " << e << endl;
    }

    return 0;
}

8.3.3 stringstream 综合应用

cpp 复制代码
// stringstream_app.cpp -- stringstream综合应用
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
#include <map>

// 解析 key=value 格式的配置字符串
map<string, string> parseConfig(const string& config)
{
    map<string, string> result;
    istringstream iss(config);
    string token;

    while (getline(iss, token, ';'))   // 以分号分隔
    {
        istringstream pair(token);
        string key, value;
        if (getline(pair, key, '=') && getline(pair, value))
        {
            // 去除首尾空格
            auto trim = [](string& s) {
                size_t start = s.find_first_not_of(" \t");
                size_t end   = s.find_last_not_of(" \t");
                s = (start == string::npos) ? "" : s.substr(start, end - start + 1);
            };
            trim(key);
            trim(value);
            result[key] = value;
        }
    }
    return result;
}

// 将 vector 转为字符串
template <typename T>
string vectorToString(const vector<T>& v, const string& sep = ", ")
{
    ostringstream oss;
    for (size_t i = 0; i < v.size(); i++)
    {
        if (i > 0) oss << sep;
        oss << v[i];
    }
    return oss.str();
}

// 分割字符串
vector<string> split(const string& s, char delim)
{
    vector<string> result;
    istringstream iss(s);
    string token;
    while (getline(iss, token, delim))
        if (!token.empty())
            result.push_back(token);
    return result;
}

int main()
{
    using namespace std;

    // 解析配置
    string config = "host=localhost; port=8080; debug=true; timeout=30";
    auto cfg = parseConfig(config);

    cout << "配置项:" << endl;
    for (const auto& [key, val] : cfg)
        cout << "  " << key << " = " << val << endl;

    // vector 转字符串
    vector<int>    nums = {1, 2, 3, 4, 5};
    vector<string> words = {"Hello", "World", "C++"};

    cout << "\nvector转字符串:" << endl;
    cout << "  " << vectorToString(nums) << endl;
    cout << "  " << vectorToString(words, " | ") << endl;

    // 分割字符串
    string csv = "张三,25,北京,92.5";
    auto fields = split(csv, ',');

    cout << "\nCSV解析:" << endl;
    for (size_t i = 0; i < fields.size(); i++)
        cout << "  字段" << i << ": " << fields[i] << endl;

    // 类型安全的字符串转换
    auto fromString = [](const string& s, auto& val) -> bool {
        istringstream iss(s);
        return (bool)(iss >> val);
    };

    int    n;
    double d;
    bool   ok1 = fromString("42", n);
    bool   ok2 = fromString("3.14", d);
    bool   ok3 = fromString("abc", n);   // 失败

    cout << "\n类型转换:" << endl;
    cout << "  \"42\" → int: " << (ok1 ? to_string(n) : "失败") << endl;
    cout << "  \"3.14\" → double: " << (ok2 ? to_string(d) : "失败") << endl;
    cout << "  \"abc\" → int: " << (ok3 ? to_string(n) : "失败") << endl;

    return 0;
}

8.4 综合示例:日志系统

cpp 复制代码
// logger.cpp -- 综合示例:日志系统
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
#include <chrono>
#include <iomanip>
#include <stdexcept>

// 日志级别
enum class LogLevel { DEBUG, INFO, WARNING, ERROR, CRITICAL };

class Logger
{
private:
    std::string   filename_;
    LogLevel      minLevel_;
    bool          toConsole_;
    std::ofstream logFile_;

    // 获取当前时间字符串
    static std::string currentTime()
    {
        auto now = std::chrono::system_clock::now();
        auto t   = std::chrono::system_clock::to_time_t(now);
        std::ostringstream oss;
        oss << std::put_time(std::localtime(&t), "%Y-%m-%d %H:%M:%S");
        return oss.str();
    }

    // 日志级别转字符串
    static std::string levelStr(LogLevel level)
    {
        switch (level)
        {
            case LogLevel::DEBUG:    return "DEBUG";
            case LogLevel::INFO:     return "INFO ";
            case LogLevel::WARNING:  return "WARN ";
            case LogLevel::ERROR:    return "ERROR";
            case LogLevel::CRITICAL: return "CRIT ";
        }
        return "UNKN ";
    }

    // 格式化日志消息
    std::string format(LogLevel level, const std::string& msg)
    {
        std::ostringstream oss;
        oss << "[" << currentTime() << "] "
            << "[" << levelStr(level) << "] "
            << msg;
        return oss.str();
    }

public:
    Logger(const std::string& filename,
           LogLevel minLevel = LogLevel::INFO,
           bool toConsole = true)
        : filename_(filename), minLevel_(minLevel), toConsole_(toConsole)
    {
        logFile_.open(filename_, std::ios::app);
        if (!logFile_)
            throw std::runtime_error("无法打开日志文件:" + filename_);
    }

    ~Logger()
    {
        if (logFile_.is_open())
            logFile_.close();
    }

    void log(LogLevel level, const std::string& msg)
    {
        if (level < minLevel_) return;

        std::string formatted = format(level, msg);

        // 写入文件
        logFile_ << formatted << std::endl;
        logFile_.flush();   // 立即刷新(确保崩溃时也能保存)

        // 输出到控制台
        if (toConsole_)
        {
            if (level >= LogLevel::ERROR)
                std::cerr << formatted << std::endl;
            else
                std::cout << formatted << std::endl;
        }
    }

    // 便捷方法
    void debug(const std::string& msg)    { log(LogLevel::DEBUG, msg); }
    void info(const std::string& msg)     { log(LogLevel::INFO, msg); }
    void warning(const std::string& msg)  { log(LogLevel::WARNING, msg); }
    void error(const std::string& msg)    { log(LogLevel::ERROR, msg); }
    void critical(const std::string& msg) { log(LogLevel::CRITICAL, msg); }

    // 读取日志
    std::vector<std::string> readLog() const
    {
        std::vector<std::string> lines;
        std::ifstream f(filename_);
        std::string line;
        while (std::getline(f, line))
            lines.push_back(line);
        return lines;
    }

    // 统计各级别日志数量
    std::map<std::string, int> statistics() const
    {
        std::map<std::string, int> stats;
        for (const auto& line : readLog())
        {
            // 从格式化字符串中提取级别
            auto start = line.find('[', 22);   // 跳过时间戳
            auto end   = line.find(']', start);
            if (start != std::string::npos && end != std::string::npos)
            {
                std::string level = line.substr(start + 1, end - start - 1);
                // 去除空格
                while (!level.empty() && level.back() == ' ')
                    level.pop_back();
                stats[level]++;
            }
        }
        return stats;
    }
};

int main()
{
    using namespace std;

    try
    {
        Logger logger("app.log", LogLevel::DEBUG);

        logger.info("程序启动");
        logger.debug("调试信息:初始化完成");
        logger.info("连接数据库...");
        logger.warning("数据库连接超时,正在重试");
        logger.info("数据库连接成功");

        // 模拟业务操作
        for (int i = 1; i <= 3; i++)
        {
            ostringstream msg;
            msg << "处理任务 #" << i;
            logger.info(msg.str());
        }

        logger.error("任务 #2 处理失败:数据格式错误");
        logger.info("程序正常结束");

        // 读取并显示日志
        cout << "\n===== 日志内容 =====" << endl;
        for (const auto& line : logger.readLog())
            cout << line << endl;

        // 统计
        cout << "\n===== 日志统计 =====" << endl;
        for (const auto& [level, count] : logger.statistics())
            cout << level << ": " << count << " 条" << endl;
    }
    catch (const exception& e)
    {
        cerr << "错误:" << e.what() << endl;
        return 1;
    }

    return 0;
}

📝 第8章知识点总结

知识点 核心要点
IO 类层次 istream/ostreamifstream/ofstreamistringstream/ostringstream
IO 不能拷贝 传递和返回流必须用引用,且不能是 const 引用
条件状态 goodbit/failbit/badbit/eofbitclear() 复位,rdstate() 查询
缓冲刷新 endl(换行+刷新)、flush(只刷新)、ends(空字符+刷新)、unitbuf
关联流 cincout 默认关联,读 cin 前自动刷新 cout
文件流 ifstream/ofstream/fstream,构造时或 open() 打开,离开作用域自动关闭
文件模式 in/out/app/ate/trunc/binaryout 默认隐含 trunc
追加模式 ios::app 保留文件内容,每次写操作前定位到末尾
istringstream 从字符串读取,用于解析结构化文本、类型转换
ostringstream 向字符串写入,用于格式化字符串、延迟输出
getline 分隔符 getline(iss, token, delim) 可指定分隔符,用于解析 CSV 等
is_open() 检查文件是否成功打开,比直接检查流对象更明确
cerr vs clog cerr 不缓冲(立即输出),clog 缓冲,都输出到标准错误
相关推荐
兰令水1 小时前
leecodecode【层序遍历】【2026.6.3打卡-java版本】
java·开发语言
Halo_tjn1 小时前
反射与设计模式2
java·开发语言·算法
kaoa0001 小时前
Linux入门攻坚——79、XEN虚拟化-2
linux·运维·开发语言
磊 子2 小时前
C++仿函数以及STL内置仿函数
开发语言·c++
0x3F(小茶)2 小时前
嵌入式C设计模式完全指南(基于《C嵌入式编程设计模式》)
c语言·开发语言·单片机·嵌入式硬件·设计模式
王璐WL2 小时前
【C++进阶】map/multimap 容器详解:从基础使用到底层实现与高频面试题
c++
灰鲸广告联盟2 小时前
新老用户广告价值不同?差异化策略如何实现收益最大化
android·开发语言·flutter·ios
周杰伦fans2 小时前
C# CAD 二次开发:无需启动 AutoCAD 实现 DWG 转 DXF 的完整技术指南
开发语言·c#