C++文件操作全解析
在实际编程中,对文件进行访问操作是很常见的需求。C++在处理文件访问时,与处理标准输入输出的方式类似。下面将详细介绍C++中文件操作的相关知识。
1. 流的概念
流指的是字节从源流入程序,或者从程序流向目标的过程。如果是流入程序(如从键盘输入),则称为输入流;如果是从程序流出(如输出到屏幕),则称为输出流。在之前的程序中,输入流通常与键盘关联,输出流与屏幕关联。而大型程序可能需要额外的流,比如与磁盘文件或其他设备(如传感器、打印机、相机等)关联的流。
常见的
cout
和
cin
对象就分别代表了输出流和输入流。例如,
cout
是
ostream
类的对象,它有存储输出格式信息的成员,还有用于存储待显示信息的内存缓冲区。当执行
cout << "This is a message";
时,信息会先存储在这个缓冲区中,然后再输出到屏幕。同样,
cin
代表输入流,信息从输入设备(如键盘)通过
cin
对象流入程序。
许多程序员在从流中读取数据时,喜欢使用
while(cin >> a)
或
while(cin.getline(s, sizeof(s))
这样的表达式。这两个循环的工作方式类似,
operator>>
函数从流中读取一个值,存储到变量中,并返回流的引用。如果读取错误或没有更多数据,条件就会变为
false
,循环终止。
2. 流的状态
在对流执行操作(如写入数据)后,需要检查流的状态,以确定操作是否成功。可以通过检查特定的标志(flag)或调用相应的函数来判断是否发生错误。这些标志在
ios_base
类中声明,具体如下:
| 标志 | 描述 | 函数 |
| ---- | ---- | ---- |
| badbit | 若发生不可恢复的错误,其值变为1 | bad(),若badbit值为1则返回true |
| failbit | 若输入/输出操作失败,其值变为1 | fail(),若badbit或failbit值为1则返回true |
| eofbit | 若到达文件末尾,其值变为1 | eof(),若eofbit值为1则返回true |
| goodbit | 若没有错误发生,即前三个值都为0,其值变为0 | good(),若没有错误发生则返回true |
还有
clear()
函数,默认参数值为0,例如
clear();
可以清除
badbit
、
failbit
和
eofbit
的值。如果这些标志位被设置,并且想再次使用流,就需要清除它们以重置流的状态。通常可以使用
operator!
函数来检查流的状态,如
if(!cin)
,若发生错误,条件为
true
。
3. 文本文件和二进制文件
-
文本文件
:由一行或多行按照标准格式(如ASCII码)的可读字符组成。使用任何可用的编辑器都可以轻松处理文本文件。每行以操作系统用于表示行尾的特殊字符结尾,在Windows系统中,是
'\r'(回车)和'\n'(换行)字符对(即CR/LF,ASCII码分别为13和10);而在Unix系统中,仅'\n'字符表示行尾。 -
二进制文件
:其字节不一定代表可读字符,例如可执行的C++程序、图像或声音文件通常以二进制形式存储。打开二进制文件时,可能会看到一些难以理解的字符。二进制文件不分行,也不会进行字符转换,在Windows中,写入二进制文件时,换行字符不会扩展为
\r\n。
另外,操作系统可能会在文本文件末尾添加一个特殊字符来标记文件结束,如在Windows中,
Ctrl+Z
字符标记文本文件的结束;而二进制文件中没有字符具有特殊意义。
存储数据时,二进制文件相比文本文件可以节省空间。例如,使用ASCII字符集将数字47654写入文本文件,由于该数字由五个字符表示,文件大小为5字节;而以二进制形式存储时,文件大小仅为2字节。不过,二进制文件也有缺点,当从一个系统传输到另一个系统时,数据的表示方式可能不同,因为不同系统存储数据的方式可能不同,而且数据类型的大小也可能因系统而异。
4. 文件输入输出
C++提供了多种有用的类来管理文件,具体如下:
-
ofstream
类:从
ostream
派生而来,用于向文件中写入数据。
-
ifstream
类:从
istream
派生而来,用于读取文件。
-
fstream
类:从
iostream
派生而来,而
iostream
又从
ostream
和
istream
派生,用于同时读写文件。
这些类继承了
ios_base
的常量和函数,用于检查流的状态。使用时需要包含
<fstream>
头文件。
下面是一个简单的向文件写入数据的示例:
#include <iostream>
#include <fstream>
#include <cstdlib>
int main()
{
int i;
std::ofstream fout;
fout.open("test.txt");
if(fout.is_open() == false)
{
std::cout << "Error: File can not be opened\n";
exit(EXIT_FAILURE);
}
for(i = 0; i < 3; i++)
fout << "Hello_" << i+1 << '\n';
fout.close();
return 0;
}
上述代码的步骤为:首先创建一个
ofstream
对象,然后将其与文件关联,最后像使用
cout
一样使用该对象向文件中写入数据。如果文件不存在,程序会创建它;如果文件已存在,其内容会被删除。
如果要读取文件内容,需要创建一个
ifstream
对象,将其与文件关联,然后像使用
cin
一样使用该对象读取内容。示例如下:
#include <iostream>
#include <fstream>
#include <string>
#include <cstdlib>
using namespace std;
int main()
{
double temp;
string fname;
cout << "Enter file name: ";
cin >> fname;
ifstream fin(fname.c_str());
if(fin.is_open() == false)
{
cout << "Error: File can not be opened\n";
exit(EXIT_FAILURE);
}
while(1)
{
fin >> temp;
if(!fin)
break;
if(temp >= -5 && temp <= 5)
cout << temp << '\n';
}
fin.close();
return 0;
}
如果要同时读写文件,则需要使用
fstream
对象。示例如下:
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <string>
using namespace std;
struct Student
{
string name;
int code;
float grd;
};
int main()
{
Student s1 = {"J.Lee", 100, 5.8}, s2;
fstream fstr("test.txt", ios_base::in | ios_base::out | ios_base::trunc);
if(fstr.is_open() == false)
{
cout << "Error: File can not be opened\n";
exit(EXIT_FAILURE);
}
fstr << s1.name << ' ' << s1.code << ' ' << s1.grd << '\n';
fstr.seekg(0);
fstr >> s2.name >> s2.code >> s2.grd;
cout << s2.name << ' ' << s2.code << ' ' << s2.grd << '\n';
fstr.close();
return 0;
}
5. 流缓冲区
ofstream
类使用缓冲区来存储数据,为每个创建的
ofstream
对象,程序会分配相应的内存。这个内存作为程序和文件之间的中间缓冲区,数据不会直接写入文件,而是先存储在输出缓冲区中。当缓冲区满时,程序会刷新缓冲区,将数据写入文件。这种缓冲方式可以提高性能,因为一次向磁盘进行大的数据传输比多次小的数据传输更高效。同样,使用
ifstream
对象从磁盘读取文件时也会应用缓冲机制,程序会读取大块数据,存储在对象的输入缓冲区中,然后处理缓冲区中的数据。
可以在需要时刷新输出缓冲区,例如使用
flush
、
endl
、
ends
或
unitbuf
操纵符。另外,输出流可以与另一个流关联,在第二个流上执行读写操作可能会导致输出流的缓冲区刷新。例如,
cin
和
cerr
流默认与
cout
关联,从
cin
读取或向
cerr
写入会刷新
cout
的缓冲区。
如果程序异常结束(如崩溃),缓冲区不会被刷新,那么缓冲区中的数据可能不会被处理。因此,为了更好地控制程序,与程序操作相关的诊断消息应该直接输出(如使用
endl
),这样即使程序崩溃,程序员也能看到这些消息。
6. 关闭文件
在使用缓冲方式向文件写入数据时,数据不会直接写入磁盘上的文件,而是先写入为其预留的缓冲区。当缓冲区满或文件关闭时,缓冲区会被刷新,数据会存储到文件中。这样可以避免频繁访问磁盘,提高程序性能。
close()
函数用于关闭打开的文件,如果文件是为写入而打开的,
close()
会将缓冲区中可能剩余的数据存储到文件中。
虽然程序正常终止时,打开的文件会自动关闭,但最好在不再使用文件时手动关闭它。例如,以下代码可能会导致数据丢失:
int *p;
ofstream fout("test.txt");
// Write data in the file.
...
// We finished with the data writing, but we did not close the file.
*p = 20; // Wrong action.
由于
p
没有指向有效的地址,程序可能会崩溃,此时缓冲区中可能剩余的数据会丢失。如果在写入完成后调用了
close()
,缓冲区会被刷新,数据会存储到文件中。所以,要记得在不再需要文件时关闭它,以确保数据不会丢失。
7. 文件打开模式
在将文件与流对象关联时,可以使用第二个参数来指定对文件执行的操作。
ios_base
类定义了表示打开模式的常量,使用
|
位运算符可以启用多种模式。具体的打开模式如下:
| 打开模式 | 操作 |
| ---- | ---- |
| ios_base::in | 以只读模式打开文件 |
| ios_base::out 或 ios_base::out | ios_base::trunc | 以写入模式打开文件,如果文件存在,其内容会被截断;如果文件不存在,则会创建它 |
| ios_base::app | 以追加模式打开文件,如果文件存在,现有数据会被保留,新数据会添加到文件末尾;如果文件不存在,则会创建它 |
| ios_base::in | ios_base::out | 以读写模式打开文件 |
| ios_base::in | ios_base::out | ios_base::trunc | 以读写模式打开文件,如果文件存在,其内容会被截断;如果文件不存在,则会创建它 |
| ios_base::in | ios_base::out | ios_base::app | 以读写和追加模式打开文件,如果文件存在,现有数据会被保留,新数据会添加到文件末尾;如果文件不存在,则会创建它 |
还有
ios_base::ate
选项,它会将文件指针置于文件末尾。
ios_base::app
和
ios_base::ate
的区别在于,前者只允许在文件末尾添加数据,而后者只是将文件指针置于文件末尾。
打开文本文件时,选择上述模式之一;打开二进制文件时,需要添加
ios_base::binary
常量。例如,以二进制模式打开文件进行读取,可使用
ios_base::in | ios_base::binary
;以二进制模式打开文件进行读写,可使用
ios_base::in | ios_base::out | ios_base::trunc | ios_base::binary
。
打开模式是可选参数,如果未指定,会应用默认模式。
ifstream
的
open()
函数和构造函数的默认模式是
ios_base::in
,
ofstream
的
open()
函数和构造函数的默认模式是
ios_base::out
,
fstream
的
open()
函数和构造函数的默认模式是
ios_base::in | ios_base::out
。以下是一些示例:
ifstream fin("test.txt"); // 以默认模式ios_base::in打开文件进行读取
ofstream fout("test.txt", ios_base::app); // 以写入和追加模式打开文件,保留现有数据
fstream f("test.txt", ios_base::in | ios_base::out | ios_base::trunc); // 以读写模式打开文件,删除现有数据
8. 打开多个文件
应用程序可能需要打开多个文件。如果需要同时打开多个文件,则必须为每个文件声明单独的对象。可以同时打开的文件数量取决于操作系统。
在打开多个文件的应用程序中,建议在不再需要某个文件时关闭它,因为操作系统对同时打开的文件数量有限制。关闭文件后,可以将相应的对象用于另一个文件,而无需声明新的对象,这样可以避免分配新的内存。例如:
ifstream fin;
fin.open(fname1);
/* Use fin to read the file. */
fin.close();
fin.open(fname2);
/* Use the same object to read a new file. */
fin.close();
...
9. 文件处理
当文件被打开并与相应的对象关联时,与文件相关的信息会存储在对象的成员中。例如,
ofstream
对象包含一个指针成员,指示文件中写入数据的位置;
ifstream
对象包含一个指针成员,指示文件中读取数据的位置。
当文件以读取或写入模式打开时,指针指向文件的开头;如果以追加模式打开,指针指向文件末尾。执行读写操作时,指针的值会自动更新。例如,以读取模式打开文件并读取50个字符后,指针会指向距离文件开头50字节的位置;以写入模式打开文件时,指针会前进与写入字节数相等的位置,下一次写入操作将在新位置进行。
处理文件可以使用串行访问和随机访问两种方式。串行访问时,每个读写操作按顺序执行,例如要读取第50个字符,必须先读取前49个字符;而随机访问可以直接移动到文件内的任何位置,使用
seekg()
和
seekp()
函数可以实现文件的随机访问。
以下是
seekg()
函数的两种原型:
istream& seekg(streamoff offset, ios_base::seekdir origin);
istream& seekg(streampos offset);
第一个版本将输入指针移动到距离
origin
指定位置
offset
字节的新位置,
streamoff
是某种整数类型的同义词,如果
offset
为负,指针会向后移动。
origin
的值应该是
ios_base
类的以下整数常量之一:
-
ios_base::beg
:指针从文件开头移动
offset
字节。
-
ios_base::cur
:指针从当前位置移动
offset
字节。
-
ios_base::end
:指针从文件末尾移动
offset
字节。
第二个版本中,
offset
的值表示从文件开头以八位字节为单位的新位置。以下是一些示例:
seekg(0, ios_base::end); // 移动到文件末尾
seekg(20, ios_base::beg); // 从文件开头移动20字节
seekg(-5, ios_base::cur); // 从当前位置向后移动5字节
如果要查找指针的当前位置,可以使用
tellg()
函数,它返回一个
streampos
值,表示输入指针相对于文件开头的当前位置(以字节为单位),如果发生错误则返回 -1。可以结合使用
tellg()
和
seekg()
返回到文件的先前位置:
// Store the location to which we want to return.
streampos old_pos = fin.tellg();
// ... move to another location to read.
fin.seekg(old_pos); // Return back to the first location.
同样,对于与
ofstream
对象关联的文件的随机访问,可以使用
seekp()
和
tellp()
函数。需要注意的是,在文本文件中使用
seekg()
和
seekp()
时,要注意换行字符的处理。例如,在Windows系统中,如果字符串包含换行字符,使用
write()
函数时可能会出现意外结果,因此使用
seekp()
和
seekg()
时,二进制文件比文本文件更安全。
在
fstream
对象的情况下,继承的输入和输出指针会同步移动,因此
tellg()
和
tellp()
返回相同的值。如果文件是为读写而打开的,要在读写之间切换,应该先调用文件定位函数(如
seekp()
)或先刷新缓冲区(如使用
flush
)。可以使用
fail()
函数检查
seekg()
或
seekp()
是否执行成功。
10. 文件结束标志
操作系统可能会在文本文件末尾添加一个特殊字符来标记文件结束,而二进制文件没有这样的标记。在Dos/Windows应用程序中,通常使用ASCII值为26的
Ctrl+Z
字符来标记文本文件的结束。例如,以下程序在这样的系统中运行时,只会显示字符
a
和
b
:
#include <iostream>
#include <fstream>
#include <cstdlib>
using namespace std;
int main()
{
int ch;
fstream fstr("test.txt", ios_base::in | ios_base::out | ios_base::trunc);
if(fstr.is_open() == false)
{
cout << "Error: is_open() failed\n";
exit(EXIT_FAILURE);
}
fstr << 'a' << 'b' << (char)26 << 'c' << 'd';
fstr.seekg(0);
while(1)
{
ch = fstr.get();
if(ch == EOF)
break;
cout << (char)ch;
}
fstr.close();
return 0;
}
如果以二进制模式创建文件,程序会显示所有字符,因为二进制文件中没有字符具有特殊意义。
11. 重定向
包含
<iostream>
文件后,可以使用
cin
、
cout
、
cerr
和
clog
对象。
cin
与标准输入设备关联,
cout
与标准输出设备关联,
cerr
和
clog
对应标准错误流,与标准输出设备(如屏幕)关联,都可用于显示消息。需要注意的是,
cerr
是无缓冲的,输出会立即显示。
许多操作系统允许对
cin
和
cout
进行重定向。例如,如果可执行文件名为
test
,在命令行中输入
test >output.txt
,
cout
中写入的数据将不会显示在屏幕上,而是会写入
output.txt
文件中。同样,可以使用
test <input.txt
将
cin
的输入重定向到
input.txt
文件,还可以同时进行两种重定向,如
test <input.txt >output.txt
。
为了与其他程序保持一致,通常继续使用
cout
输出各种消息,但在输出错误消息时,最好使用
cerr
或
clog
而不是
cout
。因为如果
cout
被重定向,在打开文件之前无法看到消息,而写入
cerr
或
clog
的错误消息会实时显示在屏幕上。
12. 读写文本文件
处理文本文件时,可以使用与
cout
和
cin
对象相同的函数,如
operator<<()
和
operator>>()
函数。以下是一个将对象写入文件的示例:
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <string>
#include <vector>
using namespace std;
class Student
{
public: // For simplicity, I declare all as public.
int code;
string name;
float grd;
Student(int c, const string& n, float g) : code(c), name(n), grd(g) {}
};
int main()
{
int i;
Student s1(1, "First", 2.5), s2(2, "Sec", 3.5), s3(3, "Third", 4.5);
vector<Student> v;
v.push_back(s1);
v.push_back(s2);
v.push_back(s3);
std::ofstream fout("test.txt");
if(fout.is_open() == false)
{
std::cout << "Error: File can not be opened\n";
exit(EXIT_FAILURE);
}
for(i = 0; i < v.size(); i++)
fout << v[i].code << '\t' << v[i].name << '\t' << v[i].grd << '\n';
fout.close();
return 0;
}
如果要指定数据在文件中的存储格式,可以使用操纵符等工具。例如,要将成绩以两位小数的形式存储,可以这样写:
fout << fixed << setprecision(2) << v[i].name << '\t' << v[i].code << '\t' << v[i].grd << '\n';
在读取文件时,最常用的函数是
operator>>()
、
getline()
和
get()
。使用
operator>>()
函数从文件中读取数据并赋值给适当的变量时,程序员需要知道文件中存储数据的类型和顺序。例如,要读取上述程序创建的文件,需要知道每行依次包含一个整数、一个字符串和一个浮点数。以下是读取文件的示例:
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <string>
using namespace std;
int main()
{
int code;
string name;
float grd;
ifstream fin("test.txt");
if(fin.is_open() == false)
{
cout << "Error: is_open() failed\n";
exit(EXIT_FAILURE);
}
while(1)
{
fin >> code >> name >> grd;
if(!fin)
break;
cout << code << ' ' << name << ' ' << grd << '\n';
}
fin.close();
return 0;
}
在循环中读取每行数据并存储到相应的变量中。如果尝试在文件末尾之后读取或发生其他错误,流的状态会变为
fail
,循环会结束。可以使用
eof()
函数确定是到达文件末尾还是其他原因导致读取失败。
也可以使用
getline()
函数逐行显示文件内容,例如:
string line;
while(1)
{
getline(fin, line);
if(!fin)
break;
cout << line << '\n';
}
许多程序员将读取语句放在
while
条件中,即
while(getline(fin, line)) cout << line << '\n';
,但使用无限循环可以更清晰地看到读取语句和终止循环的条件。还可以使用
get()
函数逐个字符读取文件。如果到达文件末尾并想再次访问文件,需要调用
clear()
清除
eofbit
。
13. 读写二进制文件
读写二进制文件分别使用
write()
和
read()
函数,虽然它们主要用于二进制文件,但也可以谨慎地用于文本文件。
write()
函数的原型为:
ostream& write(const char *buf, streamsize count);
buf
参数指向存储要写入文件数据的内存,
streamsize
是某种整数类型的同义词,
count
参数指定要写入文件的字节数。以下是一些使用
write()
函数的示例:
int arr[1000];
ofstream fout("test.dat", ios_base::out | ios_base::trunc | ios_base::binary);
fout.write((char*)arr, sizeof(arr));
double d = 1.2345;
fout.write((char*)&d, sizeof(double));
Student s;
fout.write((char*)&s, sizeof(s));
需要注意的是,只有对象的数据会被写入文件,而不是函数。应该将地址强制转换为
char*
,并使用
sizeof
运算符指定大小,以确保程序的平台独立性。可以通过检查流的状态来验证是否发生错误,例如
if(!fout) cout << "write error\n";
。
read()
函数的原型为:
istream& read(char *buf, streamsize count);
buf
参数指向存储读取数据的内存,
count
参数指定从文件中读取的字节数。由于不同系统可能存在差异(如数据类型大小不同),不能保证使用
write()
函数写入的文件在另一个系统中能被
read()
函数正确读取。以下是使用
read()
函数的示例:
double d;
ifstream fin("test.dat", ios_base::in | ios_base::binary);
fin.read((char*)&d, sizeof(d));
int arr[1000];
fin.read((char*)arr, sizeof(arr));
使用
write()
和
read()
函数时,二进制文件比文本文件更安全,因为文本文件中换行字符的潜在转换可能会产生意外结果。另外,在处理使用虚拟函数的类时要小心,因为虚拟函数会有一个隐藏指针,该指针会被存储在文件中,再次读取文件时可能会导致指针值无效,从而使程序崩溃。
相关练习
1. 练习C.24.1
将示例C.24.5中
Student
类的成员改为私有,并完成以下程序,使其显示两个学生的数据:
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <string>
using namespace std;
class Student
{
private:
int code;
string name;
float grd;
public:
Student(int c, const string& n, float g) : code(c), name(n), grd(g) {}
friend istream& operator>>(istream& in, Student& s);
friend ostream& operator<<(ostream& out, const Student& s);
void show() const {cout << "N:" << name << " C:" << code << " G:" << grd << '\n';}
};
istream& operator>>(istream& in, Student& s)
{
in >> s.name >> s.code >> s.grd;
return in;
}
ostream& operator<<(ostream& out, const Student& s)
{
out << s.name << '\t' << s.code << '\t' << s.grd << '\n';
return out;
}
int main()
{
Student s1(1, "First", 2.5), s2(5, "Second", 7.5);
fstream fstr("test.txt", ios_base::in | ios_base::out | ios_base::trunc);
if(fstr.is_open() == false)
{
cout << "Error: File can not be opened\n";
exit(EXIT_FAILURE);
}
fstr << s1 << s2;
fstr.seekg(0);
fstr >> s1 >> s2;
s1.show();
s2.show();
fstr.close();
return 0;
}
2. 练习C.24.2
编写一个程序,连续读取产品代码和价格,并将它们存储在
test.txt
文件中。如果用户输入价格为 -1,则停止插入产品。然后,程序应读取一个产品代码,在文件中查找并显示其价格(如果找到):
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <string>
using namespace std;
int main()
{
bool found;
string code, tmp;
double prc;
fstream fstr("test.txt", ios_base::in | ios_base::out | ios_base::trunc);
if(fstr.is_open() == false)
{
cout << "Error: is_open() failed\n";
exit(EXIT_FAILURE);
}
while(1)
{
cout << "Enter price: ";
cin >> prc;
if(prc == -1)
break;
cin.get();
cout << "Enter product code: ";
getline(cin, code);
fstr << code << ' ' << prc << '\n';
}
cin.get();
cout << "Enter product code to search for: ";
getline(cin, tmp);
found = 0;
fstr.seekg(0);
while(1)
{
fstr >> code >> prc;
if(!fstr)
break;
if(code == tmp)
{
found = 1;
break;
}
}
if(found == 0)
cout << tmp << " code is not listed\n";
else
cout << "The price for product " << code << " is " << prc << '\n';
fstr.close();
return 0;
}
3. 练习C.24.3
假设
students.txt
文件的每行包含学生的姓名和两门课程的成绩,编写一个程序,读取每行数据,将平均成绩大于或等于5的学生的姓名和成绩存储在
suc.txt
文件中,将平均成绩小于5的学生的姓名和成绩存储在
fail.txt
文件中,并显示每个文件中写入的学生数量:
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <string>
using namespace std;
int main()
{
string fnm, lnm;
int suc_stud, fail_stud;
double grd1, grd2;
ifstream fin("students.txt");
if(fin.is_open() == false)
{
cout << "Error: students.txt can't be loaded\n";
exit(EXIT_FAILURE);
}
ofstream fout_suc("suc.txt");
if(fout_suc.is_open() == false)
{
fin.close();
cout << "Error: suc.txt can't be created\n";
exit(EXIT_FAILURE);
}
ofstream fout_fail("fail.txt");
if(fout_fail.is_open() == false)
{
fin.close();
fout_suc.close();
cout << "Error: fail.txt can't be created\n";
exit(EXIT_FAILURE);
}
suc_stud = fail_stud = 0;
while(1)
{
fin >> fnm >> lnm >> grd1 >> grd2;
if(!fin)
break;
if((grd1+grd2)/2 >= 5)
{
fout_suc << fnm << ' ' << lnm << ' ' << grd1 << ' ' << grd2 << '\n';
suc_stud++;
}
else
{
fout_fail << fnm << ' ' << lnm << ' ' << grd1 << ' ' << grd2 << '\n';
fail_stud++;
}
}
cout << "Fail: " << fail_stud << " Success: " << suc_stud << '\n';
fin.close();
fout_suc.close();
fout_fail.close();
return 0;
}
4. 练习C.24.4
编写一个程序,读取两个文件的名称,逐行比较它们,并显示它们的第一行相同内容。如果两个文件没有相同的行,程序应显示相关消息:
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <string>
using namespace std;
int main()
{
bool found;
string fname, str1, str2;
cout << "Enter first file: ";
cin >> fname;
ifstream fin_f(fname.c_str());
if(fin_f.is_open() == false)
{
cout << "Error: first is_open() failed\n";
exit(EXIT_FAILURE);
}
cout << "Enter second file: ";
cin >> fname;
ifstream fin_s(fname.c_str());
if(fin_s.is_open() == false)
{
fin_f.close();
cout << "Error: second is_open() failed\n";
exit(EXIT_FAILURE);
}
found = 0;
while(1)
{
getline(fin_f, str1);
getline(fin_s, str2);
if(!fin_f || !fin_s)
break;
if(str1 == str2)
{
cout << "The same line is: " << str1 << '\n';
found = 1;
break;
}
}
if(found == 0)
cout << "There is no common line\n";
fin_f.close();
fin_s.close();
return 0;
}
5. 练习C.24.5
编写一个程序,读取一个文件的名称,并显示其第二行。要求使用
get()
函数而不是
getline()
函数:
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <string>
using namespace std;
int main()
{
int ch, lines;
string fname;
cout << "Enter file name: ";
cin >> fname;
ifstream fin(fname.c_str());
if(fin.is_open() == false)
{
cout << "Error: is_open() failed\n";
exit(EXIT_FAILURE);
}
cout << "Line contents: ";
lines = 1;
while(1)
{
ch = fin.get();
if((!fin) || (lines > 2))
break;
if(lines == 2)
cout << (char)ch;
if(ch == '\n')
lines++;
}
fin.close();
return 0;
}
6. 练习C.24.6
编写一个程序,读取一个密钥字符和一个文本文件的名称,通过将文件中的每个字符与密钥进行异或运算来加密文件内容,并将加密后的字符存储在用户选择的第二个文件中:
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <string>
using namespace std;
int main()
{
int ch, key_ch;
string fname;
cout << "Enter input file: ";
cin >> fname;
ifstream fin(fname.c_str());
if(fin.is_open() == false)
{
cout << "Error: Input file can't be loaded\n";
exit(EXIT_FAILURE);
}
cout << "Enter output file: ";
cin >> fname;
ofstream fout(fname.c_str());
if(fout.is_open() == false)
{
fin.close();
cout << "Error: Output file can't be created\n";
exit(EXIT_FAILURE);
}
cin.get();
cout << "Enter key char: ";
key_ch = cin.get();
while(1)
{
ch = fin.get();
if(!fin)
break;
fout << (char)(ch ^ key_ch);
}
fin.close();
fout.close();
return 0;
}
如果重新运行程序,将加密后的文件作为输入并使用相同的密钥,输出文件将与原始文件相同,因为根据布尔代数
(a ^ b) ^ b = a
。
7. 练习C.24.7
定义
Country
类,其私有成员包括名称、首都和人口。假设
country.txt
文件的每行包含相应国家的数据,格式为
name capital population
。编写一个程序,读取文件并使用该类将国家数据存储在
Country
元素的向量中。然后,程序应读取一个数字,并显示人口高于该数字的国家的数据:
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <string>
#include <vector>
using namespace std;
class Country
{
private:
string name;
string capital;
int pop;
public:
Country(const string& n = "", const string& c = "", int p = 0) : name(n), capital(c), pop(p) {}
void set(const string& n, const string& c, int p);
int get_pop() const {return pop;}
void show() const;
};
void Country::set(const string& n, const string& c, int p)
{
name = n;
capital = c;
pop = p;
}
void Country::show() const
{
cout << name << '\t' << capital << '\t' << pop << '\n';
}
int main()
{
int i, pop;
string name, capital;
Country tmp;
vector<Country> cntr;
ifstream fin("country.txt");
if(fin.is_open() == false)
{
cout << "Error: is_open() failed\n";
exit(EXIT_FAILURE);
}
while(1)
{
fin >> name >> capital >> pop;
if(!fin)
break;
tmp.set(name, capital, pop);
cntr.push_back(tmp);
}
cout << "Enter population: ";
cin >> pop;
for(i = 0; i < cntr.size(); i++)
if(cntr[i].get_pop() >= pop)
cntr[i].show();
fin.close();
return 0;
}
8. 练习C.24.8
编写一个程序,声明一个包含5个整数(值为10、20、30、40和50)的数组,并将它们写入二进制文件。然后,程序应读取一个整数,将存储的第三个整数替换为输入的数字。最后,程序应读取并显示文件的内容:
#include <iostream>
#include <fstream>
#include <cstdlib>
using namespace std;
int main()
{
int i, arr[5] = {10, 20, 30, 40, 50};
fstream fstr("test.bin", ios_base::in | ios_base::out | ios_base::trunc | ios_base::binary);
if(fstr.is_open() == false)
{
cout << "Error: is_open() failed\n";
exit(EXIT_FAILURE);
}
fstr.write((char*)arr, sizeof(arr));
cout << "Enter new value: ";
cin >> i;
fstr.seekp(2*sizeof(int));
fstr.write((char*)&i, sizeof(int));
fstr.seekg(0);
fstr.read((char*)arr, sizeof(arr));
if(!fstr)
{
fstr.close();
cout << "Error: read() failed\n";
exit(EXIT_FAILURE);
}
cout << "\n***** File contents *****\n";
for(i = 0; i < 5; i++)
cout << arr[i] << '\n';
fstr.close();
return 0;
}
9. 练习C.24.9
编写一个程序,读取10本书的标题(使用一个100字符的数组),并将它们写入用户选择的二进制文件中。先写入标题的大小,再写入标题。然后,程序应读取一个标题,并显示一条消息,指示该标题是否包含在文件中:
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <string>
#include <cstring>
using namespace std;
int main()
{
bool found;
int i, len;
char title[100], str[100];
string fname;
cout << "Enter file name: ";
cin >> fname;
fstream fstr(fname.c_str(), ios_base::in | ios_base::out | ios_base::trunc | ios_base::binary);
if(fstr.is_open() == false)
{
cout << "Error: is_open() failed\n";
exit(EXIT_FAILURE);
}
cin.get();
for(i = 0; i < 10; i++)
{
cout << "Enter title: ";
cin.getline(title, sizeof(title));
len = strlen(title);
fstr.write((char*)&len, sizeof(int));
fstr.write(title, len);
}
cout << "Enter title to search: ";
cin.getline(str, sizeof(str));
found = 0;
fstr.seekg(0);
while(1)
{
fstr.read((char*)&len, sizeof(int));
if(!fstr)
break;
fstr.read(title, len);
if(!fstr)
break;
title[len] = '\0';
if(strcmp(title, str) == 0)
{
found = 1;
break;
}
}
if(found == 0)
cout << "\nTitle not found\n";
else
cout << "\nTitle found\n";
fstr.close();
return 0;
}
10. 练习C.24.10
假设
test.bin
二进制文件包含一个学生的成绩,文件开头声明了成绩的数量。编写一个程序,从二进制文件中读取成绩(使用
float
类型),并将它们存储在动态分配的内存中。然后,程序应读取一个数字,并显示大于该数字的成绩:
#include <iostream>
#include <fstream>
#include <cstdlib>
using namespace std;
int main()
{
int i, grd_num;
float *grd_arr, grd;
ifstream fin("test.bin", ios_base::in | ios_base::binary);
if(fin.is_open() == false)
{
cout << "Error: is_open() failed\n";
exit(EXIT_FAILURE);
}
fin.read((char*)&grd_num, sizeof(int));
if(!fin)
{
fin.close();
cout << "Error: read() failed\n";
exit(EXIT_FAILURE);
}
grd_arr = new float[grd_num];
fin.read((char*)grd_arr, grd_num * sizeof(float));
if(fin)
{
cout << "Enter grade: ";
cin >> grd;
for(i = 0; i < grd_num; i++)
if(grd_arr[i] > grd)
cout << grd_arr[i] << '\n';
}
else
cout << "Error: read() failed to read grades\n";
delete[] grd_arr;
fin.close();
return 0;
}
11. 练习C.24.11
一种常见的杀毒软件识别病毒的方法是基于签名的检测。签名是一系列字节(如
F3 BA 20 63 7A 1B
),用于识别特定的病毒。编写一个程序,读取一个病毒签名(如五个整数),并检查它是否包含在二进制文件
test.dat
中:
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <cstring>
using namespace std;
int main()
{
const int SIZE = 5;
bool found;
int i, len, back, buf[SIZE], sign[SIZE];
ifstream fin("test.dat", ios_base::in | ios_base::binary);
if(fin.is_open() == false)
{
cout << "Error: is_open() failed\n";
exit(EXIT_FAILURE);
}
cout << "Enter virus signature (" << SIZE << " integers)\n";
for(i = 0; i < SIZE; i++)
{
cout << "Enter number: ";
cin >> sign[i];
}
len = sizeof(sign);
found = 0;
back = len - sizeof(int);
while(1)
{
fin.read((char*)buf, SIZE * sizeof(int));
if(!fin)
break;
if(memcmp(buf, sign, len) == 0)
{
found = 1;
break;
}
else
fin.seekg(-back, ios_base::cur);
}
if(found == 1)
cout << "SOS: Virus found\n";
else
cout << "That virus signature isn't found\n";
fin.close();
return 0;
}
12. 练习C.24.12
定义
Employee
结构体,其成员包括姓名(使用一个100字符的数组)、税号和工资。编写一个程序,使用该类型读取100名员工的数据,并将它们存储在一个这样的结构体数组中。如果用户输入姓名为
fin
,则停止数据插入,并将结构体写入
test.bin
二进制文件:
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <cstring>
using namespace std;
struct Employee
{
char name[100];
int tax_num;
int salary;
};
int main()
{
const int SIZE = 100;
int i, num_empl;
Employee empl[SIZE];
ofstream fout("test.bin", ios_base::out | ios_base::trunc | ios_base::binary);
if(fout.is_open() == false)
{
cout << "Error: is_open() failed\n";
exit(EXIT_FAILURE);
}
num_empl = 0;
for(i = 0; i < SIZE; i++)
{
cout << "\nEnter full name: ";
cin.getline(empl[i].name, sizeof(empl[i].name));
if(strcmp(empl[i].name, "fin") == 0)
break;
cout << "Enter tax number: ";
cin >> empl[i].tax_num;
cout << "Enter salary: ";
cin >> empl[i].salary;
num_empl++;
cin.get();
}
fout.write((char*)empl, num_empl * sizeof(Employee));
fout.close();
return 0;
}
13. 练习C.24.13
假设
test.bin
二进制文件包含上一个练习中定义的
Employee
类型的结构体。编写一个程序,读取这些结构体,并将工资超过用户输入金额的员工数据复制到
data.bin
二进制文件中。程序还应显示存储在
data.bin
文件中的员工的平均工资:
#include <iostream>
#include <fstream>
#include <cstdlib>
using namespace std;
struct Employee
{
char name[100];
int tax_num;
int salary;
};
int main()
{
int count, amount, sum_sal;
Employee tmp_emp;
ifstream fin("test.bin", ios_base::in | ios_base::binary);
if(fin.is_open() == false)
{
cout << "Error: Input file can't be loaded\n";
exit(EXIT_FAILURE);
}
ofstream fout("data.bin", ios_base::out | ios_base::trunc | ios_base::binary);
if(fout.is_open() == false)
{
fin.close();
cout << "Error: Output file can't be created\n";
exit(EXIT_FAILURE);
}
cout << "Enter amount: ";
cin >> amount;
count = sum_sal = 0;
while(1)
{
fin.read((char*)&tmp_emp, sizeof(Employee));
if(!fin)
break;
if(tmp_emp.salary > amount)
{
fout.write((char*)&tmp_emp, sizeof(Employee));
sum_sal += tmp_emp.salary;
count++;
}
}
if(count)
cout << "Avg:" << (float)sum_sal/count << '\n';
else
cout << "No employee gets more than " << amount << '\n';
fin.close();
fout.close();
return 0;
}
未解决的练习
以下是一些未解决的练习,供读者进一步练习和探索:
1. 假设
grades.txt
文本文件包含一些学生的成绩,编写一个程序,读取该文件并显示最好和最差的成绩,以及不及格(成绩 < 5)和及格(成绩 ≥ 5)学生的平均成绩。假设成绩在[0, 10]范围内。
2. 编写一个程序,检查两个用户选择的文本文件是否具有相同的内容。
3. 编写一个程序,在用户选择的文本文件中查找连续的重复单词(如 “In this this chapter we present…”),并将它们写入另一个文本文件。
4. 编写一个程序,从命令行读取两个文本文件的名称,将第二个文件的内容追加到第一个文件中,然后显示第一个文件的内容。
5. 编写一个程序,将用户选择的文本文件中的大写字母转换为小写字母,反之亦然。提示:在连续的读写操作之间调用文件定位函数(如
seekp()
)。
6. 假设
students.txt
文本文件的每行包含一个学生的姓名和三门课程的成绩,编写一个程序,读取该文件并显示平均成绩最好的学生的姓名。如果有多个学生具有相同的最好平均成绩,程序应显示所有这些学生的姓名。
7. 编写一个程序,读取一个文本文件的名称,并将其内容以相反的顺序(即从最后一个字符到第一个字符)复制到用户选择的第二个文本文件中。提示:将第一个文件以二进制模式打开。
8. 编写一个程序,从命令行读取一个文本文件的名称,并显示其最长的行。假设没有多行具有相同的最长长度。程序还应显示文件中每个数字[0, 9]的出现次数。此外,程序应读取一个整数(如
x
),并显示文件的最后
x
行。程序应检查输入的数字不大于总行数。例如,如果用户输入3,程序应显示文件的最后三行。限制条件是程序应仅读取每行一次。
9. 定义
Book
结构体,其成员包括标题、作者和价格。假设
book.dat
二进制文件包含100个这样的结构体。编写一个程序,读取一个书籍条目的序号(如25)和新数据,并用新数据替换现有数据。然后,程序应从文件中读取该条目的数据并显示它们,以验证是否正确写入。
10. 考虑上一个练习中的
book.dat
文件,编写一个程序,读取现有条目,并将它们以相反的顺序写入
book_rvs.dat
二进制文件中,即最后一个条目应首先写入,倒数第二个条目应第二个写入,依此类推。
11. 假设
grades.dat
二进制文件包含一些学生的成绩,文件开头存储了学生的数量。编写一个程序,读取该文件,并将成绩按升序排序后写入
grd_sort.dat
二进制文件中。
12. 凯撒算法是朱利叶斯·凯撒使用的最古老的加密方法之一。根据该算法,每个字符被位于其后三个位置的字符替换。例如,在英文字母表中应用凯撒算法时,消息 “Watch out for Ovelix !!!” 被加密为 “Zdwfk rxw iru Ryhola !!!”。注意,字符
x
被加密为
a
,因为替换从字母表的开头继续。同样,
y
被替换为
b
,
z
被替换为
c
。接收者通过将每个字符替换为位于其前三个位置的字符来解密消息。编写一个程序,提供一个菜单来执行以下操作:
- 文件加密:程序应读取一个文件的名称和用于加密其内容的密钥。例如,在凯撒算法的情况下,密钥是3。程序应仅加密小写和大写字符。
- 文件
C++文件操作全解析
14. 综合应用示例
下面通过一个综合示例,展示如何将前面所学的文件操作知识应用到实际场景中。假设我们要开发一个简单的学生成绩管理系统,该系统可以实现学生信息的录入、成绩的存储、查询和统计等功能。
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <string>
#include <vector>
using namespace std;
// 定义学生结构体
struct Student {
string name;
int id;
float score;
};
// 录入学生信息
void inputStudents(vector<Student>& students) {
int n;
cout << "请输入学生数量: ";
cin >> n;
for (int i = 0; i < n; ++i) {
Student s;
cout << "请输入第 " << i + 1 << " 个学生的姓名: ";
cin >> s.name;
cout << "请输入第 " << i + 1 << " 个学生的学号: ";
cin >> s.id;
cout << "请输入第 " << i + 1 << " 个学生的成绩: ";
cin >> s.score;
students.push_back(s);
}
}
// 将学生信息写入文件
void writeStudentsToFile(const vector<Student>& students, const string& filename) {
ofstream fout(filename, ios_base::binary);
if (!fout) {
cout << "无法打开文件!" << endl;
return;
}
for (const auto& s : students) {
fout.write(reinterpret_cast<const char*>(&s), sizeof(Student));
}
fout.close();
}
// 从文件中读取学生信息
vector<Student> readStudentsFromFile(const string& filename) {
vector<Student> students;
ifstream fin(filename, ios_base::binary);
if (!fin) {
cout << "无法打开文件!" << endl;
return students;
}
Student s;
while (fin.read(reinterpret_cast<char*>(&s), sizeof(Student))) {
students.push_back(s);
}
fin.close();
return students;
}
// 查询学生信息
void queryStudent(const vector<Student>& students) {
int id;
cout << "请输入要查询的学生学号: ";
cin >> id;
for (const auto& s : students) {
if (s.id == id) {
cout << "姓名: " << s.name << ", 学号: " << s.id << ", 成绩: " << s.score << endl;
return;
}
}
cout << "未找到该学生!" << endl;
}
// 统计平均成绩
float calculateAverageScore(const vector<Student>& students) {
if (students.empty()) return 0;
float sum = 0;
for (const auto& s : students) {
sum += s.score;
}
return sum / students.size();
}
int main() {
vector<Student> students;
string filename = "students.dat";
// 录入学生信息
inputStudents(students);
// 将学生信息写入文件
writeStudentsToFile(students, filename);
// 从文件中读取学生信息
vector<Student> readStudents = readStudentsFromFile(filename);
// 查询学生信息
queryStudent(readStudents);
// 统计平均成绩
float averageScore = calculateAverageScore(readStudents);
cout << "学生的平均成绩为: " << averageScore << endl;
return 0;
}
这个示例程序包含了学生信息的录入、写入文件、从文件读取、查询和统计平均成绩等功能。通过这个示例,我们可以看到如何将不同的文件操作函数组合起来,实现一个完整的应用程序。
15. 性能优化建议
在进行文件操作时,性能是一个需要考虑的重要因素。以下是一些性能优化的建议:
-
批量读写
:尽量使用
write()
和
read()
函数进行批量数据的读写,避免频繁的单个字符或小数据块的读写操作,这样可以减少磁盘I/O次数,提高性能。例如:
int arr[1000];
ofstream fout("test.dat", ios_base::out | ios_base::trunc | ios_base::binary);
fout.write((char*)arr, sizeof(arr));
- 合理使用缓冲区 :利用流缓冲区的特性,减少磁盘访问。例如,在写入大量数据时,让缓冲区自动填满后再进行刷新,而不是频繁手动刷新。
-
及时关闭文件
:在不再使用文件时,及时调用
close()函数关闭文件,避免占用系统资源。 - 选择合适的文件打开模式 :根据实际需求选择合适的文件打开模式,避免不必要的文件截断或覆盖操作。
16. 错误处理与异常处理
在文件操作过程中,可能会出现各种错误,如文件打开失败、读写错误等。为了保证程序的健壮性,需要进行错误处理和异常处理。
#include <iostream>
#include <fstream>
#include <cstdlib>
using namespace std;
int main() {
ifstream fin("nonexistent.txt");
if (!fin) {
cerr << "文件打开失败!" << endl;
return EXIT_FAILURE;
}
// 进行文件读取操作
// ...
fin.close();
return 0;
}
在上述代码中,通过检查
ifstream
对象的状态,判断文件是否成功打开。如果打开失败,使用
cerr
输出错误信息,并返回
EXIT_FAILURE
表示程序异常终止。
另外,还可以使用异常处理机制来处理文件操作中的错误。例如:
#include <iostream>
#include <fstream>
#include <stdexcept>
using namespace std;
void readFile(const string& filename) {
ifstream fin(filename);
if (!fin) {
throw runtime_error("文件打开失败!");
}
// 进行文件读取操作
// ...
fin.close();
}
int main() {
try {
readFile("nonexistent.txt");
} catch (const exception& e) {
cerr << "发生异常: " << e.what() << endl;
return EXIT_FAILURE;
}
return 0;
}
在这个示例中,定义了一个
readFile
函数,在函数内部如果文件打开失败,抛出一个
runtime_error
异常。在
main
函数中,使用
try-catch
块捕获异常,并输出错误信息。
17. 总结
通过本文的介绍,我们详细学习了C++中文件操作的相关知识,包括流的概念、流的状态、文本文件和二进制文件的区别、文件的输入输出、流缓冲区、文件打开模式、文件处理方式、文件结束标志、重定向以及读写文本文件和二进制文件的方法等。同时,通过大量的示例代码和练习,我们可以更好地掌握这些知识,并将其应用到实际的编程项目中。
在实际应用中,需要根据具体的需求选择合适的文件操作方式和函数,同时要注意错误处理和性能优化。希望本文能帮助你更好地理解和掌握C++文件操作,提高编程能力。
总结与展望
C++的文件操作功能强大且灵活,能够满足各种不同的应用场景。通过合理运用文件操作知识,可以实现数据的持久化存储、数据的交换和共享等功能。
未来,随着计算机技术的不断发展,文件操作的需求也会越来越多样化和复杂化。例如,在大数据、人工智能等领域,需要处理海量的数据,对文件操作的性能和效率提出了更高的要求。因此,我们需要不断学习和探索新的文件操作技术和方法,以适应不断变化的需求。
同时,我们也可以结合其他编程语言和工具,进一步拓展文件操作的功能和应用范围。例如,使用Python进行数据处理和分析,然后将结果存储到文件中,再使用C++进行高性能的文件读写操作。
总之,掌握C++文件操作是每个C++程序员必备的技能之一,希望大家在今后的学习和工作中,能够灵活运用这些知识,开发出更加优秀的程序。
流程图示例
graph TD;
A[开始] --> B[录入学生信息];
B --> C[将学生信息写入文件];
C --> D[从文件中读取学生信息];
D --> E[查询学生信息];
D --> F[统计平均成绩];
E --> G[结束];
F --> G;
表格总结
| 操作类型 | 相关函数 | 用途 |
|---|---|---|
| 文件打开 |
open()
| 打开文件并关联流对象 |
| 文件关闭 |
close()
| 关闭打开的文件,刷新缓冲区 |
| 写入数据 |
write()
| 向文件写入二进制数据 |
| 读取数据 |
read()
| 从文件读取二进制数据 |
| 定位指针 |
seekg()
、
seekp()
| 移动文件指针到指定位置 |
| 获取指针位置 |
tellg()
、
tellp()
| 获取文件指针的当前位置 |
| 检查流状态 |
fail()
、
eof()
、
bad()
、
good()
| 检查流的状态,判断操作是否成功 |
超级会员免费看

被折叠的 条评论
为什么被折叠?



