3.15. Hello STL 文件篇
使用“成绩管理系统2.0”约一个月,这个月里,李老师的班级进行了大大小小的考试十数次,每次录入成绩之前,都必须重新录入全班学生的基本信息,这让李老师感到疲惫。
在程序运行时,活动数据都生存在内存,把这些数据保存到磁盘上的过程,称为“数据持久化”。当程序退出时,内存数据“灰飞烟灭”也!然而保存在磁盘上的数据, 却可以“投胎转世”,在程序下次运行时,有机会重新被装载到内存。
〖轻松一刻〗:人生即程序?
这天我去丁小明家,他刚好一觉醒来。看到我时,丁小明一把就把我紧紧抱住,像个小孩一样的哭着。我问他怎么啦,他说刚做了个梦,在梦中,他是一段程序代码的化身。他问我:“南老师,这个世界是会不会只是一段内存?而我只不过内存中的一个数据?”
出了丁家的门,我几乎神经错乱:“我是一个人,然后我在写一个程序?或者,我是一个对象,生活在一段程序中?”我一路想着,脸上写着莫名的伤感。
3.15.1. 写文件
丁小明的问题太累人了,相比之下,还是李老师的问题好解决。
在STL中,“文件”被当成一种“流/stream”——事实上我们对“文件”的概念很清楚,那么什么是“流/stream”呢? 其实从第一节课开始,我们就一直在用“流”呢。请看:
cout << “Hello world!” << endl;
这一行代码中 “<<”就是“流操作符”,而cout我们说它是:“标准输出设备”,其实它也是一个“流设备”。这一行代码的作用是将“Hello world!”输出到控制台屏幕上,如果我们有一个“文件流”,那么:
a_file_output_stream << “Hello world!” << endl;
类似这样一行代码,就可以将“Hello world!”及一个换行符输出a_file_output_stream 所绑定的文件里——就这么简单!
请新建一个控制台应用项目,命名为“HelloFileStream”。打开main.cpp文件,完成以下代码:
#include <iostream>
002 #include <fstream>
using namespace std;
int main()
{
008 ofstream ofs;
010 ofs.open("c://hello_file_stream.txt");
012 ofs << "Hello world!" << endl;
014 ofs.close();
return 0;
}
编译、运行,然后到C盘的根目录下,你可以找到一个文件名为“hello_file_stream.txt”,双击打开后,看到以下内容:
(图 44 输出到文件的内容)
002 行,加入了包含文件流定义的头文件。文件名中“f”表示“file/文件”。
008行,定义了一个对象,类型为“ofstream”。类型名中“o”表示“output/输出”。
正如控制台区分“标准输出/cout”和“标准输入/cin”一样,文件也可以区分“输出文件流”和“输入文件流”。“输出”意味着我们要“写”一个文件;而“输入”意味着我们要从一个文件中“读取”内容。本例中,我们要新建一个文件,然后写一行话。
010行,我们调用ofstream的成员函数,“open/打开”指定的名字的文件。事实上第一次运行本程序时,你的C盘上并不存在该文件,“输出文件流/ofstream”,默认情况下,会自动创建不存在的文件。
〖危险〗: C++中如何表示文件路径
C++中通过字符串表达的文件路径的方法,与程序所运行的操作系统保持一致,在Linux下类似“/usr/yourdir/yourfile”,完全一致。在Windows要复杂一些。
Windows的表达方法是“X:/yourdir/yourfile.ext”。其中“X:/”是Windows中独有“盘符”。不过,由于“/”在C++字符串有另外的特定用途,所以C++规定,使用“//”表示“/”,如果你忘了这一点,程序往往得到混乱的结果。
还好,windows倒也从善如流,在多数情况下,也支持采用“/”来表示路径了,所以,样例中的010行代码,在windows下也允许写成:
ofs.open("c:/hello_file_stream.txt"); //改用 ‘/’,而不是烦人的’//’
012行,完成输出。
014行关闭文件流。理论上,确保输出的内容被真正写到磁盘上。
〖小提示〗:清空缓冲区
读写磁盘文件,相比内存操作,性能至少弱了10倍。因此,为了提高性能,文件操作在操作系统和C++库中,都设计缓存区,即写文件时,会首先写到内存中,等达到一定分量了,再一次性写入磁盘。而读文件时,则会首先读出一大段内容到内存中。
ofstream提供函数flash():用以强迫将缓存区数据写入磁盘,你可以把它当成是输入流中的“sync()”函数理解。不过,在当文件流在close()时,会保证自动调用flash()操作。
3.15.2. 读文件
继续前例代码,现在main函数内容如下:
int main()
{
ofstream ofs;
ofs.open("c://hello_file_stream.txt");
ofs << "Hello world!" << endl;
ofs.close();
016 ifstream ifs;
018 ifs.open("c://hello_file_stream.txt");
020 if (!ifs)
{
cout << "open file fail!" << endl;
}
024 else
{
string line;
028 getline(ifs, line);
030 cout << line << endl;
}
return 0;
}
016行,我们声明了一个文件流对象ifs,不过这回它的类型是“ifstream”,其中“i”代表“input/输入”。我们将这个文件中“获得输入”。
018行,ifs尝试打开前面输出的文件。这回如果指定一个并不存在的文件,ifstream可不会为我们自动创建那个文件——因为那样没有意义,你打开它能读什么内容。
020行正是用来判断ifs打开文件时是否失败了,如果失败,我们往屏幕上输出一行提示:“open file fail!”。
028行位于文件正确打开的分支中。你非常熟悉“getline”的函数,不是吗?相比以前,“cin”被换成文件流“ifs”,所以,以前我们是从控制台读取通过用户键盘输入的内容,而这一次,我们从指定的文件中读取内容。
030行在屏幕上输出前面从文件读出的内容,不用猜了,它肯定是“Hello world!”。
〖课堂作业〗:完成文件输入流样例项目
1、请完整实现本项目,编译、并运行。确保结果正确。
2、完成上一步。请修改018行代码中的文件名,使其代表一个实际不存在的文件,然后重新编译、运行程序,观察输出。
3.15.3. 带格式读取
假设我们有三个数字:9, 10, 11,我们想把它们写到文件,该如何实现呢?有了前面的知识,似乎可以直接给出答案:
//…
ofs << 9 << 10 << 11 << endl; //连续输出 三个数:9,10,11
//…
但问题就在此时发生,当我们用记事本打开刚刚所写的文件,你会看到文件中保存这样一个数字:91011。不信我们再写一段代码用于读出这个数,并且输出到屏幕:
//…
int number;
ifs >> number;
cout << number; // 91011
//...
怎么解决问题?似乎也不难,输出时,每个数字之间加一个分隔符即可。假设我们用逗号(注意:必须是半角英文字符)分隔。
//…
ofs << 9 << ‘,’ << 10 << ‘,’ << 11 << endl;
//...
现在输出到文件中的内容是:“9,10,11”。对应的,读取的时候,我们要跳过中间的两中逗号。输入流提供了这样一个函数:ignore(),它默认可以跳过一个字符。
//…
int n1, n2, n3; //直接定义三个整形变量
ifs >> n1;
ifs. ignore(); //跳过第一个逗号
ifs >> n2;
ifs. ignore(); //跳过第二个逗号
ifs >> n3;
cout << n1 << “, ” << n2 << “, ” << n3 << endl; //9, 10, 11
//…
问题虽然解决了,但你会使发现,有关“读”的代码变得有些繁琐,C++为此提供了两个方向的解决办法:
其一、允许我们保留对‘,’的偏爱,但我们必须采用自定义的“流操控函数”实现。这是一个高级方法,我们留待以后学习。
其二、山不转水转,改用空格作为数字间的分隔符。这正是我们今天要学习的。请看新的示例代码:
//…
ofs << 9 << ‘ ’ << 10 << ‘ ’ << 11 << endl;
//…
注意,单引号中间是一个半角空格。此时输出到文件的内容是:“9 10 11”。
然后,读代码为:
//…
int n1, n2, n3;
ifs >> n1 >> n2 >> n3; //一行代码读入!
cout << n1 << “, ” << n2 << “, ” << n3 << endl;
//…
并不是空格有比逗号神奇的本事,而是因为C++的输入流,在“有格式”的读操作中,默认可以忽略空格、缩进符(用’/t’表示)、换行符(即<< endl 的输出内容);因此,输出文件时,改用以下代码也可以工作:
//…
ofs << 9 << endl << 10 << endl << 11 << endl;
//…
结果是每个数字输出成单独一行。读代码无须变化。
这种行为,仅对通过“>>”来输入时有效。完整的表达就是:当使用“流输入操作符 >>”来读取一个有既定的格式的内容(比如数字、字符串、单个字符)时,流将自动跳过之前的连续空格符、缩进符、换行符,并且在遇到下一个前述字符时,自动结束读取。这种行为被称为:“带格式读取/Formatted Input”。而其它另外一些输入行为,比如我们常用到的“getline()”,则被称为:“无格式读取/Unformatted Input”。
带格式读取的行为,是一种默认行为,必要时,我们仍然可以取消这种行为,我们将在以后再学习此项内容。
〖课堂作业〗:练习带格式读取操作
请新建一个控制台项目:“HelloSTLFileStreamFormattedInput”,用以完成本小节的所有示例。
3.15.4. 实例:成绩管理系统3.0
“文件读写”技术可以让我们的“成绩管理系统”增加非常多强大的功能,比如:保存学生基本信息、保存某次考试成绩。保存排名成绩,以及读入各类已存信息的对应功能。不过为了节省篇幅,特别是为了不使问题复杂化,我们将只实现对“学生基本信息”的文件读写功能。
新建控制台应用项目,命名为“HelloSTL_ScoreManage_Ver3”。打开项目内main.cpp文件,确保它的文件编码为“系统默认”;接着,打开前一版本“HelloSTL_ScoreManage_Ver2”的main.cpp文件,复制后者的全部内容到前者,然后关闭前一版本的 main.cpp文件。
为了证明你的操作正确,请立即编译,运行,现在我们应该得到一个和Ver2功能完全一样的管理系统。
-
- 包含头文件:
#include <iostream>
#include <list>
#include <vector>
#include <string>
#include <algorithm>
006 #include <fstream> //增加本行
-
- 增加成员函数
/学生成绩管理类
class StudentScoreManager
{
public:
void InputStudents(); //录入学生基本信息(录入前自动清空原有数据)
030 void SaveStudents() const; // 保存学生基体信息到文件
031 void LoadStudents(); //从文件中读入学生基本信息。
//后面代码略......
};
请考虑,为什么SaveStudent是一个常量成员函数,而LoadStudents不是。
-
- SaveStudents
在原有InputStudents()函数的代码之后,插入SaveStudents的实现:
//保存学生基本信息到特定的文件中:
void StudentScoreManager::SaveStudents() const
{
ofstream ofs;
118 ofs.open(".//students_base_info.txt");
120 if (!ofs)
{
cout << "打开成绩输出文件失败!" << endl;
return;
}
//保存学员个数,方便于后面的读文件过程
127 unsigned int count = students.size();
ofs << count << endl;
for (unsigned int i=0; i<count; ++i)
{
ofs << students[i].number << endl;
ofs << students[i].name << endl;
}
ofs.close();
138 cout << "保存完毕,共保存" << count << "位学生基本信息。" << endl;
}
118行的".//students_base_info.txt",使用到了“./”。在Windows中,“./”表示当前路径。如果你不了解路径知识,或许事后你需要去补充一下这方面的知识。
〖危险〗:程序文件所在路径,不一定就是当前路径
在Code::Blocks中,通过一个名为 “HelloSTL_ScoreManage_Ver3”的项目,所生成的“调试版”可执行文件文件:
“项目父文件夹/HelloSTL_ScoreManage_Ver3/bin/Debug/ HelloSTL_ScoreManage_Ver3.exe”。
然而,当我们在Code::Blocks IDE环境内运行该程序时,它的运行路径,将位于:
“项目父文件夹/HelloSTL_ScoreManage_Ver3/”。
也就是,在本例中,文件“./students_base_info.txt”被生成后,其完整路径其实为:
“项目父文件夹/HelloSTL_ScoreManage_Ver3/ students_base_info.txt”。
120行,这次我们对输出文件也做了是否正确打开的判断,特殊情况下,比如您把项目创建在一个U盘后,很不凑巧,U盘没有磁盘空间了,或者被您临时加上了磁盘写锁开关……总之,小心行得万年船。
127行特意将当前学生个数,写到文件,写完个数,才一个个地输出学生基本信息。这是一个常用的读写数据的技巧。
138行仅用于给出一个稍微友好一点的提示,告诉用户操作完成了。
-
- LoadStudents
//从特定的文件中,读入学生基本信息:
void StudentScoreManager::LoadStudents()
{
ifstream ifs;
ifs.open(".//students_base_info.txt");
if (!ifs)
{
cout << "打开成绩输入文件失败!" << endl;
return;
}
153 students.clear(); //清除原来的学生数据
unsigned int count = 0;
156 ifs >> count; //读入个数
158 for (unsigned int i=0; i<count; ++i)
{
Student stu;
162 ifs >> stu.number;
164 ifs.ignore(); //替后续的getline跳过:学号之后的换行符
165 getline(ifs, stu.name); //读入姓名
students.push_back(stu); //加入
}
cout << "加载完毕,共加载:" << count << "位学生的基本信息。" << endl;
}
153行是一项重要的逻辑,如果不清除原有数据,那么多执行两次“LoadStudents”,学生的信息就会出现重复。
156行,我们读入文件中所保存的学生个数,然后在158行的for循环中,方便地用上这个数目。(当然,这里忽略安全问题:比如文件被恶意篡改)。
由于学号和姓名在保存时,采用换行分隔,所以当162行完成读取学号之后,会留下一个换行符,此时如果直接用getline来读入姓名,由于getline 是一个“Unformatted Input”操作,所以它不懂得跳过那个换行符,从而读到一个空行。所以,我们在164行,调用ignore()来跳过换行符。
〖小提示〗:用什么方式来读取人名?
如果我们也采用“Formatted Input”来读取姓名,则代码可以简单一些:
ifs >> stu.number;
ifs >> stu.name;
然而,这样做却带来另一个问题:姓名中间不允许带空格了。中国人名没问题,但外国人名书写时,就肯定在中间带一个空格了。这是一个需求问题,还是打个电话给李老师,问问学校里有没有外国小朋友吧。
-
- 修改 Menu函数
两个重要的函数实现了。接下来就是在菜单中增加入口了。为了方便使用,建议把二者安排为“8”和“9”号功能。8,9号本是“关于”和“帮助”,现在它们顺延为“10”和“11”。
int Menu()
{
cout << "---------------------------" << endl;
497 cout << "----学生成绩管理系统 Ver3.0----" << endl;
cout << "---------------------------" << endl;
cout << "请选择:(0~1)" << endl;
//此处略去1~7号原有菜单项
514 cout << "---------------------------" << endl;
515 cout << "8--#加载学生基本信息" << endl;
516 cout << "9--#保存学生基本文件" << endl;
cout << "---------------------------" << endl;
cout << "10--#关于" << endl;
cout << "11--#帮助" << endl;
cout << "---------------------------" << endl;
cout << "0--#退出" << endl;
int sel;
cin >> sel;
if (CheckInputFail())
{
return -1;
}
cin.sync(); //清掉输入数字之后的 回车键
return sel;
}
-
- 修改main函数
nt main()
{
StudentScoreManager ssm;
while(true)
{
int sel = Menu();
if (sel == 1)
{
ssm.InputStudents();
}
//略去部分代码...直接跳到8号功能
else if (sel == 8)
{
576 ssm.LoadStudents();
}
else if (sel == 9)
{
580 ssm.SaveStudents();
}
else if (sel == 10)
{
About();
}
else if (sel == 11)
{
Help();
}
else if (sel == 0)
{
break;
}
else //什么也不是..
{
596 cout << "请正确输入选择:范围在 0 ~ 11 之内。" << endl;
}
system("Pause");
}
cout << "bye~bye~" << endl;
return 0;
}
-
- 其它修改
Help、About函数的修改内容不影响本程序运行逻辑,请自行实现——如果一定要让我给个提示——那么请别忘了,现在是“成绩管理系统”3.0版本,如果一定要给这个3.0版本加一个时限,那么我希望是越短越好。
让我们快一点,再快一点开始学习“图形用户界面”编程吧!这样 ,我们就可以推出4.0版的“成绩管理系统”了!