MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++ 流和缓冲区

2023-09-275.1k 阅读

C++ 流概述

在 C++ 编程中,流(Stream)是一种抽象的概念,用于在程序和外部设备(如文件、控制台、网络连接等)之间进行数据的输入和输出操作。流提供了一种统一的接口,使得程序员可以以相同的方式处理不同类型的输入输出源和目标。

流的概念源于 Unix 操作系统中的“一切皆文件”理念。在 C++ 中,流将数据的读取和写入操作抽象成连续的字节序列,就像水流一样源源不断。通过流,我们可以轻松地从键盘读取数据、向屏幕输出数据,也可以对文件进行读写操作。

C++ 标准库提供了一系列与流相关的类和对象,这些类和对象被组织在<iostream><fstream><sstream>等头文件中。<iostream>主要用于控制台输入输出,<fstream>用于文件的输入输出,<sstream>用于字符串流的操作。

流的分类

  1. 输入流(InputStream):用于从外部设备读取数据到程序中。例如,从键盘读取用户输入的数据,或者从文件中读取数据。在 C++ 中,std::istream及其派生类(如std::cin)用于实现输入流操作。
  2. 输出流(OutputStream):用于将程序中的数据写入到外部设备。例如,将数据输出到屏幕,或者写入到文件中。在 C++ 中,std::ostream及其派生类(如std::cout)用于实现输出流操作。
  3. 输入输出流(IOStream):既可以进行输入操作,也可以进行输出操作。例如,对文件进行读写操作时,需要使用输入输出流。在 C++ 中,std::iostream及其派生类(如std::fstream)用于实现输入输出流操作。

标准输入输出流

std::cin

std::cin是 C++ 标准库中用于从标准输入设备(通常是键盘)读取数据的输入流对象,它是std::istream类的一个实例。std::cin支持多种数据类型的读取,例如整数、浮点数、字符和字符串等。

下面是一个简单的示例,展示如何使用std::cin读取整数和字符串:

#include <iostream>
#include <string>

int main() {
    int num;
    std::string str;

    std::cout << "请输入一个整数: ";
    std::cin >> num;

    std::cout << "请输入一个字符串: ";
    std::cin >> str;

    std::cout << "你输入的整数是: " << num << std::endl;
    std::cout << "你输入的字符串是: " << str << std::endl;

    return 0;
}

在上述代码中,std::cin >> num用于从键盘读取一个整数并存储到变量num中,std::cin >> str用于读取一个字符串并存储到变量str中。注意,std::cin在读取字符串时,遇到空格、制表符或换行符就会停止读取。

std::cout

std::cout是 C++ 标准库中用于向标准输出设备(通常是屏幕)写入数据的输出流对象,它是std::ostream类的一个实例。std::cout支持多种数据类型的输出,并且可以使用<<运算符进行链式输出。

以下是一个简单的std::cout示例:

#include <iostream>

int main() {
    int num = 42;
    double pi = 3.14159;
    const char* message = "Hello, World!";

    std::cout << "整数: " << num << std::endl;
    std::cout << "浮点数: " << pi << std::endl;
    std::cout << "字符串: " << message << std::endl;

    return 0;
}

在上述代码中,std::cout << "整数: " << num << std::endl将字符串“整数: ”和变量num的值输出到屏幕上,并换行。std::endl表示输出一个换行符并刷新缓冲区。

std::cerr 和 std::clog

  1. std::cerrstd::cerr是用于输出错误信息的输出流对象,它也是std::ostream类的一个实例。与std::cout不同的是,std::cerr的输出不会被缓冲,而是直接输出到标准错误设备(通常也是屏幕)。这意味着当程序出现错误时,错误信息可以立即显示出来,而不会因为缓冲区的原因而延迟。
    #include <iostream>
    
    int main() {
        try {
            // 假设这里发生了一个除以零的错误
            int result = 10 / 0;
        } catch (const std::exception& e) {
            std::cerr << "发生错误: " << e.what() << std::endl;
        }
    
        return 0;
    }
    
  2. std::clogstd::clog也是用于输出日志信息的输出流对象,同样是std::ostream类的实例。它与std::cout类似,输出会被缓冲,但通常用于输出程序运行过程中的一些日志信息,方便调试和监控程序的执行情况。
    #include <iostream>
    
    int main() {
        std::clog << "程序开始执行" << std::endl;
    
        // 程序的主要逻辑
    
        std::clog << "程序执行完毕" << std::endl;
    
        return 0;
    }
    

文件输入输出流

std::ifstream

std::ifstream用于从文件中读取数据,它是std::istream的派生类。在使用std::ifstream之前,需要包含<fstream>头文件。

下面是一个从文件中读取整数并输出的示例:

#include <iostream>
#include <fstream>

int main() {
    std::ifstream inputFile("data.txt");
    if (!inputFile) {
        std::cerr << "无法打开文件" << std::endl;
        return 1;
    }

    int num;
    while (inputFile >> num) {
        std::cout << "从文件中读取的整数: " << num << std::endl;
    }

    inputFile.close();

    return 0;
}

在上述代码中,std::ifstream inputFile("data.txt")尝试打开名为“data.txt”的文件。如果文件打开失败,inputFile将被设置为 false,此时输出错误信息并退出程序。然后通过while (inputFile >> num)循环从文件中读取整数并输出,最后关闭文件。

std::ofstream

std::ofstream用于向文件中写入数据,它是std::ostream的派生类。同样需要包含<fstream>头文件。

以下是一个向文件中写入字符串的示例:

#include <iostream>
#include <fstream>

int main() {
    std::ofstream outputFile("output.txt");
    if (!outputFile) {
        std::cerr << "无法创建文件" << std::endl;
        return 1;
    }

    const char* message = "这是写入文件的内容";
    outputFile << message << std::endl;

    outputFile.close();

    return 0;
}

在上述代码中,std::ofstream outputFile("output.txt")尝试创建名为“output.txt”的文件。如果文件创建失败,输出错误信息并退出程序。然后使用outputFile << message << std::endl将字符串写入文件,并最后关闭文件。

std::fstream

std::fstream既可以用于读取文件,也可以用于写入文件,它是std::iostream的派生类。以下是一个使用std::fstream读取文件内容并修改后再写回文件的示例:

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

int main() {
    std::fstream file("example.txt", std::ios::in | std::ios::out);
    if (!file) {
        std::cerr << "无法打开文件" << std::endl;
        return 1;
    }

    std::string line;
    std::string modifiedLine;
    std::string tempFileName = "temp.txt";
    std::ofstream tempFile(tempFileName);

    while (std::getline(file, line)) {
        // 假设这里对每行内容进行修改
        modifiedLine = "修改后的内容: " + line;
        tempFile << modifiedLine << std::endl;
    }

    file.close();
    tempFile.close();

    // 删除原文件并将临时文件重命名为原文件名
    std::remove("example.txt");
    std::rename(tempFileName.c_str(), "example.txt");

    return 0;
}

在上述代码中,std::fstream file("example.txt", std::ios::in | std::ios::out)以读写模式打开文件。通过std::getline逐行读取文件内容,进行修改后写入临时文件。最后删除原文件并将临时文件重命名为原文件名。

字符串流

std::istringstream

std::istringstream用于从字符串中读取数据,它是std::istream的派生类,定义在<sstream>头文件中。这在解析字符串中的数据时非常有用。

以下是一个从字符串中读取整数和字符串的示例:

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

int main() {
    std::string data = "42 Hello";
    std::istringstream iss(data);

    int num;
    std::string str;

    iss >> num >> str;

    std::cout << "从字符串中读取的整数: " << num << std::endl;
    std::cout << "从字符串中读取的字符串: " << str << std::endl;

    return 0;
}

在上述代码中,std::istringstream iss(data)将字符串data作为输入源。通过iss >> num >> str从字符串中依次读取整数和字符串。

std::ostringstream

std::ostringstream用于将数据写入到字符串中,它是std::ostream的派生类,同样定义在<sstream>头文件中。这在需要动态生成字符串时非常方便。

以下是一个将整数和字符串写入字符串流并获取最终字符串的示例:

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

int main() {
    int num = 42;
    std::string str = "Hello";

    std::ostringstream oss;
    oss << "数字: " << num << ", 字符串: " << str;

    std::string result = oss.str();
    std::cout << result << std::endl;

    return 0;
}

在上述代码中,std::ostringstream oss创建一个字符串流对象。通过oss << "数字: " << num << ", 字符串: " << str将数据写入字符串流。最后通过oss.str()获取最终生成的字符串。

流的缓冲区

缓冲区的概念

流缓冲区(Stream Buffer)是流与外部设备之间的一个数据缓存区域。它的主要作用是提高输入输出操作的效率。当进行输入操作时,数据先从外部设备读取到缓冲区,然后程序从缓冲区中读取数据;当进行输出操作时,程序先将数据写入缓冲区,缓冲区满或者满足一定条件时,数据才会被真正写入到外部设备。

例如,当使用std::cout输出数据时,数据并不是立即显示在屏幕上,而是先被写入到输出缓冲区中。只有当缓冲区满、程序结束、调用std::endl(它不仅输出换行符,还会刷新缓冲区)或者调用std::flush(专门用于刷新缓冲区)时,缓冲区中的数据才会被输出到屏幕上。

缓冲区的类型

  1. 全缓冲:对于全缓冲,缓冲区会在填满时才将数据写入到外部设备。文件通常使用全缓冲。例如,当使用std::ofstream向文件写入数据时,数据先被写入到缓冲区,当缓冲区满或者调用flush函数或者关闭文件时,数据才会被真正写入到文件中。
  2. 行缓冲:行缓冲会在遇到换行符(\n)时将缓冲区中的数据写入到外部设备。std::cout通常使用行缓冲,这就是为什么当输出内容包含换行符时,数据会立即显示在屏幕上。
  3. 无缓冲:无缓冲意味着数据不会在缓冲区停留,而是直接写入到外部设备。std::cerr通常是无缓冲的,这确保了错误信息能够立即输出,而不会因为缓冲区的原因而延迟。

控制缓冲区

  1. std::endl:如前所述,std::endl不仅输出一个换行符,还会刷新输出缓冲区。例如:
#include <iostream>

int main() {
    std::cout << "这是一行输出" << std::endl;
    return 0;
}

在上述代码中,std::cout << "这是一行输出" << std::endl输出字符串并换行,同时刷新缓冲区,确保数据立即显示在屏幕上。

  1. std::flushstd::flush专门用于刷新输出缓冲区,不输出任何字符。例如:
#include <iostream>

int main() {
    std::cout << "这是一些输出";
    std::cout << std::flush;
    return 0;
}

在上述代码中,std::cout << std::flush刷新了缓冲区,使得前面输出的“这是一些输出”立即显示在屏幕上。

  1. std::unitbuf和std::nounitbuf**:std::unitbuf设置流为无缓冲模式,std::nounitbuf恢复流为默认的缓冲模式。例如:
#include <iostream>

int main() {
    std::cout << std::unitbuf; // 设置为无缓冲模式
    std::cout << "这是无缓冲输出";
    std::cout << std::nounitbuf; // 恢复为默认缓冲模式
    std::cout << "这是恢复缓冲后的输出" << std::endl;
    return 0;
}

在上述代码中,先设置std::cout为无缓冲模式,输出的“这是无缓冲输出”会立即显示;然后恢复为默认缓冲模式,“这是恢复缓冲后的输出”会根据默认缓冲规则进行输出。

自定义缓冲区

在某些情况下,我们可能需要自定义流缓冲区。C++ 提供了std::streambuf类作为所有流缓冲区的基类,我们可以通过继承std::streambuf类并实现其虚函数来创建自定义的流缓冲区。

以下是一个简单的自定义输出流缓冲区示例,它将输出的字符全部转换为大写:

#include <iostream>
#include <sstream>
#include <cctype>

class UpperCaseBuffer : public std::streambuf {
public:
    explicit UpperCaseBuffer(std::streambuf* sb) : sb_(sb) {}

protected:
    int_type overflow(int_type c) override {
        if (traits_type::eq_int_type(c, traits_type::eof())) {
            return sb_->sputc(traits_type::eof());
        } else {
            return sb_->sputc(toupper(traits_type::to_char_type(c)));
        }
    }

    std::streampos seekoff(std::streamoff off, std::ios_base::seekdir way,
                           std::ios_base::openmode which = std::ios_base::in | std::ios_base::out) override {
        return sb_->pubseekoff(off, way, which);
    }

    std::streampos seekpos(std::streampos sp,
                           std::ios_base::openmode which = std::ios_base::in | std::ios_base::out) override {
        return sb_->pubseekpos(sp, which);
    }

private:
    std::streambuf* sb_;
};

class UpperCaseStream : public std::ostream {
public:
    explicit UpperCaseStream(std::streambuf* sb) : std::ostream(sb) {}
};

int main() {
    std::ostringstream oss;
    UpperCaseBuffer ucb(oss.rdbuf());
    UpperCaseStream ucs(&ucb);

    ucs << "hello, world!" << std::endl;

    std::cout << oss.str() << std::endl;

    return 0;
}

在上述代码中,UpperCaseBuffer继承自std::streambuf,重写了overflowseekoffseekpos等虚函数。overflow函数将输出的字符转换为大写后再传递给底层的流缓冲区。UpperCaseStream继承自std::ostream,使用自定义的UpperCaseBuffer作为其流缓冲区。最后,通过UpperCaseStream输出的字符串“hello, world!”会被转换为大写后存储在oss中,并输出到屏幕上。

流的格式化输出

控制符

C++ 提供了一系列控制符(Manipulators)来控制流的输出格式。这些控制符可以直接插入到流中,用于设置诸如宽度、精度、填充字符、进制等格式选项。

  1. 设置宽度std::setw(int n)用于设置输出的宽度为n。如果输出内容的长度小于n,则会使用填充字符(默认为空格)进行填充。
#include <iostream>
#include <iomanip>

int main() {
    int num = 123;
    std::cout << std::setw(5) << num << std::endl; // 输出宽度为5,不足5位用空格填充
    return 0;
}
  1. 设置精度std::setprecision(int n)用于设置浮点数的精度,即小数点后的位数。
#include <iostream>
#include <iomanip>

int main() {
    double pi = 3.1415926;
    std::cout << std::setprecision(3) << pi << std::endl; // 输出精度为3,即3.14
    return 0;
}
  1. 设置填充字符std::setfill(char c)用于设置填充字符为c
#include <iostream>
#include <iomanip>

int main() {
    int num = 123;
    std::cout << std::setfill('0') << std::setw(5) << num << std::endl; // 填充字符设为0,宽度为5,输出00123
    return 0;
}
  1. 设置进制std::hex用于设置输出为十六进制,std::dec用于设置为十进制,std::oct用于设置为八进制。
#include <iostream>
#include <iomanip>

int main() {
    int num = 255;
    std::cout << std::hex << num << std::endl; // 输出十六进制ff
    std::cout << std::dec << num << std::endl; // 输出十进制255
    std::cout << std::oct << num << std::endl; // 输出八进制377
    return 0;
}

自定义格式化

除了使用标准的控制符,我们还可以自定义格式化输出。这可以通过重载operator<<来实现。

以下是一个自定义日期类并实现自定义格式化输出的示例:

#include <iostream>
#include <iomanip>
#include <sstream>

class Date {
public:
    Date(int year, int month, int day) : year_(year), month_(month), day_(day) {}

    friend std::ostream& operator<<(std::ostream& os, const Date& date) {
        std::ostringstream oss;
        oss << std::setfill('0') << std::setw(4) << date.year_ << "-"
            << std::setw(2) << date.month_ << "-"
            << std::setw(2) << date.day_;
        return os << oss.str();
    }

private:
    int year_;
    int month_;
    int day_;
};

int main() {
    Date date(2023, 10, 5);
    std::cout << date << std::endl; // 输出2023-10-05
    return 0;
}

在上述代码中,Date类重载了operator<<,在函数内部使用std::ostringstream和控制符来实现日期的自定义格式化输出。

流的错误处理

流状态标志

C++ 的流对象维护了一些状态标志,用于表示流的当前状态,这些状态标志可以通过流对象的rdstate成员函数获取。

  1. std::ios::goodbit:表示流处于正常状态,没有发生任何错误。
  2. std::ios::eofbit:表示流已经到达文件末尾(对于文件流)或输入结束(对于输入流)。
  3. std::ios::failbit:表示输入输出操作失败,但不是因为到达文件末尾。例如,当从输入流中读取的数据格式不正确时,failbit会被设置。
  4. std::ios::badbit:表示流发生了严重错误,例如硬件故障、文件损坏等,流可能无法再继续使用。

以下是一个检查流状态标志的示例:

#include <iostream>
#include <fstream>

int main() {
    std::ifstream inputFile("nonexistent.txt");
    if (inputFile.fail()) {
        std::cerr << "文件打开失败" << std::endl;
        if (inputFile.eof()) {
            std::cerr << "到达文件末尾" << std::endl;
        } else if (inputFile.bad()) {
            std::cerr << "发生严重错误" << std::endl;
        }
    } else {
        std::cout << "文件打开成功" << std::endl;
        inputFile.close();
    }

    return 0;
}

在上述代码中,尝试打开一个不存在的文件,通过inputFile.fail()检查文件打开是否失败,并进一步检查eofbitbadbit状态标志来确定失败原因。

清除和设置流状态

  1. clear:流对象的clear成员函数用于清除流的状态标志,将流恢复到正常状态。例如,当读取数据失败导致failbit被设置后,可以调用clear来清除该标志,以便继续进行操作。
#include <iostream>

int main() {
    std::string str = "abc";
    int num;
    std::istringstream iss(str);

    iss >> num; // 读取失败,failbit被设置
    if (iss.fail()) {
        std::cerr << "读取失败" << std::endl;
        iss.clear(); // 清除failbit
    }

    return 0;
}
  1. setstatesetstate成员函数用于设置流的状态标志。例如,可以手动设置eofbit来模拟文件结束的情况。
#include <iostream>
#include <sstream>

int main() {
    std::string str = "123";
    std::istringstream iss(str);

    // 手动设置eofbit
    iss.setstate(std::ios::eofbit);

    int num;
    if (iss >> num) {
        std::cout << "读取成功: " << num << std::endl;
    } else {
        std::cerr << "读取失败,可能到达文件末尾" << std::endl;
    }

    return 0;
}

总结

C++ 的流和缓冲区机制为程序的输入输出操作提供了强大而灵活的支持。通过理解和熟练运用流的各种类型(标准输入输出流、文件流、字符串流)、缓冲区的工作原理、格式化输出以及错误处理等方面的知识,程序员能够高效地处理与外部设备的数据交互,编写健壮、可读的代码。无论是简单的控制台程序,还是复杂的文件处理和网络应用,流和缓冲区都是不可或缺的重要组成部分。在实际编程中,应根据具体需求选择合适的流类型和操作方式,并合理处理缓冲区和错误情况,以确保程序的稳定性和性能。