字符串文件是C++经常处理的一种文件类型,其依赖为i/ostream和fstream类。
我们看一个向文件中写入内容的例子:
#include
#include
#include
#include
using std::string;
using namespace std;
int main()
{
string filename="test.txt"; // 文件名
ofstream mytest; // 使用ofstream创建文件输出流对象,名字自拟
mytest.open(filename); // 打开文件
// 向文件中写入数据
mytest<<"这是一个测试\n"<<"我们尝试连续输入内容\n";
mytest<<"继续测试";
// 关闭文件
mytest.close();
}
在这段代码中,filename定义了一个文件名,我们尝试使用文件流打开文件,当当前路径下存在文件时,这段代码会截断文件,即打开文件后删除文件内容,重新进行写入操作,当文件不存在时,会在当前路径下创建文件并写入内容。运行这段代码会在.cpp文件路径下产生一个.txt文件:
虽然操作成功了,但是这样的做法存在不足和风险,比如我们在打开或创建文件时应当给出文件的确切位置,不应该只让它生成在.cpp文件的路径下;在使用文件之前应当判断文件是否成功打开。
为了解决以上不足,我们先详细了解一下ofstream输出流。ofstream创建了文件输出流对象,而open函数可以带有两个参数,即文件及路径和打开方式。文件的绝对路径有以下写法:
R"(C:\data…\test.txt)" (C++11标准)
“C:\data\…\text.txt”
“C:/data/…/text.txt”
其次,文件的打开方式可以这样规定:
ios::out 截断文件内容打开文件,为参数默认值
ios::truck 截断文件内容,类似于ios::out
ios::app 在文件后添加内容
我们再来测试一下:
int main()
{
ofstream mytest;
mytest.open("E:\\c++\\class3start11\\test.txt",ios::app);
// 使用ios::app 在文件后添加内容
// 向文件中写入数据
mytest<<"这是一个测试\n"<<"我们尝试连续输入内容\n";
// 关闭文件
mytest.close();
}
再打开文件其内容就变成了:
此外,文件也不是一定能打开文件的情况,通常情况下无法正常打开文件的情况有:
1.目录不存在
2.磁盘空间已满
3.没有权限(linux系统下常见)
应对这种情况,我们可以使用mytest.is_open()来判断文件是否成功打开:
int main()
{
ofstream mytest("test.txt",ios::app); // 在创建文件输出流对象时直接提供路径和打开方式参数,
// 不需要再使用open进行打开
if(mytest.is_open()) // 判断文件是否成功打开
{
mytest<<"这是一个测试\n"<<"我们尝试连续输入内容\n";
// 关闭文件
mytest.close();
return 0;
}
else
{
cout<<"打开文件失败"<<endl;
return 0;
}
}
作为演示内容,我们就不写完整路径了。
读取文本文件内容需要用ifstream输出流进行操作,其参数也是两个,文件名和打开方式。打开方式只能使用使用in:
int main()
{
// 读取文件参数只有iOS::in
ifstream mytest("test.txt",ios::in); // 用输入流打开文件,读取文件内容
if(mytest.is_open())
{
cout<<"打开文件成功"<<endl;
return 0;
}
}
而后就要读取文件内容了。读取文件内容使用函数getline。getline函数有两个参数,即输出流对象和用于存储读取信息的变量。getline会按行读取文件内容,并返回是否读取成功。它很多重载,但常用的主要有两种方式:
if(mytest.is_open())
{
string out; // 定义字符串类型out用于接收文件内容
while(getline(mytest,out)) // 将当前行内容存入out,当前行没有内容时返回值为false
{
cout<<out<<endl;
}
// 关闭文件
mytest.close();
}
else
{
cout<<"打开文件失败!"<<endl;
}
这样就可以在终端看到执行效果:
当然,这种用法还有一种比较简单的变种:
int main()
{
// 读取文件参数只有iOS::in
ifstream mytest("test.txt",ios::in);
// 打开文件失败的原因:1.文件不存在2.目录不存在3.没有权限
if(mytest.is_open())
{
string out;
while(mytest>>out)
{
cout<<out<<endl;
}
// 关闭文件
mytest.close();
}
else
{
cout<<"打开文件失败"<<endl;
return 0;
}
}
这种方式使用起来更简单,我们记住就好。
另外,使用C风格字符串也可以接收文件内容,需要用到另一个getline函数的重载:
int main()
{
ifstream mytest;
mytest.open("test.txt",ios::in)
if(mytest.is_open())
{
char out[21]; // 三个字符存储一个汉字
// 读取文件一般要用getline函数一行一行读
while(mytest.getline(out,20)) // getline的一个重载,第一个参数为记录文件内容的字符串名,第二个参数是这个字符串最大下标
// 如果字符数组不能够存下某行数据,会在对应处直接终止运行退出程序。
{
cout<<out<<endl;
}
}
else
{
cout<<"打开文件失败"<<endl;
return 0;
}
// 关闭文件
mytest.close();
}
这个例子中,我们使用的C类型字符串最多可以存储的汉字只有7个,当遇到一行超过7个汉字的情况,会立刻终止运行,但不会报错。如果我们扩大数组的大小(由于换行符的存在,这里至少需要设置成31)就可以完整读取了。但实际中如果遇到这种情况并不好处理,在不知道文本内容的时候,设置足够大的字符数组较难做到,况且如果字符数组设置过大,又会浪费空间,因此这种用法并不推荐。
文本文件的读写方便实用,其特点为每个字符都有实际意义,但只能用于处理字符串类型。在实际使用中,很多时候我们需要存取更复杂的类型,比如结构体、整形、图片等,这些内容用字符串处理并不方便。计算机的世界里,只有0和1两个数字,任何数据本质上都是由二进制数描述的,因此如果我们使用二进制对文件进行存取,理论上就可以处理所有文件和数据。C++也为我们提供了这样的存取方法。
使用二进制处理文件首先也是要用ofstream创建输出流对象,打开文件时也要额外说明使用二进制方式打开。写入的过程需要使用write函数,具体使用方法示例如下:
// 定义一个需要记录数据的基本结构,也可以使用自定义类或C++基本数据类型
struct Student
{
char name[32];
int age;
char sex;
};
int main()
{
ofstream mytest("test.doc",ios::binary);
// 需要再打开方式后面加上binary用于告诉计算机使用二进制方式打开
// 这里的ios::out可以省略
if(mytest.is_open())
{
Student child={"ZhangSan",15,'m'}; //
// 写入文件需要使用write函数
mytest.write((const char*)&child,sizeof(Student));
child=Student{"LiSi",20,'m'}; // 重载child,因为child变量之前已经赋值,因此不能直接初始化
mytest.write((const char*)&child,sizeof(Student)); // 参数为需写入内容的地址(C++要求使用const char*),需写入内容的大小
}
else
{
cout<<"打开文件失败"<<endl;
return 0;
}
// 关闭文件
mytest.close();
}
这样我们就可以得到一个doc文件了:
但是它的内容是乱码的,这是因为二进制存储文件是按字节有效的,如果我们不知道存储的方式,是不能够正确解码的。
当我们知道数据的具体存储方式时,就可以编写程序查看存储的实际内容了。上例中,我们知道test.doc文件存储的数据是Student结构体,也就可以对它进行解码并查看内容了:
int main()
{
ifstream mytest("test.doc",ios::in|ios::binary); // 读取二进制文件
if(mytest.is_open())
{
// 二进制文件打开后需要用正确的接收格式接收
Student child;
// 二进制文件以数据类型的方式组织数据,没有换行的说法
// mytest.read((char*)&ZhangSan,sizeof(Student)) 一次只能读取一个结构体数据
while(mytest.read((char*)&child,sizeof(Student)))
{
cout<<child.name<<" "<<child.age<<" "<<child.sex<<endl;
}
}
else
{
cout<<"打开文件失败"<<endl;
return 0;
}
// 关闭文件
mytest.close();
}
// 输出为:ZhangSan 15 m
// LiSi 20 m
这里需要注意几点问题:
1.由于C++中string类型内存布局具有不确定性,因此可能会影响二进制文件的读取,建议使用字符数组存储字符串;
2.一个结构体变量只能初始化一次,想要修改变量内容可以使用赋值或重载的方式;
3.C++处理文本文件时以换行符作为读取内容分隔符,getline函数也是在读取到第一个换行符时返回。二进制文件中换行符只被视为二进制数据,文件读取时以数据块作为分隔。
这节主要介绍了C++处理文本文件和二进制文件的部分方法,由于篇幅原因,我们只提到了较为常见的函数和用法。后面的学习中我会继续对文件操作进行补充。