目录
文件输入输出流(重点)
文件输入流
文件输入流对象的创建
对测试代码进行解读:
1. 代码核心逻辑
2. 读取过程详解
3. 关键特性总结
4. 注意事项
5. 完整流程示例
这里既然提到了 >> 流,那么就对他进行进一步的认识
1. >> 操作符的默认行为
2. 对“只能提取一次”的误解
3. 循环读取与换行符处理
4. 常见误区与解决方案
5. 总结
对流的解读中有提到了只要流开启可以继续 >> 提取,那么什么时候流关闭呢?
流(Stream)关闭的时机详解
1. 核心误区澄清
2. 流的生命周期
3. 代码示例解析
4. 换行符的作用
5. 需要手动关闭流的场景
6. 常见问题解答
7. 总结
按行读取
读取指定字节数的内容
文件输出流
字符串输入输出流
字符串输入流
字符串输出流
文件输入输出流(重点)
所谓“文件”,一般指存储在外部介质上数据的集合。一批数据是以文件的形式存放在外部介质上的。操作系统是以文件为单位对数据进行管理的。要向外部介质上存储数据也必须先建立一个文件(以文件名标识),才能向它输出数据。外存文件包括磁盘文件、光盘文件和U盘文件。目前使用最广泛的是磁盘文件。
文件流是以外存文件为输入输出对象的数据流。
文件输入流是从外存文件流向内存的数据,文件输出流是从内存流向外存文件的数据。每一个文件流都有一个内存缓冲区与之对应。文件流本身不是文件,而只是以文件为输入输出对象的流。若要对磁盘文件输入输出,就必须通过文件流来实现。
C++ 对文件进行操作的流类型有三个:
ifstream(文件输入流)
ofstream(文件输出流)
fstream (文件输入输出流)
他们的构造函数形式都很类似:
ifstream();
explicit ifstream(const char* filename, openmode mode = ios_base::in);
explicit ifstream(const string & filename, openmode mode = ios_base::in);
ofstream();
explicit ofstream(const char* filename, openmode mode = ios_base::out);
explicit ofstream(const string & filename, openmode mode = ios_base::out);
fstream();
explicit fstream(const char* filename, openmode mode = ios_base::in|out);
explicit fstream(const string & filename, openmode mode = ios_base::in|out);
补充:explicit关键字的意义 —— 禁止隐式转换
上面这些函数都有默认值,不希望使用一个文件名就可以创建一个对象。
隐式类型转换是编译器读到,1时看到1正好可以使用这个1来创建一个临时的point对象,然后调用拷贝构造创建对象pt6。
文件输入流
文件输入流对象的创建
首先我们要明确使用文件输入流的信息传输方向:文件 --》 文件输入流对象的缓冲区 --》通过输入流运算符-》 程序中的数据
根据上述的说明,我们可以将输入流对象的创建分为两类:
-
可以使用无参构造创建ifstream对象,再使用open函数将这个文件输入流对象与文件绑定(若文件不存在,则文件输入流进入failbit状态);
-
也可以使用有参构造创建ifstream对象,在创建时就将流对象与文件绑定(不是直接输入到缓冲区而是等待使用时可以调用),后续操作这个流对象就可以对文件进行相应操作。
通过参考文档中对ifstream的构造函数的描述,文件输入流对象的有参构造需要输入文件名,可以指定打开模式(不指定则使用in模式,为读打开)
#include <fstream>
void test0(){
ifstream ifs;
ifs.open("test1.cc");
ifstream ifs2("test2.cc");
string filename = "test3.cc";
ifstream ifs3(filename);
}
下面为测试代码可以自行测试
#include <fstream>
#include <iostream>
#include <string>
using std::cin;
using std::cout;
using std::endl;
using std::string;
using std::ifstream;
void test0()
{
ifstream ifs;
//文件输入流对象是从文件中读取内容
//需要绑定到一个存在的文件
// ifs.open("cout.cpp");
ifs.open("cout.cc");
if(!ifs.good())
{
cout << "open file is fail!" << endl;
return;
}
//默认以换行符,空格作为间隔,这里注意只是跳过,还会留在缓冲区
//一次读取一个字符串
string word;
//只要ifs是goodbit状态就会循环
//等价于while(ifs.good),会进行隐式布尔类型转换,并检查
while(ifs >> word)
{
cout << word << endl;
}
//规范操作,使用完之后关闭流
ifs.close();
}
int main()
{
test0();
return 0;
}
对测试代码进行解读:
以下是针对图片中代码的读取机制解析,结合 C++ 输入流的特性:
1. 代码核心逻辑
图片中的代码片段:
string word;
while (ifs >> word) { // 关键点:输入流操作
cout << word << endl;
}
功能:从输入流 ifs
中逐个读取以 空格或换行符分隔 的字符串,并输出每个字符串。
2. 读取过程详解
(1) 默认分隔规则
-
分隔符:
operator>>
在读取字符串时默认以 空白字符(空格、换行符、制表符等) 作为分隔符。 -
行为:每次读取会 跳过前导空白字符,直到遇到非空白字符开始读取,再遇到空白字符时停止。
(2) 单次读取步骤
ifs >> word
-
检查流状态:若
ifs
处于good()
状态(无错误、未到文件尾),继续读取。 -
跳过空白:忽略当前位置开始的连续空格/换行符。
-
读取内容:将后续非空白字符存入
word
,直到遇到下一个空白字符或文件尾。 -
更新流状态:若读取成功,
ifs
保持good()
;若失败(如文件尾),ifs
置为eof()
或fail()
。
(3) 循环条件 while (ifs >> word)
-
本质:等价于
while (ifs.good())
但更安全。 -
执行流程:
-
执行
ifs >> word
(尝试读取)。 -
检查流的隐式布尔转换结果:
-
若读取成功(流有效):返回
true
,进入循环体。 -
若读取失败(如文件尾或错误):返回
false
,退出循环。
-
-
3. 关键特性总结
特性 | 说明 |
---|---|
自动跳过空白 | 无需手动处理空格/换行符,流操作符自动跳过。 |
流状态驱动循环 | 循环条件隐式检查 ifs 的状态,避免死循环。 |
按需读取 | 每次循环读取一个逻辑单元(以空白分隔的字符串)。 |
错误处理内嵌 | 若流进入错误状态(如类型不匹配),循环自动终止。 |
4. 注意事项
(1) 文件尾与错误状态
-
文件尾(EOF):当读取到文件末尾时,
ifs.eof()
为true
,循环终止。 -
错误状态:若输入内容与目标类型不匹配(如试图将字母读入
int
),ifs.fail()
为true
,循环也会终止。
(2) 关闭流的正确方式
-
自动释放:通常不需要手动调用
ifs.close()
,输入流对象会在析构时自动关闭。 -
手动关闭场景:若需要提前释放资源(如重新打开文件),可显式调用
ifs.close()
。
5. 完整流程示例
假设输入文件内容为:
Hello World
C++ Input
执行过程:
-
第一次循环:读取
"Hello"
,输出Hello
。 -
第二次循环:跳过空格,读取
"World"
,输出World
。 -
第三次循环:跳过换行符,读取
"C++"
,输出C++
。 -
第四次循环:跳过空格,读取
"Input"
,输出Input
。 -
第五次读取:遇到文件尾,
ifs >> word
失败,循环终止。
这里既然提到了 >> 流,那么就对他进行进一步的认识
1. >>
操作符的默认行为
在 C++ 中,流提取符 >>
默认以空白字符(空格、换行符 \n
、制表符 \t
等)作为分隔符。其工作流程如下:
-
跳过前导空白:自动忽略输入流中起始位置的所有空白字符。
-
读取非空白内容:从第一个非空白字符开始读取,直到遇到下一个空白字符或流结束。
-
终止条件:遇到空白字符或流结束时,停止本次提取。
示例代码:
int num;
string str;
// 输入 " 42\nHello"(含前导空格和换行符)
cin >> num; // 读取 42(跳过前导空格,换行符终止读取)
cin >> str; // 读取 "Hello"(跳过换行符,继续读取)
2. 对“只能提取一次”的误解
用户的疑问可能源于以下观察: 当输入流中存在换行符时,cin >>
似乎只能提取一次数据。 实际原因:
-
换行符作为分隔符:
>>
遇到换行符时会终止当前读取,但流状态仍有效。 -
后续读取不受限:只要流未关闭或未到文件尾,可以继续使用
>>
提取后续数据。
示例:
int a, b;
// 输入 "10\n20"
cin >> a; // 读取 10(换行符终止本次读取,但流仍有效)
cin >> b; // 读取 20(跳过换行符,继续读取)
cout << a + b; // 输出 30
3. 循环读取与换行符处理
若需连续读取多行数据,可通过循环实现。换行符不会导致流终止,只是分隔数据单元。
示例:逐词读取多行文本:
string word;
while (cin >> word) { // 每次读取一个以空白分隔的字符串
cout << "Read: " << word << endl;
}
输入:
Hello World
C++ Input
输出:
Read: Hello
Read: World
Read: C++
Read: Input
4. 常见误区与解决方案
误区场景 | 原因分析 | 解决方案 |
---|---|---|
读取含空格的字符串时被截断 | >> 默认以空格为分隔符 | 使用 getline(cin, str) 按行读取 |
换行符导致后续读取失败 | 未正确处理流状态或缓冲区 | 结合 cin.ignore() 清除残留字符 |
混合使用 >> 和 getline 时出错 | >> 残留换行符影响 getline | 在 getline 前调用 cin.ignore() |
示例:安全混合使用 >>
和 getline
:
int id;
string name;
cin >> id; // 读取整数(如输入 123)
cin.ignore(100, '\n'); // 清除残留的换行符
getline(cin, name); // 读取完整一行(如输入 "Alice Smith")
5. 总结
-
>>
的终止条件:遇到空白字符(包括换行符)时停止当前读取,但流仍可继续使用。 -
循环读取可行性:只要流状态有效,可多次调用
>>
提取数据。 -
换行符处理:需注意残留换行符对后续操作的影响,必要时使用
ignore()
清理。
对流的解读中有提到了只要流开启可以继续 >> 提取,那么什么时候流关闭呢?
流(Stream)关闭的时机详解
根据图片中的代码和描述,您对流关闭的机制存在误解。以下是针对问题的详细解答:
1. 核心误区澄清
您提到的 “流在遇到换行符时会断掉” 和 “流提取符只能提取一次” 是对流操作的两个常见误解:
-
误解1:换行符会导致流关闭。 事实:换行符(
\n
)仅是 数据分隔符,它只会终止当前读取操作,但 不会关闭流。 -
误解2:流提取符 (
>>
) 只能提取一次数据。 事实:只要流状态正常(未到文件尾或发生错误),可以 多次调用>>
提取数据。
2. 流的生命周期
流的关闭(释放资源)与读取操作的终止是两个独立的概念:
(1) 流的关闭时机
-
自动关闭:当流对象(如
ifstream
、istringstream
)离开作用域时,析构函数会自动调用close()
方法释放资源。 -
手动关闭:可以显式调用
stream.close()
提前关闭流(例如需要重新打开另一个文件时)。
示例:
{
ifstream ifs("data.txt"); // 打开流
// 读取操作...
} // 作用域结束,ifs 析构时自动调用 close()
(2) 读取操作的终止
-
当
>>
遇到分隔符(如换行符、空格)时,当前读取操作终止,但流仍保持打开状态。 -
流仅在以下情况彻底关闭或失效:
-
显式调用
close()
。 -
流对象被销毁(如离开作用域)。
-
流到达文件尾(
eof()
为true
)。
-
3. 代码示例解析
假设图片中的代码为:
ifstream ifs("input.txt");
string word;
while (ifs >> word) { // 每次读取一个以空白分隔的字符串
cout << word << endl;
}
执行过程:
-
流保持打开:循环中每次
ifs >> word
读取一个字符串(跳过换行符、空格),但流始终未关闭。 -
终止条件:当文件内容全部读取完毕(到达文件尾),
ifs >> word
会失败,循环退出。 -
流关闭:
ifs
在程序块结束时自动关闭(析构时调用close()
)。
4. 换行符的作用
-
仅作为分隔符:换行符用于分割数据单元(如
"Hello\nWorld"
会被拆分为"Hello"
和"World"
)。 -
不影响流状态:换行符不会导致流关闭,甚至不会影响后续读取操作。
示例:
int a, b;
// 输入:"10\n20"
cin >> a; // 读取 10(换行符终止本次读取,但流仍有效)
cin >> b; // 读取 20(跳过换行符)
cout << a + b; // 输出 30
5. 需要手动关闭流的场景
场景 | 操作 |
---|---|
需要重新绑定到其他文件 | 先 close() ,再 open() |
确保资源立即释放(如写入文件) | 显式调用 close() |
示例:
ifstream ifs("data1.txt");
// 读取 data1.txt...
ifs.close(); // 手动关闭
ifs.open("data2.txt"); // 重新打开另一个文件
6. 常见问题解答
Q1:为什么有时读取后流无法继续使用?
-
可能原因:
-
流到达文件尾(
eof()
为true
)。 -
流进入错误状态(如尝试读取非数字到
int
变量)。
-
-
解决方法:检查流状态并重置:
if (ifs.fail()) { ifs.clear(); // 清除错误状态 ifs.ignore(); // 跳过无效输入 }
Q2:换行符残留导致的问题如何解决?
-
场景:混合使用
>>
和getline
时,>>
可能残留换行符影响getline
。 -
解决方案:在
getline
前清除缓冲区:int num; string line; cin >> num; cin.ignore(numeric_limits<streamsize>::max(), '\n'); // 清除残留换行符 getline(cin, line);
7. 总结
关键点 | 说明 |
---|---|
换行符是分隔符,不关闭流 | 仅终止当前读取操作,流仍可继续使用。 |
流关闭的时机 | 自动(析构时)或手动调用 close() 。 |
循环读取的可行性 | 只要流状态正常,可多次调用 >> 提取数据。 |
通过这一机制,流可以高效处理复杂输入场景,同时避免资源泄漏。如果需要按行处理数据,建议使用 getline
替代 >>
。
-
文件模式
根据不同的情况,对文件的读写操作,可以采用不同的文件打开模式。文件模式在 GNU GCC7.4 源码实现中,是用一个叫做 openmode 的枚举类型定义的,它位于 ios_base 类中。文件模式一共有六种,它们分别是:
in : 输入,文件将允许做读操作;如果文件不存在,打开失败
out : 输出,文件将允许做写操作;如果文件不存在,则直接创建一个
app : 追加,写入将始终发生在文件的末尾
ate : 末尾,写入最初在文件的末尾
trunc : 截断,如果打开的文件存在,其内容将被丢弃,其大小被截断为零
binary : 二进制,读取或写入文件的数据为二进制形式
按行读取
遇到换行符就会断开。
方法一:使用ifstream类中的成员函数getline,这种方式是兼容C的写法
ifstream ifs("test.cc");
//方法一,兼容C的写法,使用较少
char buff[100] = {0};
while(ifs.getline(buff,sizeof(buff))){
cout << buff << endl;
memset(buff,0,sizeof(buff));
}
准备好一片空间存放一行的内容,但是有一个弊端就是我们并不知道一行的内容会有多少个字符,如果超过了设置的字符长度将无法完成该行的读取,也将跳出循环。
方法二:
使用<string>提供的getline方法,工作中更常用
传入输入流对象、string、分隔符(默认换行符为分隔符)
//更方便,使用更多
string line;
while(getline(ifs,line)){
cout << line << endl;
}
将一行的内容交给一个string对象去存储,不用再关心字符数了。
下面为测试代码,可以自行测试
#include <string.h>
#include <fstream>
#include <iostream>
#include <string>
using std::cin;
using std::cout;
using std::endl;
using std::ifstream;
using std::string;
void test0()
{
ifstream ifs;
// 文件输入流对象是从文件中读取内容
// 需要绑定到一个存在的文件
// ifs.open("cout.cpp");
ifs.open("cout.cc");
if (!ifs.good())
{
cout << "open file is fail!" << endl;
return;
}
// 默认以换行符,空格作为间隔
// 一次读取一个字符串
string word;
// 只要ifs是goodbit状态就会循环
while (ifs >> word)
{
cout << word << endl;
}
// 规范操作,使用完之后关闭流
ifs.close();
}
void test1()
{
ifstream ifs;
// 文件输入流对象是从文件中读取内容
// 需要绑定到一个存在的文件
// ifs.open("cout.cpp");
ifs.open("cout.cc");
if (!ifs.good())
{
cout << "open file is fail!" << endl;
return;
}
// 方法一:兼容c的写法,使用较少
// 建立一个数组,将数组初始化为0
// char buff[100] = {0};
// while(ifs.getline(buff,sizeof(buff)))
// {
// cout << buff << endl;
// memset(buff,0,sizeof(buff));
// }
// 方法2
string str;
while (std::getline(ifs, str))
{
cout << str << endl;
}
// 规范操作,使用完之后关闭流
ifs.close();
}
int main()
{
// test0();
test1();
return 0;
}
读取指定字节数的内容
read函数 + seekg函数 + tellg函数
通过文件输入流对象读取到的内容交给字符数组,同时需要传入要读取的字符数(读取字符存到字符数组中)
要知道字符数就需要用上tellg函数了,可以这样理解,从文件中读取内容时存在一个文件游标,读取是从文件游标的位置开始读取的。tellg就是用来获取游标位置的,而seekg则是用来设置游标位置的。
调用seekg时有两种方式,一种是绝对位置(比如将游标设为流的开始位置,可以直接传参数0);一种是相对位置,传入偏移量和基准点——第一个参数:相对基准点需要向前偏移则传入负数,不偏移则传入0,需要向后偏移则传入正数。第二个参数格式为std::ios::beg(以流的开始位置为例)
如图示:
例子:读取一个文件的全部内容
void test0(){
string filename = "test.cc";
ifstream ifs(filename);
if(!ifs){
cerr << "ifs open file fail!";
return;
}
//读取一个文件的所有内容先要获取文件的大小
//将游标放到了文件的最后(尾后)流的结尾尾后
ifs.seekg(0,std::ios::end);
long length = ifs.tellg();//获取尾后下标,实际就是总的字符数
cout << length << endl;
char * pdata = new char[length]();
//需要将游标再放置到文件开头
ifs.seekg(0,std::ios::beg);
ifs.read(pdata,length);
//content包含了文件的所有内容,包括空格、换行
string content(pdata);
cout << "content:" << content << endl;
/* cout << pdata << endl; */
ifs.close();
}
还可以在创建输入流对象时指定ate模式(打开后会自动寻位到结尾所以打印出来游标就是结尾的位置),省去第一步将游标置流末尾处的操作。
下面为测试代码,可以自行测试
#include <string.h>
#include <fstream>
#include <iostream>
#include <string>
#include <unistd.h>
using std::cin;
using std::cout;
using std::endl;
using std::ifstream;
using std::string;
void test0()
{
string filename = "cout.cc";
ifstream ifs(filename, std::ios::ate);
if (!ifs)
{
std::cerr << "ifs open file fail!";
return;
}
cout << ifs.tellg()<<endl;
// 读取一个文件的所有内容先要获取文件的大小
// 将游标放到了文件的最后(尾后)流的结尾尾后
// ifs.seekg(0, std::ios::end);
long length = ifs.tellg(); // 获取尾后下标,实际就是总的字符数
cout << length << endl;
char *pdata = new char[length]();
// 需要将游标再放置到文件开头
ifs.seekg(0, std::ios::beg);
ifs.read(pdata, length);
// content包含了文件的所有内容,包括空格、换行
string content(pdata);
cout << "content:" << content << endl;
/* cout << pdata << endl; */
ifs.close();
}
int main()
{
test0();
return 0;
}
文件输出流
文件输出流的作用是将流对象保存的内容传输给文件
ofstream对象的创建与ifstream对象的创建类似
#include <fstream>
void test0(){
ofstream ofs;
ofs.open("test1.cc");
ofstream ofs2("test2.cc");
string filename = "test3.cc";
ofstream ofs3(filename);
}
推测一下,如果文件输出流对象绑定的文件不存在,可以吗?
—— 可以,如果文件不存在,就创建出来
-
通过输出流运算符写内容
ofstream对象绑定文件后,可以往该文件中写入内容
string filename = "test3.cc";
ofstream ofs3(filename);
string line("hello,world!\n");
ofs << line;
ofs.close();
内容传输的过程是string中的内容传给ofs对象,再传给这个对象绑定的文件。
但是我们会发现进行多次写入,并没有保留下多次的内容,因为这种创建方式会使打开模式默认为std::ios::out,每次都会清空文件的内容。
为了实现在文件流结尾追加写入内容的效果,可以在创建流对象时指定打开模式为std::ios::app(追加模式)
string filename = "test3.cc";
ofstream ofs3(filename,std::ios::app);//不能使用无参构造,不能将参数传给对象,所以使用有参构造
-
通过write函数写内容
除了使用输出流运算符<< 将内容传输给文件输出流对象(传给ofstream对象就是将内容传到其绑定的文件中),还可以使用write函数进行传输
char buff[100] = "hello,world!";
ofs.write(buff,strlen(buff));
const char* a = "HEllo!\n";
ofs,write(a,sizeof(a));
strig str("summer\n");
ofs.write(str.c_str(),str.size());
-
动态查看指令
为了更方便地查看多次写入的效果(动态查看文件的内容)可以使用指令
tail 文件名 -F //动态查看文件内容
ctrl + c //退出查看
字符串输入输出流
字符串I/O是内存中的字符串对象与字符串输入输出流对象之间做内容传输的数据流,通常用来做格式转换。
C++ 对字符串进行操作的流类型有三个:
istringstream (字符串输入流)
ostringstream (字符串输出流)
stringstream (字符串输入输出流)
它们的构造函数形式都很类似:
istringstream(): istringstream(ios_base::in) { }
explicit istringstream(openmode mode = ios_base::in);
explicit istringstream(const string& str, openmode mode = ios_base::in);
ostringstream(): ostringstream(ios_base::out) { }
explicit ostringstream(openmode mode = ios_base::out);
explicit ostringstream(const string& str, openmode mode = ios_base::out);
stringstream(): stringstream(in|out) { }
explicit stringstream(openmode mode = ios_base::in|ios_base::out);
explicit stringstream(const string& str, openmode mode = ios_base::in|ios_base::out);
字符串输入流
将字符串的内容传输给字符串输入流对象,再通过这个对象进行字符串的处理(解析)
创建字符串输入流对象时传入c++字符串,字符串的内容就被保存在了输入流对象的缓冲区中。之后可以通过输入流运算符将字符串内容输出给不同的变量,起到了字符串分隔的作用。
——如下,将字符串s的内容传给了两个int型数据
void test0(){
string s("123 456");
int num = 0;
int num2 = 0;
//将字符串内容传递给了字符串输入流对象
istringstream iss(s);
iss >> num >> num2;
cout << "num:" << num << endl;
cout << "num2:" << num2 << endl;
}
因为输入流运算符会默认以空格符作为分隔符,字符串123 456中含有一个空格符,那么传输时会将空格前的123传给num,空格后的456传给num2,因为num和num2是int型数据,所以编译器会以int型数据来理解缓冲区释出的内容,将num和num2赋值为123和456
字符串输入流通常用来处理字符串内容,比如读取配置文件
//myserver.conf
ip 192.168.0.0
port 8888
dir ~HaiBao/53th/day06
//readConf.cc
void readConfig(const string & filename){
ifstream ifs(filename);
if(!ifs.good()){
cout << "open file fail!" << endl;
return;
}
string line;
string key, value;
while(getline(ifs,line)){
istringstream iss(line);
iss >> key >> value;
cout << key << " -----> " << value << endl;
}
}
void test0(){
readConfig("myserver.conf");
}
字符串输出流
通常的用途就是将各种类型的数据转换成字符串类型
void test0(){
int num = 123, num2 = 456;
ostringstream oss;
//把所有的内容都传给了字符串输出流对象
oss << "num = " << num << " , num2 = " << num2 << endl;
cout << oss.str() << endl;
}
将字符串、int型数据、字符串、int型数据统统传给了字符串输出流对象,存在其缓冲区中,利用它的str函数,全部转为string类型并完成拼接。