在C++标准库中,流(stream)是一个核心抽象概念,它代表了数据的流动方向。就像水管中的水流一样,数据可以从源头流向目的地。C++通过流类体系为我们提供了统一的数据处理接口,无论是操作内存中的字符串还是磁盘上的文件,都可以使用相似的语法和操作方式。
流主要分为输入流和输出流两种基本类型:
在标准库中,iostream头文件定义了基本的流类体系:
cpp复制ios_base → ios → istream
↘ ostream
↘ iostream
stringstream是C++中处理内存字符串的强大工具,它位于
创建和使用stringstream的基本示例:
cpp复制#include <sstream>
#include <iostream>
int main() {
std::stringstream ss;
// 写入数据
ss << "Hello" << " " << 2023 << "!";
// 读取数据
std::string result = ss.str();
std::cout << result << std::endl; // 输出:Hello 2023!
return 0;
}
标准库提供了三种stringstream变体,适用于不同场景:
istringstream:只读字符串流
cpp复制std::istringstream iss("123 45.67");
int i; double d;
iss >> i >> d; // 从字符串解析数据
ostringstream:只写字符串流
cpp复制std::ostringstream oss;
oss << "The answer is " << 42;
std::string s = oss.str(); // 获取构建的字符串
stringstream:可读可写的字符串流
stringstream最常见的用途之一就是实现各种类型之间的安全转换:
cpp复制template <typename T>
std::string toString(const T& value) {
std::ostringstream oss;
oss << value;
return oss.str();
}
template <typename T>
T fromString(const std::string& str) {
std::istringstream iss(str);
T value;
iss >> value;
return value;
}
// 使用示例
double d = fromString<double>("3.14159");
std::string s = toString(42);
处理CSV格式数据或日志文件时,stringstream非常有用:
cpp复制std::string data = "John,Doe,35,New York";
std::stringstream ss(data);
std::string token;
while (std::getline(ss, token, ',')) {
std::cout << token << std::endl;
}
当需要构建包含多种数据类型的复杂字符串时,ostringstream比直接拼接更清晰:
cpp复制std::ostringstream report;
report << "Sales Report\n"
<< "------------\n"
<< "Date: " << getCurrentDate() << "\n"
<< "Total: $" << calculateTotal() << "\n"
<< "Items: " << itemCount << "\n";
注意事项:stringstream虽然方便,但频繁创建和销毁会影响性能。在性能关键代码中,可以考虑重用stringstream对象或使用更高效的方法。
C++中的文件流位于
基本文件操作示例:
cpp复制#include <fstream>
#include <iostream>
int main() {
// 写入文件
std::ofstream outFile("example.txt");
if (outFile) {
outFile << "This is a test file.\n";
outFile << 123 << " " << 3.14 << "\n";
}
// 读取文件
std::ifstream inFile("example.txt");
if (inFile) {
std::string line;
while (std::getline(inFile, line)) {
std::cout << line << std::endl;
}
}
return 0;
}
文件流构造函数或open()方法可以指定打开模式,这些模式是位掩码,可以用|组合:
| 模式标志 | 描述 |
|---|---|
| std::ios::in | 打开用于读取 |
| std::ios::out | 打开用于写入 |
| std::ios::binary | 二进制模式 |
| std::ios::app | 追加模式(总是在末尾写入) |
| std::ios::ate | 打开后定位到文件末尾 |
| std::ios::trunc | 如果文件存在则清空 |
常见组合示例:
cpp复制// 追加写入(不会覆盖原有内容)
std::ofstream logFile("app.log", std::ios::app);
// 二进制读写
std::fstream binFile("data.bin",
std::ios::in | std::ios::out | std::ios::binary);
文件流提供了控制读写位置的成员函数:
示例:读取文件最后100字节
cpp复制std::ifstream file("largefile.bin", std::ios::binary);
if (file) {
file.seekg(0, std::ios::end); // 跳到文件末尾
std::streampos fileSize = file.tellg();
if (fileSize >= 100) {
file.seekg(-100, std::ios::end);
char buffer[100];
file.read(buffer, sizeof(buffer));
// 处理最后100字节数据...
}
}
处理二进制数据(如图片、音频等)时,必须使用二进制模式:
cpp复制struct Record {
int id;
char name[32];
double value;
};
// 写入二进制数据
Record rec = {1, "test", 3.14};
std::ofstream binOut("records.bin", std::ios::binary);
binOut.write(reinterpret_cast<char*>(&rec), sizeof(Record));
// 读取二进制数据
Record inRec;
std::ifstream binIn("records.bin", std::ios::binary);
binIn.read(reinterpret_cast<char*>(&inRec), sizeof(Record));
重要提示:二进制操作要注意平台兼容性问题,如字节序、结构体对齐等。跨平台应用建议使用专门的序列化库。
每个流对象都维护一组状态标志,可以通过以下成员函数检查:
| 函数 | 描述 |
|---|---|
| good() | 流处于正常状态 |
| eof() | 到达文件末尾 |
| fail() | 操作失败但流仍可用 |
| bad() | 严重错误,流可能不可用 |
| clear() | 重置状态标志 |
正确处理流状态的典型模式:
cpp复制std::ifstream file("data.txt");
if (!file) {
// 文件打开失败处理
std::cerr << "无法打开文件!" << std::endl;
return;
}
int value;
while (file >> value) {
// 成功读取一个整数
process(value);
}
if (file.eof()) {
std::cout << "到达文件末尾" << std::endl;
} else if (file.fail()) {
std::cerr << "格式错误" << std::endl;
} else if (file.bad()) {
std::cerr << "严重错误" << std::endl;
}
文件打开失败:
格式读取错误:
二进制文件问题:
调试技巧:
cpp复制// 打印详细的错误信息
void checkStream(const std::ios& stream) {
if (stream.good()) {
std::cout << "流状态正常" << std::endl;
} else {
std::cout << "流状态错误: ";
if (stream.eof()) std::cout << "EOF ";
if (stream.fail()) std::cout << "Fail ";
if (stream.bad()) std::cout << "Bad ";
std::cout << std::endl;
}
}
频繁的I/O操作是性能瓶颈,可以采用缓冲策略:
cpp复制// 低效方式:每次写入一行
for (const auto& item : items) {
outFile << item << "\n"; // 每次都会flush
}
// 高效方式:构建完整字符串后一次性写入
std::ostringstream buffer;
for (const auto& item : items) {
buffer << item << "\n";
}
outFile << buffer.str();
通过继承std::streambuf可以创建自定义流缓冲区,实现特殊功能:
cpp复制class TeeBuffer : public std::streambuf {
public:
TeeBuffer(std::streambuf* buf1, std::streambuf* buf2)
: buf1(buf1), buf2(buf2) {}
protected:
virtual int overflow(int c) override {
if (c == EOF) {
return !EOF;
} else {
int r1 = buf1->sputc(c);
int r2 = buf2->sputc(c);
return (r1 == EOF || r2 == EOF) ? EOF : c;
}
}
virtual int sync() override {
int r1 = buf1->pubsync();
int r2 = buf2->pubsync();
return (r1 == 0 && r2 == 0) ? 0 : -1;
}
private:
std::streambuf* buf1;
std::streambuf* buf2;
};
// 使用示例:同时输出到cout和文件
std::ofstream logFile("log.txt");
TeeBuffer teeBuffer(std::cout.rdbuf(), logFile.rdbuf());
std::ostream teeStream(&teeBuffer);
teeStream << "这条消息会同时显示在屏幕和日志文件中\n";
对于超大文件处理,可以考虑使用内存映射(需要操作系统支持):
cpp复制// Windows示例
HANDLE hFile = CreateFile("large.bin", GENERIC_READ,
FILE_SHARE_READ, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL);
HANDLE hMap = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
LPVOID pData = MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);
// 现在可以像数组一样访问文件内容
const char* data = static_cast<const char*>(pData);
size_t fileSize = GetFileSize(hFile, NULL);
// 使用完毕后
UnmapViewOfFile(pData);
CloseHandle(hMap);
CloseHandle(hFile);
性能提示:对于频繁读写的小文件,可以考虑完全读入内存处理。对于大文件,使用流式处理或内存映射。
实现一个简单的INI格式配置文件解析器:
cpp复制#include <sstream>
#include <fstream>
#include <map>
#include <string>
class ConfigParser {
public:
bool load(const std::string& filename) {
std::ifstream file(filename);
if (!file) return false;
std::string line, section, key, value;
while (std::getline(file, line)) {
std::istringstream iss(line);
if (line.empty() || line[0] == ';') continue;
if (line[0] == '[') {
// 处理节段
size_t pos = line.find(']');
if (pos != std::string::npos) {
section = line.substr(1, pos-1);
}
} else {
// 处理键值对
size_t pos = line.find('=');
if (pos != std::string::npos) {
key = line.substr(0, pos);
value = line.substr(pos+1);
data[section + "." + key] = value;
}
}
}
return true;
}
std::string get(const std::string& key,
const std::string& defaultValue = "") const {
auto it = data.find(key);
return (it != data.end()) ? it->second : defaultValue;
}
private:
std::map<std::string, std::string> data;
};
// 使用示例
ConfigParser config;
if (config.load("settings.ini")) {
std::string server = config.get("database.server", "localhost");
int port = std::stoi(config.get("database.port", "3306"));
}
构建一个线程安全的日志系统:
cpp复制#include <fstream>
#include <sstream>
#include <mutex>
#include <chrono>
#include <iomanip>
class Logger {
public:
enum Level { DEBUG, INFO, WARNING, ERROR };
Logger(const std::string& filename)
: logFile(filename, std::ios::app) {}
~Logger() {
if (logFile.is_open()) {
logFile.close();
}
}
void log(Level level, const std::string& message) {
std::lock_guard<std::mutex> lock(mutex);
if (!logFile) return;
auto now = std::chrono::system_clock::now();
auto time = std::chrono::system_clock::to_time_t(now);
logFile << std::put_time(std::localtime(&time), "%Y-%m-%d %H:%M:%S")
<< " [" << levelToString(level) << "] "
<< message << std::endl;
}
private:
std::ofstream logFile;
std::mutex mutex;
std::string levelToString(Level level) {
switch (level) {
case DEBUG: return "DEBUG";
case INFO: return "INFO";
case WARNING: return "WARNING";
case ERROR: return "ERROR";
default: return "UNKNOWN";
}
}
};
// 使用示例
Logger logger("app.log");
logger.log(Logger::INFO, "Application started");
logger.log(Logger::ERROR, "Failed to open file: data.txt");
实现一个简单的对象序列化框架:
cpp复制#include <sstream>
#include <fstream>
#include <vector>
class Serializable {
public:
virtual ~Serializable() = default;
virtual void serialize(std::ostream& out) const = 0;
virtual void deserialize(std::istream& in) = 0;
bool saveToFile(const std::string& filename) const {
std::ofstream file(filename, std::ios::binary);
if (!file) return false;
serialize(file);
return file.good();
}
bool loadFromFile(const std::string& filename) {
std::ifstream file(filename, std::ios::binary);
if (!file) return false;
deserialize(file);
return file.good();
}
};
class Person : public Serializable {
public:
std::string name;
int age;
std::vector<std::string> hobbies;
void serialize(std::ostream& out) const override {
size_t nameLen = name.size();
out.write(reinterpret_cast<const char*>(&nameLen), sizeof(nameLen));
out.write(name.c_str(), nameLen);
out.write(reinterpret_cast<const char*>(&age), sizeof(age));
size_t numHobbies = hobbies.size();
out.write(reinterpret_cast<const char*>(&numHobbies), sizeof(numHobbies));
for (const auto& hobby : hobbies) {
size_t hobbyLen = hobby.size();
out.write(reinterpret_cast<const char*>(&hobbyLen), sizeof(hobbyLen));
out.write(hobby.c_str(), hobbyLen);
}
}
void deserialize(std::istream& in) override {
size_t nameLen;
in.read(reinterpret_cast<char*>(&nameLen), sizeof(nameLen));
name.resize(nameLen);
in.read(&name[0], nameLen);
in.read(reinterpret_cast<char*>(&age), sizeof(age));
size_t numHobbies;
in.read(reinterpret_cast<char*>(&numHobbies), sizeof(numHobbies));
hobbies.resize(numHobbies);
for (auto& hobby : hobbies) {
size_t hobbyLen;
in.read(reinterpret_cast<char*>(&hobbyLen), sizeof(hobbyLen));
hobby.resize(hobbyLen);
in.read(&hobby[0], hobbyLen);
}
}
};
// 使用示例
Person p;
p.name = "Alice";
p.age = 30;
p.hobbies = {"Reading", "Hiking"};
p.saveToFile("person.dat");
Person p2;
p2.loadFromFile("person.dat");
不同操作系统使用不同的行结束符:
处理建议:
不同操作系统的路径分隔符不同:
可移植的路径处理方法:
cpp复制#include <filesystem> // C++17
std::string getConfigPath() {
std::filesystem::path configDir("config");
configDir /= "settings.ini"; // 自动使用正确的分隔符
return configDir.string();
}
对于C++11/14,可以手动处理:
cpp复制#ifdef _WIN32
const char PATH_SEP = '\\';
#else
const char PATH_SEP = '/';
#endif
std::string joinPaths(const std::string& a, const std::string& b) {
if (a.empty()) return b;
if (b.empty()) return a;
return a + PATH_SEP + b;
}
处理非ASCII文本文件时需要注意编码:
处理多语言文本的建议:
cpp复制// 使用UTF-8编码(最通用的Unicode编码方式)
std::ofstream utf8File("utf8.txt", std::ios::binary);
std::string utf8Text = u8"这是UTF-8编码的中文";
utf8File.write(utf8Text.c_str(), utf8Text.size());
// 读取时也需要知道编码
std::ifstream inFile("utf8.txt", std::ios::binary);
std::string content((std::istreambuf_iterator<char>(inFile)),
std::istreambuf_iterator<char>());
// content现在包含UTF-8编码的文本
对于需要转换编码的情况,可以考虑使用第三方库如ICU或iconv。
C++17引入了标准文件系统库,大大简化了文件操作:
cpp复制#include <filesystem>
namespace fs = std::filesystem;
// 检查文件是否存在
if (fs::exists("data.txt")) {
// 获取文件大小
auto size = fs::file_size("data.txt");
// 创建目录
fs::create_directory("backup");
// 复制文件
fs::copy("data.txt", "backup/data.txt");
// 遍历目录
for (const auto& entry : fs::directory_iterator(".")) {
std::cout << entry.path() << std::endl;
}
}
C++20的std::span可以方便地处理内存块:
cpp复制#include <span>
#include <fstream>
void processChunk(std::span<char> buffer) {
// 处理内存块
}
int main() {
std::ifstream file("data.bin", std::ios::binary);
if (file) {
file.seekg(0, std::ios::end);
auto size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<char> buffer(size);
file.read(buffer.data(), size);
processChunk(buffer);
}
}
C++20引入了新的格式化库,可以替代stringstream的部分用途:
cpp复制#include <format>
#include <iostream>
int main() {
std::string name = "Alice";
int age = 30;
// 替代ostringstream
std::string message = std::format("{} is {} years old", name, age);
std::cout << message << std::endl;
return 0;
}
虽然