分享

《白话C++》第三章《感受》(一)3.13.Hello STL 列表篇

 梦月0320 2010-08-27

3.13. Hello STL 列表篇

vector是一种容器。我们还知道,vector其实是一个“类模板”,具体使用前,必须通过以下语法指定将要存储的元素类型:

vector<元素类型>

 

这就是决定了vector是一个“通用”的容器,如果元素类型是“Beauty”,那么它就是一个装美女的容器,如果元素类型是“Money”,那么它就是装钱的容器。

“列表/list” 也是一种容器,同样,list也是一个“类模板”,具体使用前,必须通过以下语法指定将要存储的元素类型:

list <元素类型>

 

看来list也是一种通用容器——或许你想问:既生vector,何生list呢?

 

3.13.1. vector VS. list

最简单的回答是:list和vector的结构不同,因此决定了它们在能力分工上的不同。且先说说生活中的容器。桶是圆柱体,柜子是立方体,这就叫“结构不同的容器”。在设计上,圆柱体的桶,有利于用最少的材质,制造出最大容量的桶,而柜子正好相反,如果家里所有衣柜、书柜等等全部是圆柱体,那么它们将凭空浪费掉屋子的很多空间。

因此,虽然容器的功能粗一看就是“存放元素”,但是,由于要存放的元素自身结构不同,或者外部对容器的遍历方式不同等原因,我们不得不需要有多种“功能倾向”明显不同的容器。比如vector和list。

vector最大的结构特点是:它开辟连续的内存空间用以存储元素。而list正好相反,它允许使用不连续内存空间。

假设你想在热闹的市区盖5间房子,有两种方案,第一种方案把5间房连续盖在一起,这会让你生活很方便,但你需要一大块地皮,可能市政府不会满足你,为了你这一块地,可能要拆牵别人许多房;第二种方案你同意把五间房分开盖,这会给生活带来一定不便,但现在你只需在市区见缝插针地找到5块小地皮,就可以动工了。

第一种方案正是vector分配内存的策略,第二种方案则是list分配内存的策略。由此,我们可以得出第一个“猜想”:如果我们有一大堆对象,并且这些对象个头都很大,那么我们就必须考虑是否放弃采用vector直接存储?

作为代替方案,有两种,其一是仍然采用vector存储,但存放的元素改成是“堆对象”,也就是说将每个对象创建在“堆内存区”中,而容器中仅仅保存对象在“堆内存”中的地址。这个方案不足之处是:由于“堆对象”我们必须手工“杀死它们”,所以会带来额外的内存管理工作。其二,我们可以采用list容器进行管理。list允许我们仍然将数据保存在“栈内存区”中,不过必须将每个元素分开保存,从而有利于分配出足够的内存。

当然,“内存分配”并不是我们考虑是否采用list的唯一因素——如果非要找一个vector的代替方案,STL中的deque容器可能更适合——更为重要的考虑对元素的访问,包括读取元素、添加元素、插入元素、删除元素的方式,及其代价。

比如“插入元素”:在vector模式下:你有三间紧密盖在一起的房子,现在你想在第1、2间房子中插入一间新房子——老天,这在现实中几乎难以实现。听说有一种“楼房平移”技术,允许在地基下安装轮子,然后以极慢的,比如一天1厘米的速度,将楼房不知不觉地挪开数米……

vector采用连续内存保存元素,因此当需要在中间插入元素时,同样需要将插入点之后的所有元素往后“平移”,有时甚至需要将整个内存摧毁重建。类似的,在vector中删除元素的代价也非常之大。由此我们得出第二个“猜想”:如果你在存储对象的同时,还需要经常插入、删除位于中部的元素,那么采用vector也是不合适的。这时候又可以考虑list,因为list中的元素,东一个西一个,元素之间存在不是“物理”的,而是“逻辑”上的次序关系。要在所谓“中间位置”插入一个元素,易如反掌。

“猜想”到此结束,还是直接了解一下list的结构吧。

 

3.13.2. 基础

list_struct

(图 39 list 内存结构示意)

  • 每个方格表示一块内存区域,其中黑色部分表示已经被其它数据占用的内存块(钉子户)。
  • A、B、C三个元素依次加入list中,分别“见缝插针”地占用三块地盘,从图示可看,这三块内存并不连续,甚至连次序也没有保证。
  • 每个数据加入list时,不仅要占用自身需要的影子,而且要额外占用两块小内存,分别用来保存“前一元素的地址”和“后一元素的地址”,即图示中的“前”、“后”。
  • 图中箭头表示:A的后一元素是B,B的后一元素是C;C的前一元素是B;B的前一元素是A。
  • A没有前一元素,C没有后一元素,但为了使用方便,分别将它们的“前一元素地址”和“后一元素地址”设置一个虚拟值(具体实现方式并无标准)。访问这两个虚拟值,将会导致未定义的行为。

现在,假设B左边的那位“钉子户”搬走,而我们正好在此时在A和B之间插入A’元素,这引起的变化是什么呢?

list_struct2

(图 40 A与B之间插入A''元素)

  • 原有的A、B、C三个元素不必“搬家”,仍然位于原来的位置。换句话说,往list中插入新元素(删除也一样),不需要移动任何原有的元素。所以速度很快。
  • 变化的只是A、B中“前一元素地址”和“后一元素地址”的值,在上图用箭头表示,即:A的“后一元素”改为指向A’,B的“前一元素”改为指向A’。而A’则把“前一元素”指向A,“后一元素”指向B。

list可以实现高速地插入、删除元素,这一点让vector望尘莫及。vector仅当在尾部“追加”元素时,才有可能因为不必迁移前面元素而获得较高速度。然而,list却没有vector的“随机访问”能力。我们无法采用“[N]”来直接访问list中第N个元素,相反,我们只能直接访问到第一个,或最后一个元素,然后依据“后一元素地址”,不断往后, 或者依据“前一元素地址”不断往前,从而遍历每个元素。

如果你不太理解,那么想象一队士兵,我们被规定只允许“接触”正副班长,但却要将球传给中间的某个士兵时该如何办?(提示:一队士兵中,通常班长排头,副班长压尾)。

3.13.3. 迭代器/iterator概念

由于在访问某一元素时,必须同时得到“前一元素地址”和“后一元素地址”等相关信息,因此,当我们在访问list时,并不是直接得到当初存入list的裸数据(如上图中的A、B、C),而是得到另外一种类型的数据,称为“迭代器/Iterator”。这不仅仅是list的策略,而是STL中常规容器共有的特点。对于list,“迭代器”包装了裸数据,同时又新增了用于前往“前一元素”和“后一元素”的信息。

有关迭代器的实现,需要触及许多们尚未学习的C++知识。在今天,你暂时可以把迭代器当作是对“裸数据”的一层封装,比如一个list<Beauty>的迭代器,暂时可以理解为这样一个结构:

struct iterator
            {
            Beauty *ptr; //指向我们保存的“美女”元素
               iterator* next; //后一个美女的位置
               iterator* prior; //前一个美女的位置
            };
            

依据这个简单的迭代器结构,假设我们现在拥有一个iterator的对象,名为 cur。那么,我们可以这样的操作:

1.访问到当前“美女”的名字:

cur.ptr->GetName();

2. 前进到后一个“美女”元素:

cur = cur.next;

3. 后退到前一个“美女”元素:

cur = cur.prior;

 

真实的STL迭代器内部实现要比上面的例子,复杂得多——作为回报——它的对外的接口更加直观了:

1. 访问到当前“美女”的名字:

cur->GetName();

2. 前进到后一个“美女”元素:

++cur; //或者:cur++;

3. 后退到前一个“美女”元素:

--cur; //或者:cur--;

 

hint〖小提示〗:迭代器特定接口的实现原理

和vector提供的“[]”一样,“++、- -、->、*”也是操作符,同样可以通过“操作符重载”技术,使其与“函数”类型的方式,提供特定的功能。

普通迭代器的类型名称为:iterator。不过它总是定义在具体的容器类中。对于list<Beauty>。我们可以认为存在这样一个类型:

struct list< Beauty >
            {
            //…
            };
            

而它的迭代器类型的定义,总是嵌套其中:

struct list< Beauty >
            {
            struct iteraotr
            {
            //…
              };
            //…
            };
            

回忆一下,我们经常可以把一个类也当作一个名字空间(namespace),所以,list<int>容器类型的迭代器的类型完整名称即是:list< Beauty >::iterator——还记得“德国小蠊::小强”吗?

定义一个list<Beauty>的迭代器的变量,代码为:

list<Beauty>::iterator iter;

由于list的迭代器既可以前进(++操作),又可以后退(--操作),因此,我们称之为“双向迭代器”。

list的不少常用函数,都会涉及到迭代器,比如返回值是一个迭代器,或者参数是一个迭代器。我们先了解和迭代器无关的常用函数。

 

3.13.4. 常用函数(一)

请新建一个控制台应用项目,命名为“HelloSTLVector”。打开项目唯一的源文件:main.cpp。如果你觉得必要,请确保修改文件编码为“系统默认”。

在002行加入包含<list>头文件的代码:

001 #include <iostream>
            002 #include <list>
            

在main函数最前面加入一行定义,以产生一个“专门用于存储整数的list”的变量,变量名为“lst”。(第1个字符是字母“l”还是数字“1”?你猜得出的,不是吗?)

006 int main()
            {
            008    list<int> lst;
            ......
            

请在当前代码的基础上,请一边阅读以下课程,一边在项目中完成相应的代码。

  • push_front(elem)

在list头部插入一个元素。

lst.push_front(10);

lst.push_front(20);

现在,lst中的元素是:20、10。

  • push_back(elem)

在list尾部插入一个元素。

lst.push_back(8);

lst.push_back(9);

现在,list中的元素是:20、10、8、9。

  • pop_front( )

删除将list第一个元素。

lst.pop_front();

现在,lst中的元素是:10、8、9。没错,第一个元素被丢弃了。

pop_front()仅仅是“丢”掉第一个元素,并不将第一个元素返回给函数的调用者。

  • pop_back()

删除list最后一个元素。

lst.pop_back();

现在,lst中的元素是:10、8。

 

〖课堂作业〗:理解pop_front和pop_back

以下代码片段错在哪里:

list<int> lst;

lst.push_back(1);

lst.push_back(2);

int a = lst.pop_front();

int b = lst.pop_back();

 

请对比随后的front()与back()函数,二者允许我们得到(但不丢弃)第一个元素或最后一个元素。

  • clear()

删除容器中所有元素。

lst.clear();

现在lst中一个元素也没有。

  • front( ) const

返回第一个元素(“裸元素”,而非迭代器)。

lst.push_back(1);

lst.push_back(2);

int a = lst.front();

cout << a << endl;

a的值是1。同时lst中的元素现在是:1、2。

front()并不影响容器内部的任何数据,所以它是一个“常量成员函数”。

  • back() const

返回最后一个元素(“裸元素”,而非迭代器)。

int b = lst.back();

cout << b << endl;

b的值是2。

和front()一样,back()同样直接返回容器中的元素,而不是迭代器。本例与前例中,lst的类型是:list<int>,所以a和b都被定义成int类型。

  • size() const

返回容器中元素的个数。

int count = lst.size();

cout << count << endl;

count值为2。

  • empty() const

判断容器是否为空(元素个数为0)。

如果仅关心容器是否为空,请调用此函数以获得更好性能,而不要通过:“size() == 0” 判断。

cout << lst.empty() << endl;

lst.clear();

cout << lst.empty() << endl;

第一行输出0;第二行输出1。

 

〖小提示〗:输出bool类型的值

默认状态下,cout将bool值视为整数处理,值false对应0,值true对应1。

 

3.13.5. 常用函数(二)

  • begin( ) 和end( )

begin()函数返回list第一个元素的迭代器。

需要特别注意的是:end() 返回的是最后一个元素的“下一个”迭代器。假设list有3个元素,那么end()返回了第4个元素的迭代器,第4个元素并不真实存在,它是个“虚”的元素(请参看“list 内存结构示意图”)。

得到lst的第一个迭代器代码如下:

list<int>::iterator iter = lst.begin();

有了一个迭代器,如果通过它得到对应的元素呢?方法很简单——操作一个“迭代器”和操作堆对象非常类似——采用“*”操作符。

int a = *iter;

我们也可以通过迭代器来修改对应的元素:

*iter = 1000;

我们来一个相对完整的代码片段,演示如何通过一个迭代器读取与修改对应的元素。

list <int> lst;
            lst.push_back(1);
            lst.push_back(2);
            list<int>::iterator  iter = lst.begin();
            int a = *iter;
            cout << a << endl; //输出 1
            cout << *iter << endl; //同样输出1
            *iter = 1000;
            cout << *iter << endl; //输出1000
            cout << a << endl; //输出1;
            int b = *iter;
            cout << b << endl; //输出1000;
            

光说begin()函数了,end()函数是不是也可以有相同操作呢?

lst<int>::iterator iter2 = lst.end();
            int a = *iter2; //灾难发生
            *iter2 = 100;   //灾难发生
            

因为end()返回的迭代器绑定到一个“虚拟”的元素。元素总是保存在内存中,所谓“虚拟”的元素,可能是一块“并不真实存在的内块”,也可能是指一块“并不属于你的内存”——还不理解吗?来听一个故事吧。

 

〖轻松一刻〗:丁小明家的存款单

有一次,丁小明兴冲冲地给我一张1000万元的存款单,我拿了直奔银行要求取现,结果那天我鼻青脸肿地回来了。

后来我去丁家,正遇上他家老婆蹲在地上,面前一张板凳,板凳上一个诡异纸盒,纸盒内一撂单据。他家老婆神情兴奋地数着:“开始!50元、120元、75元5角、60元2角7分……1000万?结束。”我不耻地骂了一声:“虚伪!”。丁家老婆腼腆地冲我家一笑,盖上了纸盒。我才发现纸盒上写了几行字:

产地:C++标准委员会

结构:std::list 类型: std::list<存款单>

我恶狠狠地抢过这个纸盒,翻成底朝天——果然,这个盒子两边都有盖!我恼怒地打开后面的盖,天!那张千万元的单据,居然自动跑到另一边去了!

  • rbegin() / rend()

rbegin() 是list中最后一个元素的迭代器。而rend()同样需要引起你的注意:它是第一个元素之前的那个虚拟元素的迭代器。

rbegin()、rend()返回的迭代器,和begin()/end()返回类型并不相同,前者称为“逆向迭代器”,具体类型是:list<元素类型>:: reverse_iterator。

  • insert (pos, elem)

在指定位置插入一个新元素。pos并不是一个用来表示元素位置的整数,而是一个iterator(并且不能是reverse_iterator),表示在指定迭代器的前面,插入新元素。

lst.clear();
            lst.push_back(10);
            lst.push_back(100);
            list<int>::iterator  iter = lst.begin();
            ++iter; //iter前进1步,指向第二个元素
            lst.insert(iter, 1); //在第二个元素的位置上,插入新元素
            

现在,lst中的元素是:10、1、100。

  • erase(pos)

从容器中删除pos位置对应的元素。pos同样是一个迭代器(但不能是reverse_iterator)。

lst.clear();
            lst.push_back(10);
            lst.push_back(100);
            list<int>::iterator  iter = lst.begin();
            ++iter;
            lst.erase(iter);
            

现在lst中只有一个元素:10。

 

3.13.6. 常量迭代器

迭代器可以“访问”到容器中的每个元素,“访问”方法即可以是“读取”,也可以“修改”。和“常量成员函数”的思路一样,为了更好的“封装”效果,STL提供了一种“只读迭代器”,类名为“const_iterator”及反向版“const_ reverse_iterator”。对应的,“begin()/end()、rbegin()/rend()”也分别有一个“常量版”。

list<int>::const_iterator  c_iter = lst.begin();  //此时调用的是常量版
            int a = *c_iter; //正确
            *c_iter = 1000;  //错误!c_iter是只读版迭代器,不允许修改它绑定的元素
            

 

〖课堂作业〗:lst常用函数汇总

请将“常用函数(一)”“常用函数(二)”、“常量迭代器”三小节中的所有示例代码,合并到工程的main.cpp源文件中,并确保没有编译错误。

 

3.13.7. 遍历list容器

list容器不允许“随机”访问,不过,我们可以得到第一个元素的 “迭代器”,然后通过“迭代器”不断前进,从而访问到每一个元素。

下面的代码,实现将lst中的每一个整数,输出到屏幕上。

001 for (list<int>::const_iterator c_iter = lst.begin();
            002       c_iter != lst.end();
            003       ++c_iter)
            {
            005    cout << *c_iter << endl;
            }
            

for循环头被我故意折成三行,因为它看起来有点长,但其实仍然是这样三部分:

001行是“初始化语句”。定义了一个“只读迭代器”的变量:c_iter,它指向lst的第一个元素。

002行是“循环条件判断”语句。“!=”是C++中用以实现“不等”判断的操作符。本循环不断进行的条件就是:c_iter不等于lst的“结束迭代器”。

003行,c_iter通过++操作,实现前进到下一元素的位置上。

如果你对这三行感觉得有些不太理解,那么你需复习一下3.12.3 小节有关如何遍历vector的内容。

005行,输出c_iter当前指向的元素的值。

由于list拥有“双向迭代器”,所以这个次序也可以倒过来,改成从最后一个元素开始,“倒着”走向第一个元素。强调一点,对于一个逆向迭代器/ reverse_iterator,对它进行“++”操作,就是促使它从容器的尾部往头部“前进”一个位置,而对其进行“--”操作,则是促使它从容器的头部往“后退”一个位置。

001 for (list<int>::const_reverse_iterator c_iter = lst.rbegin();
            002     c_iter  !=  (list<int>::const_reverse_iterator) lst.rend();
            ++c_iter)
            {
            cout << *c_iter << endl;
            }
            

 

〖危险〗:gcc的一个BUG

事实上,002行中的加粗部分,本不必要。然而gcc 4.0(含4.0)以下版本存在错误,我们不得不加上该段代码,用于强制告知编译器我们要的是一个“const_reverse_iterator”,而非“reverse_iterator”。

4.1以上版本的gcc不存在该问题,然而Code::Blocks自带的gcc是mingw-gcc3.4.5。

 

变化发生在001行和002行。你喜欢玩“找碴”游戏吗?仔细找找两段代码之间的不同。

〖课堂作业〗:两种方向遍历list

请在上次课堂作业的基础上,加入本小节两种方向遍历list的练习代码。

 

3.13.8. 实例:成绩管理系统1.0

〖轻松一刻〗:李老师的出现

夜。

风雨夜。

白发苍苍的老者,敲开丁家大门。

来者姓李,20年前丁小明的小学老师。

他带着厚厚的一摞试卷……

他想做什么?

 

  • 需求与基本思路

自打小丁完成了“美女大赛管理系统”之后,他就一直在想写一个更有意义的软件。

现在需求来了:老师希望能够按照试卷的次序,录入学生成绩,然后依据学号的次序,输出学生成绩。小丁简单地翻了一下试卷,发现一个事实:试卷的次序和学号无关。

李老师说,录入时,需要输入学号与成绩;输出时,最好能同时显示学号与姓名。

小丁的思路是:分成两个struct,其一包含学号、姓名;其二包含学号、成绩。二者之间通过“学号”构成逻辑关联:

//学生
            struct Student
            {
            unsigned int number; //学号
              string name;
            };
            //成绩
            struct Score
            {
            unsigned int number; //学号
               float mark; //分数
            };
            

学号采用“无符号整数”(0和正整数)类型,因为学号不允许为负数,为0则有特殊用处。分数采用“float”类型,因为存在像86.5分这样带小数的成绩。

 

〖重要〗:减少信息过度偶合

小丁的思路是正确的。表面看,“学生”可以拥有一或多个“成绩”,但“拥有”并不一定适合在类中加入一个相关的成员数据。“成绩”在很多时候,可以暂时脱离“学生”而独立进行运算,比如本例的“录入成绩”。这种情况下,相关信息独立定义,通过一个“关键值”来维系两者的关系是个好主意。本例中,这个关键值就是“学号”。

再考虑一个更为极端的例子:“学生”有“家长”,“家长”有“工作单位”、“工作单位”有“法人代表”。显然,在“学生”结构中含有“法人代表”很不合理,这就称为信息之间的过度偶合,在编程设计中,应该避免。

 

接着,小丁决定使用vector来保存班级里的学生。因为学生数据相对稳定。成绩数据则使用list保存,每次录入成绩时,根据录入的学号,找到合适的位置插入,比如已经录入10、11、14、16号成绩,当录入12号时,自动找到11与14之间插入。既然成绩要不断地插入,那么采用list确实很合理。

  • 类定义

新建一个控制台应用项目,命名为“HelloSTL_ScoreManage_Ver1”。打开该项目下的main.cpp文件,再通过菜单:“编辑->文件编码”设置为“系统默认”。

首先加入<list>、<vector>、<string>等头文件的包含,以及前述两个类型的定义。代码如下:

#include <iostream>
            #include <list>
            #include <vector>
            #include <string>
            
            using namespace std;
            //学生
            struct Student
            {
            unsigned int number; //学号
              string name;
            };
            //成绩
            struct Score
            {
            unsigned int number; //学号
               float mark; //分数
            };
            //此处是 main() 函数的默认实现,略……
            
            

接着,需要一个“学生成绩管理/StudentScoreManager”类型,该类型当前提供以下三样功能:

第一、批量录入学生基本信息(学号、姓名)

第二、批量录入考试成绩。

第三、以学号为次序,输出考试成绩。

考试时,谁先做完,谁就先交卷,所以交完以后的卷子,可以看成是无序的。那么第三步如何实现呢?我们现在想到的办法是:拿到一个学号时,就在无序的list中查找这个学号的成绩。

请在main()函数之前,Score类定义之后,插入以下代码:

//学生成绩管理类
            class StudentScoreManager
            {
            public:
            void InputStudents();
            void InputScores();
            void OutputScores() const;
            private:
            vector<Student> students;
            list<Score> scores;
            };
            

OutputScores函数被声明为“const”,因为通常一个用来“显示”成绩的功能,不应该修改类型的任何成员数据。想想,你的老师交给你一份学生成绩表,让你将它打印,并张贴出来——在这个过程中,你的成绩由30分变成80分?有这可能吗?

  • 学生信息输入函数

InputStudents()函数将用来实现录入学生基本信息。由于通常是对着“花名册”录入,所以实现为只能按照学号次序录入——这样做也有一个好处:可以不用手工输入学号了。

void StudentScoreManager::InputStudents()
            {
            037    int number = 1; //学号从1开始
            
            while(true)
            {
            041        cout << "请输入" << number << "号学生姓名(输入x表示结束):";
            string name;
            getline(cin, name);
            047        if (name == "x")
            {
            break;
            }
            052        Student student;
            053        student.number = number;
            054        student.name = name;
            056        students.push_back(student);
            058        ++number;
            }
            }
            

这又是一个“死循环”,不过这次打破循环的方法比较有趣,041行提示用户输入小写字母‘x’表示退出。具体实现是在047选择的if条件判断。

程序被设计成以1号、2号、3号顺序输入学生姓名,学号从1开始,并且在每次录完一个学生之后,学号就自动加1。这是037行与058两行所完成的重要任务。

每次循环,都会在052行新定义了一个Student的“栈对象”,并且在随后053、054两行取得必要的值,然后在056行处,被加入到vector中。“栈对象”student会在每次循环结束自动消亡,然而,由于push_back()函数是先复制一份,再把复制品扔入vector,所以不必担心每次录入的学生对象会人间蒸发。

  • 成绩录入函数

接下来是录入成绩的成员函数:InputScores()。

void StudentScoreManager::InputScores()
            {
            while(true)
            {
            unsigned int number;
            cout << "请输入学号(输入0表示结束):";
            cin >> number;
            if (number == 0)
            {
            break;
            }
            //简单判断学号是否正确:
            078        if (number > students.size())
            {
            cout << "错误:学号必须位于: 1 ~ " << students.size() << " 之间。" << endl;
            081            continue;
            }
            float mark;
            cout << "请输入该学员成绩:";
            cin >> mark;
            Score score;
            score.number = number;
            score.mark = mark;
            092        scores.push_back(score); //直接加在尾部
                }
            }
            

录入成绩的代码,并不比录入学生信息复杂多少。我们耍了同样的花招来结束一个循环,只不过这一次用到学号上面:输入0,表示结束录入。

078行我们对用户输入的学号做一个简单的合法判断。学员的总数已知是students.size(),如果用户输入大于学员总数的学号,被认为不合法。这里用到了continue。

092行通过调用push_back,直接将新录入的成绩添加在list的尾部,所以list听成绩存储并没有按照学号排序。

  • 成绩显示函数

我们将遍历students中的每一个学生,输出他的学号和姓名。然后再次通过循环,在scores中找到指定学号的成绩,如果没有找到,提输出:“查无成绩”。

void StudentScoreManager::OutputScores() const
            {
            120    for (unsigned int i=0; i<students.size(); ++i)
            {
            122        unsigned int number = students[i].number; //学号
            
            cout << "学号:" << number << endl;
            cout << "姓名:" << students[i].name << endl;
            //查找成绩:
            128        bool found = false;
            130        for (list<Score>::const_iterator iter = scores.begin();
            iter != scores.end();
            ++iter)
            {
            if (iter->number == number)
            {
            found = true; //找到了
            
            cout << "成绩:" << iter->mark << endl;
            break;
            }
            }
            144        if (found ==false) //没找到??
                    {
            cout << "成绩:" << "查无成绩。" << endl;
            }
            }
            }
            

这段代码有两层for循环。

120行的for循环,遍历了每个学生。122行定义number记住当前学员的学号。随后两行输出学号和姓名。

128行我们又定义了一个bool变量:found。通过130行的for循环,遍历每个成绩,找到指写学号的分数,然后输出。如果没有找到,则直至循环结束,found也不会被置为真。 随后144行的判断条件成立,屏幕输出相应提示。

 

〖危险〗:当心:缺少错误处理的程序

为了简化代码,前述两InputXXX函数对用户非法输入的情况仅做最简单的判断。事实上,在要求输入学号或者分数时,如果用户不小心输入一些字母,比如:abc,程序就会陷入真正的死循环,此时必须使用Ctrl+C来强行中断。如果你想预防此类错误,可以每次接受输入之后,立即判断cin.fail()是否为真,具体请参看3.12.4节的实例。

 



    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多