C++IO流

发布于:2025-09-12 ⋅ 阅读:(18) ⋅ 点赞:(0)

C++IO流

C++的IO流库是一个用于输入输出的强大、可扩展但有时也有些令人困惑的框架。我们将从基础概念一直讲到高级用法。


1. IO流库的核心概念与层次结构

原理与作用

C++的IO流库提供了类型安全可扩展的输入输出机制。相比于C语言的printfscanf,它的核心优势在于:

  1. 类型安全:编译器能检查类型是否匹配,不会出现%d对应double的运行时错误。
  2. 可扩展性:你可以通过重载<<>>操作符,让你自定义的类也能像内置类型一样进行IO操作。
  3. 状态管理:流对象维护一个状态,可以检测IO操作是否成功(如文件结束、格式错误等)。
层次结构

C++ IO流库的核心是一个复杂的继承体系,理解它是掌握流库的关键。

                                    ios_base
                                       |
                                        ios
                                       /  \
                                      /    \
                                     /      \
                                    /        \
                        basic_istream<>    basic_ostream<>
                                 \          /
                                  \        /
                                   \      /
                                    \    /
                         basic_iostream<>
                                       |
                                       |
                        ----------------------------
                        |             |            |
                 basic_ifstream<>  basic_ofstream<>  basic_fstream<>
                 (文件输入)      (文件输出)      (文件输入输出)
                        |             |            |
                 basic_istringstream<> basic_ostringstream<> basic_stringstream<>
                 (字符串输入)    (字符串输出)    (字符串输入输出)
  • ios_base:定义了所有流类的基本属性,与模板参数无关(如格式标志、浮点数精度等)。
  • basic_ios<>:模板类,管理流的状态(如goodbit, eofbit, failbit, badbit)和流缓冲区streambuf)。
  • basic_istream<>, basic_ostream<>, basic_iostream<>:分别定义了输入、输出和输入输出的基本接口。我们常用的cinistream)和coutostream)就是它们的特化别名。
  • 文件流 (basic_ifstream<>, basic_ofstream<>, basic_fstream<>):用于文件操作,继承自相应的通用流类。
  • 字符串流 (basic_istringstream<>, basic_ostringstream<>, basic_stringstream<>):用于在内存中读写字符串,极其有用,继承自相应的通用流类。

常用的类型别名(特化char类型):

typedef basic_ios<char>           ios;
typedef basic_istream<char>       istream;
typedef basic_ostream<char>       ostream;
typedef basic_iostream<char>      iostream;
typedef basic_ifstream<char>      ifstream;
typedef basic_ofstream<char>      ofstream;
typedef basic_fstream<char>       fstream;
typedef basic_istringstream<char> istringstream;
typedef basic_ostringstream<char> ostringstream;
typedef basic_stringstream<char>  stringstream;

2. 标准IO对象 (cin, cout, cerr, clog)

C++预定义了四个标准流对象,它们在<iostream>头文件中声明。

对象 类型 缓冲情况 用途
cin istream 带缓冲 标准输入(通常对应键盘)
cout ostream 带缓冲 标准输出(通常对应终端/控制台)
cerr ostream 无缓冲 标准错误输出,用于显示错误信息,立即刷新
clog ostream 带缓冲 标准错误输出,用于记录日志

代码示例与区别:

#include <iostream>
using namespace std;

int main() {
    int value;
    cout << "Please enter a number: "; // 输出到标准输出(缓冲)
    cin >> value;                      // 从标准输入读取

    if (cin.fail()) {
        cerr << "Error: Invalid input!\n"; // 立即无缓冲输出错误信息
    } else {
        cout << "You entered: " << value << endl;
        clog << "User successfully entered a number: " << value << '\n'; // 记录日志(缓冲)
    }

    // 演示缓冲区别:cout和clog可能不会立即显示,但cerr会。
    cout << "This is cout (buffered).";
    clog << "This is clog (buffered).";
    cerr << "This is cerr (unbuffered).";

    // 程序结束时,cout和clog的缓冲区才会被刷新并显示
    return 0;
}

3. 格式化输入输出

流对象有一系列成员函数和操纵符 (Manipulators) 来控制输出的格式(如精度、宽度、进制等)。操纵符在<ios>, <iomanip>, <iostream>中定义。

常用操纵符示例:
#include <iostream>
#include <iomanip> // 用于带参数的操纵符

int main() {
    double pi = 3.141592653589793;
    int num = 42;

    // 1. 设置宽度(只对下一次IO有效)
    std::cout << std::setw(10) << num << "|\n"; // 输出: "        42|"

    // 2. 设置填充字符
    std::cout << std::setfill('*') << std::setw(10) << num << "\n"; // 输出: "********42"

    // 3. 设置浮点数精度和格式
    std::cout << std::fixed << std::setprecision(2) << pi << "\n"; // 输出: "3.14"
    std::cout << std::scientific << pi << "\n";                    // 输出: "3.14e+00"

    // 4. 设置进制
    std::cout << std::hex << num << "\n"; // 十六进制: "2a"
    std::cout << std::oct << num << "\n"; // 八进制: "52"
    std::cout << std::dec << num << "\n"; // 改回十进制: "42"

    // 5. 设置对齐
    std::cout << std::left << std::setw(10) << num << "\n";  // 左对齐: "42        "
    std::cout << std::right << std::setw(10) << num << "\n"; // 右对齐: "        42"

    // 6. 布尔值输出格式
    std::cout << std::boolalpha << true << " " << false << "\n"; // 输出: "true false"
    std::cout << std::noboolalpha << true << "\n";               // 输出: "1"

    return 0;
}
成员函数方式实现格式化:
#include <iostream>
int main() {
    double pi = 3.14159;
    // cout.precision() 等同于 setprecision
    std::streamsize old_precision = std::cout.precision(4);
    std::cout << pi << '\n'; // 输出 3.142
    std::cout.precision(old_precision); // 恢复原有精度

    // cout.width() 等同于 setw
    std::cout.width(10);
    std::cout << 123 << '\n'; // 输出 "       123"

    return 0;
}

4. 文件输入输出 (File I/O)

使用ifstream(读)、ofstream(写)、fstream(读写)类进行文件操作。核心步骤:打开 -> 操作 -> 关闭

文件打开模式 (Open Modes):
模式标志 作用
std::ios::in 为读而打开
std::ios::out 为写而打开(默认会截断文件)
std::ios::app 追加模式,所有写入都追加到文件末尾
std::ios::ate 打开后定位到文件末尾
std::ios::trunc 截断文件(如果文件存在)
std::ios::binary 二进制模式(非常重要!避免文本模式的转换)

代码示例:

#include <iostream>
#include <fstream>
#include <string>

int main() {
    // --- 写入文件 ---
    std::ofstream outfile("example.txt"); // 隐式使用 ios::out | ios::trunc
    // 显式指定模式:std::ofstream outfile("example.txt", std::ios::out | std::ios::app);
    if (outfile.is_open()) { // 总是检查是否成功打开!
        outfile << "This is a line.\n";
        outfile << "This is another line.\n";
        outfile.close(); // 可以显式关闭,但析构时也会自动关闭
    } else {
        std::cerr << "Unable to open file for writing!";
    }

    // --- 读取文件 ---
    std::ifstream infile("example.txt"); // 隐式使用 ios::in
    std::string line;
    if (infile) { // 更简洁的检查方式:ifstream 重载了 bool 操作符
        std::cout << "Reading file contents:\n";
        while (std::getline(infile, line)) { // 逐行读取,推荐方式
            std::cout << line << '\n';
        }
        infile.close();
    }

    // --- 读写文件 ---
    std::fstream iofile("data.txt", std::ios::in | std::ios::out);
    if (!iofile) {
        // 如果文件不存在,就创建它
        iofile.open("data.txt", std::ios::in | std::ios::out | std::ios::trunc);
    }
    if (iofile) {
        iofile << "Some data";
        // 移动文件指针到开始位置以便读取
        iofile.seekg(0, std::ios::beg);
        iofile >> line;
        std::cout << "Read: " << line << std::endl;
        iofile.close();
    }

    return 0;
}

5. 字符串流 (String Streams)

字符串流(<sstream>)允许你像操作流一样操作字符串。这是极其强大和常用的工具,主要用于:

  1. 字符串格式化:将各种类型的数据组合成一个复杂的字符串。
  2. 字符串解析:从一个字符串中提取各种类型的数据。
  3. 类型转换:在字符串和其他数据类型之间进行转换。

代码示例:

#include <iostream>
#include <sstream>
#include <string>

int main() {
    // --- 1. 字符串格式化 (替代 sprintf) ---
    std::ostringstream oss;
    std::string name = "Alice";
    int age = 25;
    double score = 87.5;

    oss << "Name: " << name << ", Age: " << age << ", Score: " << score;
    std::string info_str = oss.str(); // 获取格式化后的字符串
    std::cout << info_str << std::endl; // 输出: Name: Alice, Age: 25, Score: 87.5

    // --- 2. 字符串解析 (替代 sscanf) ---
    std::string data = "123 3.14 hello";
    std::istringstream iss(data);

    int n;
    double d;
    std::string s;

    iss >> n >> d >> s; // 从字符串流中提取数据
    std::cout << "Extracted: " << n << ", " << d << ", " << s << std::endl;

    // --- 3. 类型转换 ---
    std::string num_str = "42";
    int value;
    std::istringstream converter(num_str);
    if (converter >> value) { // 使用流的状态检查转换是否成功
        std::cout << "Converted to int: " << value * 2 << std::endl;
    } else {
        std::cerr << "Conversion failed!";
    }

    return 0;
}

6. 流状态与错误处理

每个流对象都维护一个状态,由以下状态标志位组成:

  • goodbit:一切正常,值为0。
  • eofbit:已到达文件末尾(End-of-File)。
  • failbit:上次IO操作失败(如类型不匹配),但流本身未损坏。
  • badbit:流已损坏,发生了严重的错误(如读写操作本身失败)。

相关成员函数:

  • good():如果goodbit被设置(即没有错误)则返回true
  • eof():如果eofbit被设置则返回true
  • fail():如果failbitbadbit被设置则返回true
  • bad():如果badbit被设置则返回true
  • clear():重置流状态。
  • rdstate():返回当前的状态位。

代码示例:

#include <iostream>
#include <limits>

int main() {
    int number;

    std::cout << "Enter an integer: ";
    std::cin >> number;

    // 检查输入是否成功
    if (std::cin.fail()) {
        std::cerr << "Error: Invalid input (not an integer).\n";
        std::cin.clear(); // 重置错误状态,否则后续所有IO都会失败
        // 清空输入缓冲区中的错误数据
        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
    } else {
        std::cout << "You entered: " << number << '\n';
    }

    // 演示eof
    std::cout << "Enter some text (Ctrl+D/Ctrl+Z to send EOF): ";
    char c;
    while (std::cin.get(c)) { // 循环读取字符
        std::cout << c;
    }
    if (std::cin.eof()) {
        std::cout << "\nEOF encountered.\n";
    }

    return 0;
}

7. 重载输入/输出操作符

这是C++ IO流库可扩展性的体现。你可以让你自定义的类支持<<>>操作。

规则:

  • 重载operator<<非成员函数(通常是友元)。
  • 重载operator>>非成员函数(通常是友元)。
  • 参数和返回类型都是流的引用。

代码示例:

#include <iostream>

class Person {
public:
    Person() = default;
    Person(const std::string& n, int a) : name(n), age(a) {}

    // 声明为友元,以便访问私有成员
    friend std::ostream& operator<<(std::ostream& os, const Person& p);
    friend std::istream& operator>>(std::istream& is, Person& p);

private:
    std::string name;
    int age = 0;
};

// 重载输出操作符 <<
std::ostream& operator<<(std::ostream& os, const Person& p) {
    os << "Name: " << p.name << ", Age: " << p.age;
    return os; // 必须返回流引用以支持链式调用
}

// 重载输入操作符 >>
std::istream& operator>>(std::istream& is, Person& p) {
    std::cout << "Enter name and age: ";
    is >> p.name >> p.age;
    // 注意:这里应该进行错误检查,这里为了简洁省略了
    return is;
}

int main() {
    Person alice("Alice", 30);
    Person bob;

    std::cout << alice << std::endl; // 输出: Name: Alice, Age: 30

    std::cin >> bob;    // 输入: Bob 25
    std::cout << bob;   // 输出: Name: Bob, Age: 25

    return 0;
}

8. 二进制文件操作与序列化

文本模式会进行转换(如\n -> \r\n),而二进制模式直接读写内存字节,不做任何转换。用于处理非文本数据(如图片、视频、自定义数据结构)。

核心函数:

  • read(const char_type* s, std::streamsize count):从流中读取二进制数据。
  • write(const char_type* s, std::streamsize count):向流中写入二进制数据。

代码示例(简单序列化/反序列化):

#include <iostream>
#include <fstream>

struct Data {
    int id;
    double value;
    char name[20];
};

int main() {
    Data data_to_write = {1, 3.14, "Binary Example"};

    // --- 二进制写入 ---
    std::ofstream outfile("data.bin", std::ios::binary);
    if (outfile) {
        outfile.write(reinterpret_cast<const char*>(&data_to_write), sizeof(Data));
        outfile.close();
    }

    // --- 二进制读取 ---
    Data data_to_read;
    std::ifstream infile("data.bin", std::ios::binary);
    if (infile) {
        infile.read(reinterpret_cast<char*>(&data_to_read), sizeof(Data));
        infile.close();

        std::cout << "Read Data: ID=" << data_to_read.id
                  << ", Value=" << data_to_read.value
                  << ", Name=" << data_to_read.name << std::endl;
    }

    return 0;
}

警告:这种简单的二进制序列化不可移植(不同平台字节序、数据类型大小可能不同),也不安全(指针数据无效)。生产环境应使用专门的序列化库(如 Protocol Buffers, FlatBuffers)。


总结与最佳实践

  1. 理解层次结构:知道istream, ifstream, istringstream之间的关系。
  2. 总是检查流状态:在文件操作和输入后,使用if (stream)stream.fail()进行检查。
  3. 优先使用字符串流sstream是进行字符串格式化和解析的利器,比C风格的sprintfsscanf更安全。
  4. 谨慎使用二进制IO:明白其局限性和风险。
  5. 利用操纵符:掌握<iomanip>中的工具来控制格式。
  6. 重载<<>>:让你自定义的类无缝集成到C++的IO框架中。
  7. 区分cerrclog:错误信息用无缓冲的cerr,日志记录用缓冲的clog
  8. 注意缓冲endl会刷新缓冲区,影响性能。在需要频繁输出的场景,使用\n可能更好。

网站公告

今日签到

点亮在社区的每一天
去签到