在实际应用中,常以磁盘文件作为对象,即能从磁盘文件读取数据,也能将数据输出到磁盘文件,磁盘是计算机的外部存储器,能够长期保留信息,能读能写,可以刷新重写等等。
在C++中,文件操作通常通过文件流(file streams)来完成,这是<fstream>库提供的功能。fstream库中的三个主要类用于文件操作:ifstream(用于输入文件),ofstream(用于输出文件)和fstream(用于双向文件操作)。
一、文件的概念
常用的文件有两大类:一类是程序文件(program file),如C++的源程序文件(.cpp)、目标文件(.obj)、可执行文件(.exe)等。一类是数据文件(data fle)在程序运行时,常常需要将一些数据输出到磁盘上存放起来,后期需要时再从磁盘中输入到计算内存,这种磁盘文件就是数据文件。
文件数据的组织形式,可分为ASCII文件和二进制文件。ASCII文件又称文本(text)文件或字符文件,它的每一个字节放一个ASCII码,代表一个字符。二进制文件又称为内部格式文件或字节文件,是把内存中的数据按其在内存中的存储形式原样输出到磁盘上存放。
C++提供了低的I/O功能和高级I/O功能。高级的I/O功能是把若干个字节组合为一个有意义的单位(如整数、单精度数、双精度数、字符串或用户自定义的类型的数据),然后以ASCII码字符形式输入和输出(但在传输大容量的文件时由于数据格式转换,速度较慢,效率不高)。低级的I/O功能是以字节为单位输入和输出的,在输入和输出时不进行数据格式的转换,以二进制形式进行的(这种输入输出速度快、效率高、但会感到不方便)。
二、文件流类与文件流对象
文件流是以外存文件为输入输出对象的数据流,输出文件流是从内存流向外存文件的数据,输入文件是从外存文件流向内存的数据,每一个文件流都有一个内存缓冲区与之对应。
标准输入输出流istream、ostream和iostream类外,还有3个用于文件操作的文件类:
- ifstream类,它是从istream类派生的,用来支持从磁盘文件的输入。
- ofstream类,它是从ostream类派生的,用来支持从磁盘文件的输出。
- fstream类,它是从iostream类派生的,用来支持从磁盘文件的输入输出。
三、文件的打开与关闭
对磁盘文件的操作是通过文件流对象(面不是cin和cout)实现的,文件流对象是用文件流类定义的,而不是用istream和ostream类来定义。例如建立一个输出文件流对象:
ofstream outfile;
3.1 打开磁盘
所谓打开文件是一种形象的说法,打开文件是指在文件读写之前做的必须准备,如:
- 为文件流对象和指定的磁盘文件建立关联,以便使文件流流向指定的磁盘文件。
- 指定文件的工作方式,如该文件是作为输入文件还是输出文件,是ASCiI文件还是二进制文件等。
打开文件可以两种形式,具体如下:
1)调用成员函数open形式
文件流对象.open(磁盘文件名, 输入输出方式);
示例如下:
cpp
ofstream outfile;
outfile.open("file.txt", ios::out);
磁盘文件名可以包括路径,如"c:\new\file.txt",如缺省路径,则默认为当前目录下的文件。
2)在定义文件流对象时指定参数
在声明文件流类时定义了带参数的构造函数,其中包括了打开磁盘文件的功能。示例如下:
cpp
ofstream outfile("file.txt", out;:out);
输入输出方式是在ios类中定义的,它们是枚举常量,有多种选择,具体如下表:
方式 | 作用 |
---|---|
ios::in | 以输入方式打开文件 |
ios::out | 以输出方式打开文件(这是默认方式),如果已有此名字的文件,则将其原有内容全部清除 |
ios::app | 以输出方式打开文件,写入的数据添加在文件末尾 |
ios::ate | 打开一个已有的文件,文件指针指向文件末尾 |
ios::trunc | 打开一个文件,如果文件已存在,则删除其中全部数据,如文件不存在,则建立新文件。如已指定了ios::out方式,而末指定ios::app、ios::ate、ios::in,则同样默认此方式。 |
ios::binary | 以二进制方式打开一个文件,如不指定此方式则默认为ASCII方式 |
ios::nocreate | 打开一个已有的文件,如文件不存在,则打开失败。nocreate的意思是不建立新文件 |
ios::noreplace | 如文件不存在则建立新文件,如果文件已存在则操作失败,noreplace的意思是不更新原有文件 |
ios::int|ios::out | 以输入和输出方式打开文件,文件可读可写 |
ios::out|ios::binary | 以二进制方式打开一个输出文件 |
ios::in|ios::binary | 以二进制方式打开一个输入文件 |
注意:
- 新版本 C++系统I/O类库中不提供ios::nocreate和ios::noreplace。
- 每一个打开的文件都有一个文件指针,该指针的初始位置由I/O方式指定,每次读写都从文件指针的当前位置开始。每读入一个字节,指针就后移一个字节。当文件指针移到最后,就会遇到文件结束EOF(文件结束符也占一个字节,其值为-1)此时流对像的成员函数eof的值为非0值(一般设为1),表示文件结束。
- 要以用"位或"运算符"|"对输入输出方式进行组合。
示例:
cpp
#include <iostream>
#include <fstream>
using namespace std;
int main(){
ofstream outfile;
outfile.open("file.txt", ios::app);
if(!outfile.is_open()){
clog <<"Open Error" <<endl;
}
return 0;
}
运行如上代码,如file.txt文件不存在,则会输出"Open Error"错误信息,并创建file.txt文件。
3.2 关闭磁盘文件
在对打开的磁盘文件的读写操作完成后,应关闭该文件。将3.1中代码修改后如下:
cpp
#include <iostream>
#include <fstream>
using namespace std;
int main(){
ofstream outfile;
outfile.open("file.txt", ios::app);
if(!outfile.is_open()){
clog <<"Open Error" <<endl;
}
outfile.close(); //关闭流
return 0;
}
所谓的关闭,实际上是解除磁盘文件与文件流的关联,原来设置的工作方式也失效,就不能再通过文件流对该文件进行输入或输出。
四、对ASCII文件的操作
文件的每一个字节中均以ASCII代码形式存放数据,即一个字节存放一个字符,这个文件就是ASCII文件(或称字符文件)。
对于ASCII文件的读写操作有以下两种方式:
- 用流插入运算符"<<"和流提取运算符">>"输入输出标准类型的数据。
- 用文件流put, get, getline等成员函数进行字符的输入输出。
4.1 打开文件并输出数据
下面将一个整形数组,含10个元素,从键盘输入10个整数给数组,将此数组存放到磁盘文件file2.txt中。代码如下:
cpp
#include <iostream>
#include <fstream>
using namespace std;
int main(){
int a[10];
ofstream outfile("file2.txt", ios::out); //定义文件流对象,打开磁盘文件"file2.txt"
if(!outfile){
cerr <<"open errror!" <<endl;
exit(1);
}
cout <<"enter 10 integer numbers:" <<endl;
for(int i = 0; i < 10; i++){
cin >>a[i];
outfile <<a[i] <<" "; // 向磁盘文件 file2.txt 输出数据
}
outfile.close(); //关闭磁盘文件"file2.txt"
return 0;
}
运行结果如下图:
说明:
- 程序中ofstream类定义文件流对象outfile,调用结构函数打开磁盘文件,并向文件中写入数据。另外参数ios::out可以省略,因为输出默认为ios::out。
- 如果打开成功,则文件流对象outfile的返回值为非0值,如果打开失败,则返回 值为0(假),加上"!"号非则为真。所以"!outfile"为真,则显示信息并执行exit退出。
4.2 打开文件并输入数据
下在通过示例,从文件file3.txt文件中读取事前存储好的10个整数,通过输入流读取内容,并计算出其中最大值以及其对应数组索引址。
file3.txt文件中下图:
示例代码:
cpp
#include <iostream>
#include <fstream>
using namespace std;
int main(){
int number[10];
int max, index, temp;
// 定义输入文件流对象
ifstream infile("file3.txt", ios::in);
// 如果输入失败,显示错误信息
if(!infile){
cerr <<"open eror~" <<endl;
exit(1);
}
// 循环输入整数
for(int i = 0; i < 10; i++){
infile >>number[i]; //将读取整数按顺序存放在number数组中
cout <<number[i] <<" "; // 在控制台输出结果
}
cout <<endl <<endl;
// 默认数组中第一个数组最大
index = 0;
// 默认先设置最大值为数组第一位元素
max = number[index];
// 循环判断最大值
for(int i = 0; i < 10; i++){
// 如果最大值小于当前元素值,则将其赋值给max
if(max < number[i]){
index = i;
max = number[i];
}
}
//
cout <<"max value:" <<max <<endl <<",index value:" <<index <<endl;
// 关闭输入流
infile.close();
return 0;
}
运行后结果如下图:
4.3 复制文件
输出和输出数据前面已了解了,现在可以用已了解的知识,写一个复制文件功能。
复制文件message.txt文本如下:
Desktop version: Privacy online features carefully protect you. Firefox automatically blocks more than 2,000 trackers from collecting records of your online behavior.
Mobile: No matter where you are, your privacy doesn't have to be compromised - your passwords, search history, open tabs, and other data are safe to take with you.
Enterprise Edition: Tailor product support cycles to your business needs for unparalleled data protection.
示例代码如下:
cpp
#include <iostream>
#include <fstream>
using namespace std;
int main(){
ifstream infile("message.txt");
ofstream outfile("messageCopy.txt");
// 判断是否打开成功
if(!infile){
cerr <<"Open message.txt Error";
exit(1);
}
if(!outfile){
cerr <<"Open messageCopy.txt Error";
exit(1);
}
// 定义变量接收字符
char ch;
// 循环读取文件中字符
while(infile.get(ch)){
outfile.put(ch);
cout <<ch;
}
cout <<endl;
// 关闭流文件
infile.close();
outfile.close();
return 0;
}
运行后,本来只有message.txt文件的目录,则会多出一个messageCopy.txt文件,并且将message.txt文件中内容全部复制到messageCopy.txt新创建文件中。(注意:ofstream流对象在默认ios::out情况下,则也默认ios:trunc,当文件不存时创建它。这个在本章节"2)在定义文件流对象时指定参数"中,文件输入输出方式设置值表中有说明)。
4.4 复制内容转换为大写
在4.3的按钮中,复制内容都是英文内容,而且txt文件中存为字符ASCII码。英文字母a~z小写字符对应范围是97~122,大写字母范围则是65~90,所以a与A之间相关为32。
在了解此规律后,再把上述示例修改一下,将所有复制内容转换为大写后,再复制到新文件中。示例代码如下:
cpp
#include <iostream>
#include <fstream>
using namespace std;
int main(){
ifstream infile("message.txt");
ofstream outfile("messageUpper.txt");
// 判断是否打开成功
if(!infile){
cerr <<"Open message.txt Error";
exit(1);
}
if(!outfile){
cerr <<"Open messageCopy.txt Error";
exit(1);
}
// 定义变量接收字符
char ch;
// 循环读取文件中字符
while(infile.get(ch)){
// 如果发现字母ASCII码值在97~122,则为小写字母,减32将其转换为大写
if(ch >= 97 && ch <= 122) ch -= 32;
outfile.put(ch);
cout <<ch;
}
cout <<endl;
// 关闭流文件
infile.close();
outfile.close();
return 0;
}
运行后结果如下:
此时目录中也会多出messateUpper.txt文件,其中内容已全部转换为大写。
五、对二进制文件的操作
二进制文件不是以ASCII码存放数据的它将内存中的数据存储形式不加转换地传送到磁盘文件,因此它又称作为内存数据的映像文件。因为文件中的信息不是字符数据,而是字节中的二进制形式的信息,因此它又称为字节文件。
5.1 用成员函数read和write读写二进制文件
对二进制文件的读写主要用istream类的成员函数read和write来实现。这两个函数的原型为:
istream& read(char *buffer, int len);
ostream& write(const char *buffer, int len);
5.1.1 通过二进制形式存储数据
在程序中定义一个Student类对象,再定义数组存储三个Student对象数据,然后使用流对象将数组信息通过二进制形式存储起来。示例代码如下:
cpp
#include <iostream>
#include <fstream>
using namespace std;
// 定义学生类
class Student{
private:
string name;
int num;
int age;
public:
Student(const char* nameStr = "None", int num = 0, int age = 0): num(num), age(age){
// 使用strncpy来避免缓冲区溢出(注意这里要确保不会超过30个字符)
// 如果nameStr可能超过29个字符(需要留一个位置给'\0'),则需要额外的长度检查
strncpy(name, nameStr, sizeof(name) - 1); // 复制nameStr到name,并确保最后一个字符是'\0'
name[sizeof(name) - 1] = '\0'; // 确保字符串是null终止的
}
};
int main(){
//定义学生类对象并初始化数据
Student list[] = {
Student("Tom", 1, 18),
Student("Lily", 2, 19),
Student("John", 3, 20)
};
// 创建输入流
ofstream outfile("student.data", ios::binary);
if(!outfile){
cerr <<"Open file Error" <<endl;
exit(1);
}
// 循环输出学生数据
for(int i = 0; i < 3; i++){
outfile.write((char*)&list[i], sizeof(list[i]));
}
// 关闭流对象
outfile.close();
return 0;
}
运行后,目录中则会出现student.data文件,使用文本打开发现内容是一堆乱码,这侧是通过二进制保存的数据。如下图:
上述代码中,&list[i]是结构体数组的一个元素首地址,但这个指向结构体的指针,与形参类型不匹配,因此要用(char *)把它强制转换为字符指针。第二个sizeof(list[i])的值是结构体数组的一个元素的字节数据,sizeof也在之前章节中应用过,并通过它获取过数组的长度,想必大家也并不陌生。
5.1.2 通过二进制读取数据
能通过二进制存储数据,也可以通过二制制将数据读取回来。现在就将5.1.1中生成的student.data数据读取到内存中,示例代码如下:
cpp
#include <iostream>
#include <fstream>
using namespace std;
// 定义学生类
class Student{
private:
char name[30];
int num;
int age;
public:
Student(const char* nameStr = "None", int num = 0, int age = 0): num(num), age(age){
// 使用strncpy来避免缓冲区溢出(注意这里要确保不会超过30个字符)
// 如果nameStr可能超过29个字符(需要留一个位置给'\0'),则需要额外的长度检查
strncpy(name, nameStr, sizeof(name) - 1); // 复制nameStr到name,并确保最后一个字符是'\0'
name[sizeof(name) - 1] = '\0'; // 确保字符串是null终止的
}
string getName(){
return name;
}
int getNum(){
return num;
}
int getAge(){
return age;
}
};
int main(){
Student list[3];
// 定义流对象,读取文件
ifstream infile("student.data", ios::binary);
// 判断是否打开成功
if(!infile){
cerr <<"Open file error" <<endl;
}
for(int i = 0; i < 3; i++){
infile.read((char *)&list[i], sizeof(list[i]));
}
// 关闭流文件
infile.close();
// 输出数据
for(int m = 0; m < 3; m++){
cout <<"name:" <<list[m].getName() <<endl;
cout <<"num:" <<list[m].getNum() <<endl;
cout <<"age:" <<list[m].getAge() <<endl <<endl;
}
return 0;
}
运行后结果如下图:
5.2 与文件指针有关的流成员函数
在磁盘文件中有一个文件指针,用来指明当前应进行读写的位置。在输入时每读入一个字节,指针就向后移动一个字节。在输出时每向文件输出一个字节,指针就向后移动一个字节,随着输出文件中字节不断增加,指针不断后移。对于二进制文件,允许对指针进行控制,使它按用户的效果图移动到所需的位置,以便在该位置上进行读写。
文件流提供了一些有关文件指针的成员函数,具体如下表:
成员函数 | 作用 |
---|---|
gcount() | 返回最后一次输入所读入的字节数 |
tellg() | 返回输入文件指针的当前位置 |
seekg(文件中的位置) | 将输入文件中指针移到指定的位置 |
seekg(位移量, 参照位置) | 以参照位置为基础移动若干字节("参照位置"的用法见说明) |
tellp() | 返回输出文件指针当前的位置 |
seekp(文件中的位置) | 将输出文件中指针移到指定的位置 |
seekp(位移量, 参照位置) | 以参照位置为基础移动若干字节 |
注意:上述函数名中带的"g"或"p",其实g是get的首字母,p是put的首字母。所以带有get的则是用于输入文件,带有put则是用于输出文件。
参照位置如下表:
|----------|--------------------------|
| 名称 | 描述 |
| ios::beg | 文件开头(beg是begin的缩写),这是默认值 |
| ios::cur | 指针当前的位置(cur是current的缩写) |
| ios::end | 文件末尾 |
它们是在ios类中定义的枚举常量。它们使用方法,如下示例:
cpp
infile.seekg(100); //输入文件中的指针向前移到100字节位置
infile.seegg(-50, ios::cur); //输入文件中的指针从当前位置后移50字节
outfile.seekp(-75, ios::end); //输出文件中的指针从文件尾后移50字节
5.3 随机访问二进制数据文件
一般情况下读写是顺序进行的,即逐个字节进行读写。但是对于二进制数据文件来说,可以利用上的成员函数移动指针,随机地访问文件中任一位置上的数据,还可以修改文件中的内容。
5.3.1 reinterpret_cast
语法形式:
reinterpret_cast<type-id> (expression)
type-id 必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,再把该整数转换成原类型的指针,还可以得到原先的指针值)。
该运算符的用法比较多。操作符修改了操作数类型,但仅仅是重新解释了给出的对象的比特模型而没有进行二进制转换。例如:
cpp
int *n= new int ;
double *d=reinterpret_cast<double*> (n);
在进行计算以后, d 包含无用值. 这是因为 reinterpret_cast 仅仅是复制 n 的比特位到 d, 没有进行必要的分析。
5.3.2 static_cast
static_cast是一个c++运算符,功能是把一个表达式转换为某种类型,但没有运行时类型检查来保证转换的安全性。
语法形式:
static_cast <type-id>( expression )
static_cast和reinterpret_cast的区别主要在于多重继承,例如:
cpp
#include <cstdio>
class A {
public:
int m_a;
};
class B {
public:
int m_b;
};
class C : public A, public B {};
//那么对于以下代码:
int main(){
C c;
printf("%p,\n%p, \n%p", &c, reinterpret_cast<B*>(&c), static_cast <B*>(&c));
return 0;
}
运行结果如下图:
前两个的输出值是相同的,最后一个则会在原基础上偏移4个字节,这是因为static_cast计算了父子类指针转换的偏移量,并将之转换到正确的地址(c里面有m_a,m_b,转换为B*指针后指到m_b处),而reinterpret_cast却不会做这一层转换。
编译器隐式执行任何类型转换都可由static_cast显示完成;reinterpret_cast通常为操作数的位模式提供较低层的重新解释。
5.3.3 示例
将5.1.1和5.1.2中示例代码稍作修改,将数据添加至5条,修改第三条数据的内容,再将其保存到文件中。代码如下:
cpp
#include <iostream>
#include <fstream>
#include <cstring>
using namespace std;
class Student {
private:
char name[50];
int id;
int age;
public:
Student(){}
// 构造函数,假设name不会超过49个字符(需要为'\0'留出空间)
Student(const char* nameStr, int id, int age) {
strncpy(name, nameStr, sizeof(name) - 1);
name[sizeof(name) - 1] = '\0';
this->id = id;
this->age = age;
}
void setId(int id){
this->id = id;
}
void setAge(int age){
this->age = age;
}
// 打印函数
void print() const {
cout << "Name: " << name << ", ID: " << id << ", Age: " << age << endl;
}
};
int main() {
// 定义学生数组并初始化数据
Student students[5] = {
Student("Tom", 1, 18),
Student("Lily", 2, 19),
Student("John", 3, 20),
Student("LiuLei", 4, 18),
Student("LiMei", 5, 17)
};
string filename = "stu.data";
// 写入文件
fstream file(filename, ios::out | ios::binary | ios::trunc);
if (!file) {
cerr << "Open file error for writing." << endl;
return 1; // 退出程序
}
for (const auto& student : students) {
file.write(reinterpret_cast<const char*>(&student), sizeof(student));
}
file.close();
// 读取文件并打印学生信息
file.open(filename, ios::in | ios::binary);
if (!file) {
cerr << "Open file error for reading." << endl;
return 1; // 退出程序
}
Student tempStudent;
for (int i = 0; i < 5; i++) {
file.read(reinterpret_cast<char*>(&tempStudent), sizeof(tempStudent));
tempStudent.print();
}
file.close();
// 修改第三个学生的信息
students[2].setId(303);
students[2].setAge(21);
// 重新打开文件以进行写入
file.open(filename, ios::in | ios::out | ios::binary);
if (!file) {
cerr << "Open file error for updating." << endl;
return 1; // 退出程序
}
// 定位到第三个学生的开始位置
file.seekg(2 * sizeof(students[2]), ios::beg); // 跳过前两个学生信息
if (file) {
// 写入第三个学生的新信息
file.write(reinterpret_cast<const char*>(&students[2]), sizeof(students[2]));
} else {
cerr << "Seek error." << endl;
}
file.close();
cout << "Student information has been updated and saved to the file." << endl;
// 再次读取文件并显示所有学生的信息
file.open(filename, ios::in | ios::binary);
if (!file) {
cerr << "Open file error for reading again." << endl;
return 1; // 退出程序
}
cout << "Updated student information:" << endl;
for (int i = 0; i < 5; i++) {
Student tempStudent;
file.read(reinterpret_cast<char*>(&tempStudent), sizeof(tempStudent));
tempStudent.print();
}
file.close();
return 0;
}
运行后结果如下图:
注意几点:
- 我们使用了fstream而不是分别使用ofstream和ifstream,因为我们要在同一个文件中进行读写操作。
- 使用seekg来定位到文件中第三个Student对象的开始位置。