C++ 流和缓冲区
C++ 流概述
在 C++ 编程中,流(Stream)是一种抽象的概念,用于在程序和外部设备(如文件、控制台、网络连接等)之间进行数据的输入和输出操作。流提供了一种统一的接口,使得程序员可以以相同的方式处理不同类型的输入输出源和目标。
流的概念源于 Unix 操作系统中的“一切皆文件”理念。在 C++ 中,流将数据的读取和写入操作抽象成连续的字节序列,就像水流一样源源不断。通过流,我们可以轻松地从键盘读取数据、向屏幕输出数据,也可以对文件进行读写操作。
C++ 标准库提供了一系列与流相关的类和对象,这些类和对象被组织在<iostream>
、<fstream>
和<sstream>
等头文件中。<iostream>
主要用于控制台输入输出,<fstream>
用于文件的输入输出,<sstream>
用于字符串流的操作。
流的分类
- 输入流(InputStream):用于从外部设备读取数据到程序中。例如,从键盘读取用户输入的数据,或者从文件中读取数据。在 C++ 中,
std::istream
及其派生类(如std::cin
)用于实现输入流操作。 - 输出流(OutputStream):用于将程序中的数据写入到外部设备。例如,将数据输出到屏幕,或者写入到文件中。在 C++ 中,
std::ostream
及其派生类(如std::cout
)用于实现输出流操作。 - 输入输出流(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
- std::cerr:
std::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; }
- std::clog:
std::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
(专门用于刷新缓冲区)时,缓冲区中的数据才会被输出到屏幕上。
缓冲区的类型
- 全缓冲:对于全缓冲,缓冲区会在填满时才将数据写入到外部设备。文件通常使用全缓冲。例如,当使用
std::ofstream
向文件写入数据时,数据先被写入到缓冲区,当缓冲区满或者调用flush
函数或者关闭文件时,数据才会被真正写入到文件中。 - 行缓冲:行缓冲会在遇到换行符(
\n
)时将缓冲区中的数据写入到外部设备。std::cout
通常使用行缓冲,这就是为什么当输出内容包含换行符时,数据会立即显示在屏幕上。 - 无缓冲:无缓冲意味着数据不会在缓冲区停留,而是直接写入到外部设备。
std::cerr
通常是无缓冲的,这确保了错误信息能够立即输出,而不会因为缓冲区的原因而延迟。
控制缓冲区
- std::endl:如前所述,
std::endl
不仅输出一个换行符,还会刷新输出缓冲区。例如:
#include <iostream>
int main() {
std::cout << "这是一行输出" << std::endl;
return 0;
}
在上述代码中,std::cout << "这是一行输出" << std::endl
输出字符串并换行,同时刷新缓冲区,确保数据立即显示在屏幕上。
- std::flush:
std::flush
专门用于刷新输出缓冲区,不输出任何字符。例如:
#include <iostream>
int main() {
std::cout << "这是一些输出";
std::cout << std::flush;
return 0;
}
在上述代码中,std::cout << std::flush
刷新了缓冲区,使得前面输出的“这是一些输出”立即显示在屏幕上。
- 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
,重写了overflow
、seekoff
和seekpos
等虚函数。overflow
函数将输出的字符转换为大写后再传递给底层的流缓冲区。UpperCaseStream
继承自std::ostream
,使用自定义的UpperCaseBuffer
作为其流缓冲区。最后,通过UpperCaseStream
输出的字符串“hello, world!”会被转换为大写后存储在oss
中,并输出到屏幕上。
流的格式化输出
控制符
C++ 提供了一系列控制符(Manipulators)来控制流的输出格式。这些控制符可以直接插入到流中,用于设置诸如宽度、精度、填充字符、进制等格式选项。
- 设置宽度:
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;
}
- 设置精度:
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;
}
- 设置填充字符:
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;
}
- 设置进制:
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
成员函数获取。
- std::ios::goodbit:表示流处于正常状态,没有发生任何错误。
- std::ios::eofbit:表示流已经到达文件末尾(对于文件流)或输入结束(对于输入流)。
- std::ios::failbit:表示输入输出操作失败,但不是因为到达文件末尾。例如,当从输入流中读取的数据格式不正确时,
failbit
会被设置。 - 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()
检查文件打开是否失败,并进一步检查eofbit
和badbit
状态标志来确定失败原因。
清除和设置流状态
- 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;
}
- setstate:
setstate
成员函数用于设置流的状态标志。例如,可以手动设置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++ 的流和缓冲区机制为程序的输入输出操作提供了强大而灵活的支持。通过理解和熟练运用流的各种类型(标准输入输出流、文件流、字符串流)、缓冲区的工作原理、格式化输出以及错误处理等方面的知识,程序员能够高效地处理与外部设备的数据交互,编写健壮、可读的代码。无论是简单的控制台程序,还是复杂的文件处理和网络应用,流和缓冲区都是不可或缺的重要组成部分。在实际编程中,应根据具体需求选择合适的流类型和操作方式,并合理处理缓冲区和错误情况,以确保程序的稳定性和性能。