15. 文件操作
一、为什么要使用文件
我们之前写的程序在运行起来的时候,我们可以给程序增加或删除数据,此时的数据都是存在内存中。当程序执行完毕退出的时候,之前程序中增减或减少的数据就不存在了,等程序下一个运行的时候,数据又会重新录入。
如果我们想把程序中的数据记录下来,只有在我们选择删除的时候,数据才不复存在。这就涉及到数据持久化的问题。我们一般数据持久化的方法是把数据放在磁盘文件、存放到数据等方式。因此,我们可以使用文件的方式将数据直接存放在电脑的硬盘上,做到了数据的持久化。
二、什么是文件
文件(file)通常是磁盘或固态硬盘上的一段已命名的存储区。它是指一组相关数据的有序集合。这个数据集合有一个名称,叫做文件名。文件名 是文件的唯一标识,以便用户识别和引用。文件名包括 3 个部分:文件路径 + 文件名主干 + 文件后缀名。所有的文件都通过流进行输入、输出操作。C++ 把文件看作一系列连续的字节,每个字节都能被单独读取。与文本流和二进制流对应,文本可以分为 文本文件 和 二进制文件。
- 文本文件,也称 ASCII 文件。这种文件在保留的时,每一字符对应一个字节,用于存放对应的 ASCII 码。
- 二进制文件,不保存 ASCII 码,而是按二进制的编码方式来保存文件内容。
文件在程序中是以流的形式来操作的。流 是指数据源(文件)和程序(内存)之间的经历的路径。输入流 指的是数据从数据源(文件)到程序(内存)的路径。输出流 指的是数据从程序(内存)到数据源(文件)的路径。
当我们提到输入时,这意味着要向程序写入一些数据。输入可以是以文件的形式或命令行中进行。C语言 提供了一系列内置的函数来读取给定的输入,并根据需要写入到程序中。
当我们提到输出时,这意味着要在屏幕上、打印机上或任意文件中显示一些数据。C++ 提供了一些列内置的函数来输出数据到计算机屏幕上和保存数据到文本文件或二进制文件中。
三、文件的基本操作
要写入文件,需要创建一个 ofstream 对象,并使用 ostream 方法,如 << 插入运算符或 write()。要读取文件,需要创建一个 ifstream 对象,并使用 istream 方法,如 >> 抽取运算符或 get()。C++ 在头文件 fstream 中定义了多个类,其中包括用于文件输入的 ifstream 类和用于文件输出的 ofstream 类。C++ 还定义了一个 fstream 类,用于同步文件 I/O。这些类都是从头文件 iostream 中的类派生而来的。
3.1、文本模式
文本模式描述的是文件将被如何使用:读、写、追加等。将流与文件关联时(无论是使用文件名初始化文件流对象,还是使用 open() 方法),都可以提供指定文件模式的第二个参数。
io_base 类定义了一个 openmode 类型,用于表示模式。我们可以选择 ios_base 类中定义的多个常量来指定模式。
常量 | 模式 |
---|---|
io_base::in | 为读文件而打开文件 |
io_base::out | 为写文件而打开文件 |
io_base::ate | 打开文件,并移到文件尾 |
io_base::app | 追加方式写文件 |
io_base::trunc | 如果文件存在先删除,再创建 |
io_base::binary | 以二进制方式打开文件 |
文件打开方式可以配合使用,利用 | 连接两个打开方式。
3.2、文本文件的操作
3.2.1、写入文本文件
写入文件步骤如下:
- 包含头文件 fstream。
- 创建一个 ofstream 对象来管理输出流。
- 将该对象的 open() 方法与特定的文件关联起来,
- 写入文件。
- 关闭文件。
#include <iostream>
// 1、包含头文件
#include <fstream>
using namespace std;
int main(void)
{
// 2、创建文件流对象
ofstream file;
// 3、指定打开方式
file.open("test.txt", ios::out);
// 4、写入数据
file << "姓名: Sakura" << endl;
file << "性别: 女" << endl;
file << "年龄: 10" << endl;
// 5、关闭文件
file.close();
}
3.2.2、读取文本文件
读取文件步骤如下:
- 包含头文件 fstream。
- 创建一个 ifstream 对象来管理输入流。
- 将该对象的 open() 方法与特定的文件关联起来,
- 读取文件。
- 关闭文件。
#include <iostream>
// 1、包含头文件
#include <fstream>
using namespace std;
int main(void)
{
// 2、创建文件流对象
ifstream file;
// 3、指定打开方式
file.open("test.txt", ios::in);
// 4、判断文件是否打开成功
if (!file.is_open())
{
cout << "文件打开失败" << endl;
return 0;
}
// 5、读取数据
cout << "第一种方式读取文件:" << endl;
char buffer[1024] = {0};
while (file >> buffer)
{
cout << buffer << endl;
}
// 清除文件流指针位置
file.clear();
// 文件指针移到文件开头
file.seekg(0, ios_base::beg);
cout << endl << "第二种方式读取文件:" << endl;
char buffer2[1024] = {0};
while (file.getline(buffer2, sizeof(buffer2)))
{
cout << buffer2 << endl;
}
// 清除文件流指针位置
file.clear();
// 文件指针移到文件开头
file.seekg(0);
cout << endl << "第三种方式读取文件:" << endl;
string buffer3;
while (getline(file, buffer3))
{
cout << buffer3 << endl;
}
// 清除文件流指针位置
file.clear();
// 文件指针移到文件开头
file.seekg(0);
cout << endl << "第四种方式读取文件:" << endl;
char ch;
while ((ch = file.get()) != EOF)
{
cout << ch;
}
cout << endl;
// 5、关闭文件
file.close();
}
3.3、二进制文件的操作
以二进制的方式对文件进行读写操作时,打开方式要指定为:io_base::binary
。
3.3.1、写入二进制文件
二进制方式写文件主要利用流对象调用成员函数 write()。函数原型如下:
ostream & write(const char * buffer, int lenght);
字符指针 buffer 指向内存中一段存储空间,length 是读写的字节数。
#include <iostream>
// 1、包含头文件
#include <fstream>
using namespace std;
struct Person
{
string name;
int age;
};
int main(void)
{
// 2、创建文件流对象,并指定打开方式
ofstream file("person.txt", ios::out | ios::binary);
// 3、写入数据
Person p = {"Sakura", 10};
file.write((char *)&p, sizeof(Person));
// 4、关闭文件
file.close();
}
3.3.2、读取二进制文件
二进制方式读取文件主要利用流对象调用 read(),函数原型如下:
istream & read(char * buffer, int length);
字符指针 buffer 指向内存中一段存储空间,length 是读写的字节数。
#include <iostream>
// 1、包含头文件
#include <fstream>
using namespace std;
struct Person
{
string name;
int age;
};
int main(void)
{
// 2、创建文件流对象,并指定打开方式
ifstream file("person.txt", ios::in | ios::binary);
// 3、判断文件是否打开成功
if (!file.is_open())
{
cout << "文件打开失败" << endl;
return 0;
}
// 4、读取数据
Person p;
file.read((char *)&p, sizeof(Person));
cout << "{姓名:" << p.name << ",年龄:" << p.age << "}" << endl;
// 5、关闭文件
file.close();
}
四、随机读取
随机读取指的是直接移动(不是依次移动)到文件的任何位置。fstream 类是从 iostream 派生而来的,而后者基于 istream 和 ostream 两个类。因此,它继承了它们的方法,并且它还继承了两个缓冲区,一个用于输入,一个用于输出,并能同步这两个缓冲区的处理。也就是说,当程序读写文件时,它将协调地移动输入缓冲区中的输入指针和输出缓冲区中的输出指针。
fstream 类继承了两个方法:seekg() 和 seekp()。前者将输入指针移到指定的文件位置,后者将输出指针移到指定的文件位置。
istream & seekg(streamoff, ios_base::seekdir);
istream & seekg(streampos);
第一个原型定位到离第二个参数指定的文件特定距离(单位为字节)的位置。第二个原型定位到离文件开头特定距离(单位为字节)的距离。
streamoff 值被用来度量相对于文件特定位置的偏移量(单位为字节)。streamoff 参数表示相对于三个位置之一的偏移量为特定值的文件位置(类型可以定位为整形或类)。seek_dir 参数时 ios_base 类中定义的另一种整形,有 3 个可能的值。
常量值 | 位置 |
---|---|
io_base::beg | 相对于文件开始出的偏移量 |
io_base::cur | 相对于当前位置的偏移量 |
io_base::end | 当对于文件尾步的偏移量 |
streampos 类型的值定位到文件中的一个位置。它可以是类,但如果是这样的话,这个类包含一个接受 streamoff 参数的构造函数和一个接受整形参数的构造函数,以便将两种类型转换为 streampos 值。streampos 值表示文件中的绝对位置(从文件开始处算起)。可以将 streamoff 位置看作相对于文件开始处的位置。
如果要检查文件指针的当前位置,则对于输入流,可以使用 tellg() 方法,对于输出流,可以使用 tellp() 方法。它们都返回一个表示当前位置的 streamoff 值(以字节为单位,从文件开始处算起)。创建 fstream 对象时,输入指针和输出指针一前一后地移动,因此 tellg() 和 tellp() 返回的值相同。然而,如果使用 istream 对象来管理输入流,而使用 ostream 对象管理同一个文件的输出流,则输入指针和输出指针将彼此独立地移动,因此 tellg() 和 tellp() 将返回不同的值。
如果移动文件指针不生效,我们可以先使用 clear() 方法重置流状态。