第3章 感受(一)——3.14. Hello STL 文件篇

来源:互联网 发布:52单片机论坛 编辑:程序博客网 时间:2024/05/16 17:48
 
[回到目录]
白话C++

3.15. Hello STL 文件篇

使用“成绩管理系统2.0”约一个月,这个月里,李老师的班级进行了大大小小的考试十数次,每次录入成绩之前,都必须重新录入全班学生的基本信息,这让李老师感到疲惫。

在程序运行时,活动数据都生存在内存,把这些数据保存到磁盘上的过程,称为“数据持久化”。当程序退出时,内存数据“灰飞烟灭”也!然而保存在磁盘上的数据, 却可以“投胎转世”,在程序下次运行时,有机会重新被装载到内存。

 

qsyk〖轻松一刻〗:人生即程序?

这天我去丁小明家,他刚好一觉醒来。看到我时,丁小明一把就把我紧紧抱住,像个小孩一样的哭着。我问他怎么啦,他说刚做了个梦,在梦中,他是一段程序代码的化身。他问我:“南老师,这个世界是会不会只是一段内存?而我只不过内存中的一个数据?”

出了丁家的门,我几乎神经错乱:“我是一个人,然后我在写一个程序?或者,我是一个对象,生活在一段程序中?”我一路想着,脸上写着莫名的伤感。

 

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”,双击打开后,看到以下内容:

open txt file

(图 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!”。

 

zy〖课堂作业〗:完成文件输入流样例项目

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”。

带格式读取的行为,是一种默认行为,必要时,我们仍然可以取消这种行为,我们将在以后再学习此项内容。

 

xczy〖课堂作业〗:练习带格式读取操作

请新建一个控制台项目:“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()来跳过换行符。

 

hint〖小提示〗:用什么方式来读取人名?

如果我们也采用“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版的“成绩管理系统”了!


白话C++
[回到目录]

原创粉丝点击