[C++]:IO流
1. IO 流
1.1 流的概念
在C++中,存在一种被称为“流”的概念,它描述的是信息流动的过程,具体来说就是信息从外部输入设备(比如常见的键盘)传输到计算机内部(像内存区域),以及信息从内存向外部输出设备(例如显示器)传输的过程,因其类似水流一样的连续性,所以被形象地比喻成“流”。
C++的“流”具备两个重要特性:
- 有序连续:意味着信息在流动过程中是按照一定顺序依次进行传输的,就好像水流沿着固定的河道依次流淌那样,不会出现无序混乱的情况。
- 具有方向性:流的传输是有明确方向的,要么是从外部输入设备到内存的输入方向,要么是从内存到外部输出设备的输出方向,不能随意反向或者混淆。
为了能够实现这种信息的流动过程,C++专门定义了I/O标准类库。在这个类库当中包含了多个类,每一个这样的类都被称作“流”或者“流类”,它们各自承担着完成某一方面相关功能的任务,共同协作来保障信息在计算机内外设备之间顺利地实现输入和输出操作。
1.1 C++ IO 流
C++ 系统实现了一个庞大的类库,其中 ios 为基类,其他类都是直接或间接派生自 ios 类。
1.2 iostream
其中 iostream它主要用于标准输入输出操作。其中,std::cin
用于从标准输入设备(通常是键盘)读取数据。std::cout
用于向标准输出设备(一般是显示器)输出数据,就能在屏幕上显示指定的字符串。
比如说下面一个简单的示例,首先用 cout
打印提示信息,然后用 cin
读入,最后再用 cout
打印来验证你输入是否正确。
#include <iostream>
using namespace std;
int main() {
int num;
cout << "请输入一个整数: ";
cin >> num; // 从键盘读取用户输入的整数
cout << "你输入的整数是: " << num << endl;
return 0;
}
我们同样可以使用 cerr
来打印错误信息,比如下面这段代码:
当用户输入的除数为 0
时,程序使用 cerr
输出错误信息,告知用户除数不能为 0
,需要重新输入。因为 cerr
是无缓冲的,所以这个错误提示会马上显示在屏幕上,方便用户及时察觉错误情况并进行相应处理。
#include <iostream>
using namespace std;
int main() {
int dividend, divisor;
cout << "请输入被除数: ";
cin >> dividend;
cout << "请输入除数: ";
cin >> divisor;
if (divisor == 0) {
cerr << "错误: 除数不能为 0,请重新输入!" << endl; // 使用 cerr 输出错误提示,无缓冲,会立即显示
return 1; // 结束程序,返回错误码
}
int result = dividend / divisor;
cout << "除法运算的结果是: " << result << endl;
return 0;
}
并且我们有时候也可以使用 clog
打印日志信息:
#include <iostream>
#include <ctime> // 引入时间相关头文件,用于获取当前时间
using namespace std;
int main() {
time_t now = time(0); // 获取当前时间的时间戳
char* dt = ctime(&now); // 将时间戳转换为字符串形式的日期时间
clog << "程序开始运行,当前时间是: " << dt; // 使用 clog 记录程序开始运行的日志信息,有缓冲
int num;
cout << "请输入一个整数: ";
cin >> num;
cout << "你输入的整数是: " << num << endl;
clog << "用户输入了整数 " << num << endl; // 继续使用 clog 记录用户输入的相关日志信息
return 0;
}
在这里:
- 首先通过
clog
输出程序开始运行时的日志信息,记录下当前的时间,由于clog
是有缓冲的,它会先将这些日志信息暂存起来(通常在缓冲区满了或者程序正常结束等情况下才会输出到屏幕对应的标准错误输出处)。 - 之后在用户输入整数后,又使用
clog
记录用户输入的具体数值相关的日志内容,展示了如何利用clog
在程序运行过程中对一些关键信息进行日志记录,便于后续查看程序执行情况等分析工作。
我们使用 cin
和 cout
可以直接输入和输出 内置类型数据,原因就是标准库已经将所有内置类型的输入和输出全部重载了。
如果我们想使用 cin
,cout
输入输出自定义类型,就需要进行流插入与流提取重载。比如下面我们实现一个日期类的流提取与流插入。
inline ostream& operator<<(ostream& out, const Date& d)//流插入
{
out << d._year << "/" << d._month << "/" << d._day <<endl;
return out;
}
inline istream& operator>>(istream& in, Date& d)//流提取
{
in >> d._year >> d._month >> d._day;
return in;
}
并且我们一般在刷 OJ 练习题时,可能会使用一些这样的做法:
// 单个元素循环输入
while(cin>>a)
{
// ...
}
// 多个元素循环输入
while(cin>>a>>b>>c)
{
// ...
}
// 整行接收
while(cin>>str)
{
// ...
}
但是你可能会提出这样的疑问:就是 while
语句的判断条件一般是 布尔类型,而 cin
返回的 istream
对象怎么能够做 while
的判断条件呢?
其实就是 istream
实现了 bool
类型转换运算符重载:
1.3 fstream
fstream 需要包含头文件<fstream>
。其用于文件的输入输出操作。它包含了 ifstream
(用于从文件中读取数据,即文件输入流)、ofstream
(用于向文件中写入数据,即文件输出流)以及 fstream
(既可以读又可以写文件的流对象)这几个类。
首先我们来介绍一下常见的文件操作函数:
函数名 | 功能 |
---|---|
getline | 常用于从输入流(比如std::cin 或者文件输入流ifstream 等)中读取一行字符串,它可以处理包含空格等空白字符的整行文本内容,参数一般是输入流对象和用于存储读取字符串的字符串变量。例如从文件中逐行读取文本内容进行后续处理时经常会用到它。 |
put | 向输出流写入单个字符,例如可以用于向文件输出流(ofstream )或者标准输出流(cout )写入特定的字符。 |
write | 用于将一段指定长度的字符数组(或可看作字节序列)写入到输出流中,常用于文件输出操作,可按照设定的字节数将内存中的数据准确地写入到文件等输出目标中。 |
read | 从输入流中读取指定长度的字节数据到字符数组(或其他合适的内存区域)中,常用于文件输入操作,比如从文件里读取一定字节数的数据进行后续解析等处理。 |
open | 用于打开文件,是文件流类(ifstream 、ofstream 、fstream )的成员函数,通过传入文件名等参数,按照指定的模式(如只读、只写、读写等)打开文件,为后续的文件输入输出操作做准备。 |
close | 对应文件流的 open 操作,用于关闭已经打开的文件,释放相关的系统资源,保证文件操作的正常结束和资源合理利用。 |
is_open | 文件流类的成员函数,用于判断文件是否已经成功打开,返回 bool 类型的值,在进行文件输入输出操作前,通常先通过这个函数检查文件打开情况,避免后续对未成功打开的文件进行无效操作。 |
seekg | 主要用于文件输入流(ifstream )或者可读写的文件流(fstream 在读取模式下),可以设置文件读取指针的位置,实现从文件的指定位置开始读取数据,比如可以跳到文件中间某个位置读取后续内容。 |
seekp | 针对文件输出流(ofstream )或者可读写的文件流(fstream 在写入模式下),用于设置文件写入指针的位置,决定后续向文件中写入数据的起始位置,例如可以覆盖文件中某个特定位置的原有内容等。 |
tellg | 用于获取文件输入流(ifstream )或者可读写的文件流(fstream 在读取模式下)当前读取指针的位置,返回值通常是一个表示位置的偏移量,便于记录或者后续根据该位置进行相关操作。 |
tellp | 对应文件输出流(ofstream )或者可读写的文件流(fstream 在写入模式下),可获取当前写入指针的位置,同样返回表示位置偏移量的值,方便对文件写入操作进行定位和管理。 |
1.3.1 ifstream
首先我们可以使用 ifstream
对文件进行对取。
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
ifstream ifile("input.txt"); // 尝试打开名为input.txt的文件
if (ifile.is_open()) { // 判断文件是否成功打开
string line;
while (getline(ifile, line)) { // 逐行读取文件内容
cout << line << endl; // 将读取到的每行内容输出到屏幕显示
}
ifile.close(); // 读取完成后关闭文件
} else {
cerr << "无法打开文件 input.txt" << endl;
}
return 0;
}
上述代码尝试打开 input.txt
文件,若成功打开,则逐行读取文件内容并输出到屏幕,最后关闭文件;若打开失败,会输出错误提示信息。
1.3.2 ofstream
然后我们也可以使用 ofstream
对文件进行写入:
#include <iostream>
#include <fstream>
using namespace std;
int main() {
ofstream ofile("output.txt"); // 创建并打开名为output.txt的文件
if (ofile.is_open()) {
ofile << "这是写入文件的一些内容" << endl; // 向文件写入内容
ofile.close(); // 写完后关闭文件
} else {
cerr << "无法打开文件 output.txt" << endl;
}
return 0;
}
此示例创建 output.txt
文件(若不存在)并向其写入一行文字,操作结束后关闭文件;若文件打开失败会输出相应错误提示。
1.3.3 fstream
fstream 它兼具输入和输出的功能,既可以读取文件里的数据,也能往文件里写入数据,灵活性更高。一般会与常见的文件打开方式标志结合使用:
打开方式标志 | 含义 |
---|---|
ios::in | 以输入(读取)模式打开文件。用于 ifstream 时,表示从已存在的文件中读取数据;用于 fstream 时,使文件可用于读取操作。如果文件不存在,使用 ifstream 打开会失败。 |
ios::out | 以输出(写入)模式打开文件。对于 ofstream ,会创建新文件(若文件不存在)或者截断(覆盖)已有文件内容来写入数据;用于 fstream 时,使文件可用于写入操作。 |
ios::app | 以追加模式打开文件,即写入的数据总是添加在文件末尾,不会覆盖原有文件内容。常用于 ofstream 或者 fstream 在写入相关操作时,确保每次新写入的内容接续在已有内容之后。 |
ios::ate | 打开文件后立即将文件指针定位到文件末尾(“ate”表示“at the end”),可以用于后续的读写操作。常用于 fstream 这样既能读又能写的流,后续可以从文件末尾往前读取内容或者继续往文件末尾添加内容等。 |
ios::binary | 以二进制模式打开文件,用于处理二进制数据文件,区别于默认的文本模式。在进行文件读写操作时,不会对数据进行文本格式相关的转换(比如换行符的转换等),适用于读写非文本格式的文件,像图片、音频等文件或者自定义二进制格式的数据文件,可与其他打开方式标志组合使用,如 `ios::in |
这些打开方式标志可以通过逻辑或(|
)运算符进行组合使用,以满足不同的文件操作需求。例如,ios::in | ios::out | ios::binary
表示以二进制模式打开文件,并且这个文件既可以进行读取操作也可以进行写入操作。
比如下面代码,我们先读取文件内容,再向文件中写入相关数据。
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
fstream iofile("test.txt", ios::in | ios::out); // 以可读可写模式打开test.txt文件
if (iofile.is_open()) {
string line;
// 先读取文件已有的内容并输出到屏幕
while (getline(iofile, line)) {
cout << line << endl;
}
// 将新内容写入文件
iofile << "新添加的一行内容" << endl;
iofile.close();
} else {
cerr << "无法打开文件 test.txt" << endl;
}
return 0;
}
代码中以可读可写模式打开 test.txt
文件,先读取文件原有的内容并显示出来,然后往文件里写入新的内容,最后关闭文件;若打开失败则输出错误提示。
最后我们可以将相关的文件操作封装成类,再进行使用:
#include<iostream>
#include<string>
#include<fstream>
// 定义结构体ServerInfo,用于存储服务器相关信息,包含IP地址(用字符数组表示)和端口号
struct ServerInfo
{
char _ip[32];
int _port;
};
// ConfigManager类用于管理配置文件的读写操作
class ConfigManager
{
public:
// 构造函数,默认配置文件名是"liren.config",可传入其他文件名来指定
ConfigManager(const char* configfile = "liren.config")
:_configfile(configfile)
{
}
// 将ServerInfo结构体信息以二进制形式写入配置文件
void WriteBin(const ServerInfo& info)
{
// 以二进制写模式打开指定的配置文件
std::ofstream ofs(_configfile, std::ofstream::out | std::ofstream::binary);
if (ofs.is_open())
{
// 将整个ServerInfo结构体数据写入文件,这里强制转换为const char*类型以适配write函数的参数要求
ofs.write((const char*)&info, sizeof(ServerInfo));
}
else
{
// 如果文件打开失败,输出错误提示信息到标准错误输出
std::cerr << "无法打开文件" << std::endl;
}
// 关闭文件流,释放相关资源
ofs.close();
}
// 从配置文件中以二进制形式读取数据到ServerInfo结构体中
void ReadBin(const ServerInfo& info)
{
// 以二进制读模式打开配置文件
std::ifstream ifs(_configfile, std::ifstream::in | std::ifstream::binary);
if (ifs.is_open())
{
// 从文件中读取sizeof(ServerInfo)字节的数据到ServerInfo结构体对应的内存空间,这里强制转换为char*类型以适配read函数的参数要求
ifs.read((char*)&info, sizeof(ServerInfo));
}
else
{
// 文件打开失败时输出错误提示信息
std::cerr << "无法打开文件" << std::endl;
}
// 关闭文件流
ifs.close();
}
// 将ServerInfo结构体信息以文本形式写入配置文件(每行分别写IP和端口号)
void WriteText(const ServerInfo& info)
{
// 以文本模式打开配置文件(默认会覆盖原有内容)
std::ofstream ofs(_configfile);
if (ofs.is_open())
{
// 先写入IP地址,然后换行,再写入端口号,再换行
ofs << info._ip << std::endl << info._port << std::endl;
}
else
{
// 文件打开失败输出提示信息
std::cerr << "无法打开文件" << std::endl;
}
// 关闭文件流
ofs.close();
}
// 从配置文件中以文本形式读取数据到ServerInfo结构体中(按顺序读取IP和端口号)
void ReadText(ServerInfo& info)
{
// 以文本模式打开配置文件
std::ifstream ifs(_configfile);
if (ifs.is_open())
{
// 从文件中依次读取IP地址和端口号到ServerInfo结构体对应的成员变量中
ifs >> info._ip >> info._port;
}
else
{
// 文件打开失败输出错误提示
std::cerr << "无法打开文件" << std::endl;
}
// 关闭文件流
ifs.close();
}
private:
std::string _configfile;
};
int main()
{
// 创建ConfigManager类的实例,使用默认配置文件名
ConfigManager cfgMgr;
// 初始化一个ServerInfo结构体实例,设置IP和端口号
ServerInfo winfo = { "192.0.0.1", 80 };
ServerInfo rdinfo;
// 二进制读写操作
cfgMgr.WriteBin(winfo);
cfgMgr.ReadBin(rdinfo);
// 输出读取到的IP地址信息
std::cout << rdinfo._ip << std::endl;
// 输出读取到的端口号信息
std::cout << rdinfo._port << std::endl;
// 文本读写操作
cfgMgr.WriteText(winfo);
cfgMgr.ReadText(rdinfo);
// 输出读取到的IP地址信息
std::cout << rdinfo._ip << std::endl;
// 输出读取到的端口号信息
std::cout << rdinfo._port << std::endl;
return 0;
}
1.4 sstream
在 C++ 中,sstream
是一个非常实用的输入输出流相关的头文件,它提供了对字符串进行类似流操作的功能,主要涉及 stringstream
、istringstream
和 ostringstream
这几个类,以下为你详细介绍:
要使用 sstream
相关功能,需在代码中包含 <sstream>
头文件。
1.4.1 ostringstream
ostringstream
类的功能侧重于向一个字符串中写入数据,它可以将不同类型的数据按照指定的格式转换为字符串并存储起来,方便后续对生成的字符串进行统一的使用或处理,常用于动态生成字符串内容,比如根据程序运行中的各种变量值拼凑出特定格式的字符串等场景。
以下示例展示了如何使用 ostringstream
生成一个包含特定格式信息的字符串:
#include <iostream>
#include <sstream>
#include <string>
using namespace std;
int main() {
ostringstream oss;
int age = 25;
string name = "Alice";
oss << "姓名: " << name << ", 年龄: " << age;
string result = oss.str(); // 获取ostringstream对象中存储的字符串
cout << result << endl;
return 0;
}
在上述代码中:
- 先创建了
ostringstream
对象oss
。 - 接着通过流插入操作符
<<
将一个字符串"姓名: "
、变量name
的值、字符串", 年龄: "
和变量age
的值依次写入到oss
中,此时oss
内部就根据这些写入的内容拼凑出了一个符合特定格式的字符串。 - 最后通过调用
oss
的str()
方法获取存储在oss
中的字符串,并将其赋值给变量result
后输出显示在屏幕上。
1.4.2 istringstream
istringstream
类主要用于从字符串中提取数据,也就是将一个字符串当作输入流来解析其中的内容,按照特定的格式和数据类型规则读取相应的值,常被用于对已经存在的字符串进行信息提取和数据拆分等操作。
假设我们有一个包含多个用空格隔开的整数的字符串,想要把这些整数提取出来分别处理,示例代码如下:
#include <iostream>
#include <sstream>
#include <string>
using namespace std;
int main() {
string inputStr = "1 2 3 4 5";
istringstream iss(inputStr);
int num;
while (iss >> num) { // 从istringstream对象中不断提取整数,直到读完所有整数
cout << num << " ";
}
cout << endl;
return 0;
}
在这个示例中:
- 首先定义了一个包含多个整数(用空格隔开)的字符串
inputStr
。 - 然后创建了
istringstream
对象iss
,并将inputStr
作为参数传入,这样iss
就可以把这个字符串当作输入流来处理了。 - 通过
while
循环和流提取操作符>>
,不断从iss
中读取整数并输出显示,直到读完字符串中所有的整数。
1.4.3 stringstream
首先stringstream
类兼具输入和输出的功能,它可以基于一个字符串对象创建流,然后在这个流上进行数据的读取(类似从输入流提取数据)和写入(类似向输出流插入数据)操作,使得字符串能够像普通的输入输出流(如 iostream
中的标准输入输出流、fstream
中的文件输入输出流)一样被灵活操作。
以下是创建 stringstream
对象并进行简单操作的示例代码:
#include <iostream>
#include <sstream>
#include <string>
using namespace std;
int main() {
stringstream ss;
int num = 10;
double d = 3.14;
string str = "Hello, World!";
// 将不同类型的数据插入到stringstream对象中,相当于写入操作
ss << num << " " << d << " " << str;
// 从stringstream对象中提取数据,还原出原来插入的数据类型和值
int extractedNum;
double extractedD;
string extractedStr;
ss >> extractedNum >> extractedD >> extractedStr;
cout << "提取出的整数: " << extractedNum << endl;
cout << "提取出的小数: " << extractedD << endl;
cout << "提取出的字符串: " << extractedStr << endl;
return 0;
}
在上述代码中:
- 首先创建了一个
stringstream
对象ss
。 - 接着通过流插入操作符
<<
将一个整数num
、一个双精度浮点数d
和一个字符串str
依次写入到ss
中,此时这些不同类型的数据被组合到了同一个字符串流里,以特定的格式(这里是用空格隔开)暂存起来。 - 然后使用流提取操作符
>>
从ss
中依次读取数据,将读取到的值赋给相应的变量extractedNum
、extractedD
和extractedStr
。