第3章 感受(一)——3.12. Hello STL 向量篇

[回到目录]
白话C++

3.12. Hello STL 向量篇

“ 好消息!第XXX届国际美女大赛即将在中国举行,届时将有2999名来自世界各地的美女参赛。最新消息表明,本次大赛将对所有数据统一采用专业软件进行管 理。另据IT界知情人士透露,该软件首席架构师来自第2学堂的,他是我国著名的软件设计大师:丁小明先生!丁先生今年18岁,十数年来他长期奋斗在……”

丁小明正在家里忙着呢!他把选手名单摆在电脑边上,手在键盘上飞快敲击。“2999名选手,那就是说,我得定义2999个对象……”

噢!等等!才学到半截就跑出去混“大师”称号,是不是有些早了?至少……你也得把“容器”篇学完后,再出去忽悠吧?

根据我们当前学到的知识,可以这样定义两个美女对象:

Beauty zhiLing;

Beauty jiaLing;

不过今天我们的需求,有了很大变化。我们需要2999个美女对象,显然要将这2999美女的信息在编程时就一个一个写好,不仅很累,而且得很很笨:难道我们下次变成3000个选手时,我们就又要改写代码吗?

是时候需要感受一下C++的“容器”类型了。

什么叫“容器”,家里的杯子、碗、碟子就是容器。一个容器可以用来很多东西,并且东西的个数可变。今天我们要讲的C++容器类,来自于STL(C++ 标准模板库/Standard Template Library)。

C++标准库中的容器类,通通是采用C++“泛型技术”写成的“类模板”。

先来解释一个什么叫“模板”。我们可以想到“模具”,在工业生产中,当产家当需批量生产某种形状固定的新零件之前,往往需要先通过压力把金属或塑料等材质制造出一个有着特定的轮廓或内腔形状的“样板”。依据该样板,可以产生出非常一致的真实零件。

“类模板”不是真正的类,但当我们为它增加一定的信息之后,就可以很容易产生了真正的类。为了简单起见,我们暂时就将“容器类模板”,当成“容器类”对待。

通 常,杯子用来装水,碗用来装饭,碟子用来盛菜。不过这只是约定成俗,现实生活中实拿杯子装可乐,拿碗装菜也未尝不可;然而,今天我们所要讲解的C++容 器,都是有着严谨规定的容器,当我们一旦决定某个容器用来装什么类型的对象时,事情就被固定下来——意思就是,如果我们决定某个“碗”是“饭碗”,那么这 个碗就不允许拿去装汤了。

3.12.1. 基础

向 量/vector”是一种很常用的C++容器。它的特点是“整整齐齐”地保存对象。vector开辟出一块连续的内存用来保存对象,再加上前面我们说的, 容器保存的对象类型总是一致(因此大小也一致),所以可以把vector的内部,想象成一个个连续的,整齐的“格子”。这样所带来的好处是,我们可以“任 意”地访问指定的格子,这在C++里,称为“可随机访问”。

(图 37 vector 内存结构示意)

  • 图中,每一块格子表示一块内存,黑色格子表示已经被其它数据占用。白色格子表示可以使用的内存。
  • 依次将A、B、C三个对象追加到vector容器中,则它们根据加入的次序,一前一后地保存在连续的内存中。
  • 假设继续加入D对象,则D元素位于C对象之后。
  • 假设在加入D对象之后,再次加入E、F对象,则由于当前位置已经不存在空闲内存,此时vector将在另外地方寻找至少足够保存6个元素的连续空间,然后将此处全部数据复制过去。
  • 假设要在A与B之间插入新对象A’,则原有的B、C对象必须向后移动。

 

要使用vector,需要包括标准库文件:

#include <vector>

你应该还记得STL的头文件没有扩展名,以及include的含义。

vector只是一个“类模板”,当要它演化成一个真正的“类”,必须指定我们准备往里面存放什么类型的对象:

vector<要存放的对象类型>

比如我们准备往里面存“美女”,那么真正的“类”是:

vector<Beauty>

有了“数据类型”,我们就可以定义出对象:

vector<Beauty> manyBeauties;

你 可能想把对象取名为“2999Beauties”,这是错误的,第一,在语法上,C++不允许变量以数字开头(必须以字母或_开头,其它位置可以包含数 字);第二,在语意上,其实一个vector<Beauty>的对象,可以存放美女对象,并没有限定。可以是0个,也可以是2999个,或者 是十万个,当然,机器内存得足够。

3.12.2. 常用函数

  • 成员函数push_back

接下来,如果往manyBeauties中存入Beauty对象呢?vector(严格讲是vector<Beauty>)提供了 push_back()成员函数用于实现在后面加入一个对象:

vector<Beauty>  manyBeauties; //定义一个容器对象
Beauty zhiLing, maiLing, jiaLing; //定义三个要存入容器的对象

//开始存放:
manyBeauties.push_back(zhiLing); //复制一个“志玲”,扔到容器里去
manyBeauties.push_back(maiLing);
manyBeauties.push_back(jiaLing);

通常,我们将存在容器中的对象,称为容器的一个“元素”。

 

〖重要〗:元素是复制品

manyBeauties.push_back(zhiLing); 这行代码,是将对象zhiLing复制了一个,然后存放到容器中,也就是说,原来的zhiLing和容器中的志玲,除了内容一样以外,它们之间没有关系, 修改外部的zhiLing,并不会造成容器中的复制品也发生变化。

 

  • 操作符[]

现 在manyBeauties存放了三个美女,那么我们如何访问这三个美女呢?vecotr允许“随机”访问,我们可以通过指定次序,来访问 manyBeauties的元素。在这C++中,称为“通过索引”访问,通常,C++的索引都是被设计为从0开始,也就是说,在manyBeauties 中,索引为0的元素,是ZhiLing,索引为1的元素,是maiLing……请问,jiaLing的索引是多少?

另外,“通过索引”访问的操作,C++中通常设计成使用“[]”操作符进行。因此,访问manyBeauties中第0个元素,代码就是:

manyBeauties[0]

比如,我们要输出zhiLing的名字,代码如下:

cout << manyBeauties[0].GetName() << endl;

 

〖小提示〗:“操作符”也是一种函数

在 C++中,可以为一个类型,定义某一操作符的具体功能。比如前述的“中括号”操作符:[],其实就是vector的一定函数。不过操作符函数的名字,必须 有一个固定的前缀:operator。这样,[]函数的完整名字为: “operator []”;参数是索引值(通常是正整数类型);因此,前面的代码还有一个又长又丑的写法,虽然可以工作,但相信你不会喜欢。

cout << manyBeauties.operator [] (0).GetName() << endl;

 

  • 成员函数size 和empty

vector 的成员函数 size() 返回当前容器中元素的个数。empty()则返回当前容器是否为空。虽然判断size() 返回值是否等于0也可以得知当前容器是否为0,但empty函数执行速度更快。因此,如果仅仅想关心容器是否为空,请使用empty();如果需要知道具 体个数,才使用size()。

下面代码在屏幕上打印出:“3”。

cout << manyBeauties.size() << endl;

vector 的 size 和 empty 都是“常量成员函数”。

 

〖小提示〗:不恰当的函数名字:empty

没错,C++标准库也会犯错,empty的命名就是一个小问题,取名为“is_empty”显然更容易让人理解它的作用。

 

  • 成员函数 clear

vector 的成员函数clear()清空当前容器中的所有元素

 

3.12.3. 遍历

我们可以使用之前学过的while来遍历一个vector对象中的所有元素。

001 int i=0;
002 while ( i < 3)
003 {
004 cout << manyBeauties[i].GetName() << endl;
005 ++i;
006 }

这不再是一个“死循环”,相反,仅当整数i的值小于3时,循环体中的代码才得以执行。

001 行定义时, i 初始值为0;而005行代码,“++”是一种算术运算符,它会将操作数增加1。因此这个循环被继续执行三遍,manyBeauties[i]中的i的值,分别是:0、1、2。最后i的值被增加到3,于是while的条件不成立,循环结束。

  • for 循环

通过某个自变量的变化(通常是递增或递减),控制一个循环执行的代码,C++提供了一人更直观明确的循环流程:for。它的语法格式如下:

for (初始化语句; 循环条件; 循环变化语句)
{
//循环体
}

前述的代码可以转变为:

for (int i=0; i < 3; ++i)
{
cout << manyBeauties[i].GetName() << endl;
++i;
}

初始化语句:int i=0; 它定义一个整数变量,并且初始值为0。在循环过程,初始化语句被且仅被执行一次。特别的,最新的C++标准规定,此处的变量“i”的生命周期,将开始for循环,同时结束于for循环,其可见区也同样限定在for循环内部。

循环条件仍然是 i < 3。它在将要执行新的一遍循环体前,都会进行判断,如果条件成立,则执行循环体,否则,结束循环。在本例中,由于我们事先确定知道manyBeauties中存放了3个元素。但更好的方法,应该是通过size来获取。

for (int i=0; i < manyBeauties.size(); ++i)

循环变化语句:++i。它在每一遍循环结束后都要执行一次,如前所述,++i的作用是让整数i的值往上加1。

3.12.4. 实例:美女大赛管理系统

匆匆感受了一番vector,丁小明准备重新动手了。

下面是“美女大赛管理系统”主要功能清单:

第一、美女信息录入——录入美女以下信息:姓名、国籍、简介……大量读者打来电话质询:女性选美怎么可以没有三围数据?对啊?我怎么给忘了?加上!

第二、美女信息查找——输入美女姓名,查出该美女的详细信息,如果有同名,全部输出;

第三、查询已录入的美女人数;

第四、全部美女出场介绍;

第五、清空美女信息——清空已录入的全部美女信息;

 

新建一个控制台应用项目,命名为“HelloSTLVector”。打开main.cpp,更改其文件编码为“系统默认”。

我 们将不从“Hello Object 多态版”复制代码。顺便强调一点,本节的实例主要演示vector的用法,和“多态”没有多少关系。为什么?因为在“多态版”, 一个人可能是“普通人”,也有可能是“美人”,但在本例,所有人都是“美女”。不过,我们还是定义了Person类,希望大家能顺便复习一下“派生”。

 

步骤 1: 包含必要的头文件,以及加入std名字空间的使用声明。

#include <iostream>
#include <string>
#include <vector>

using namespace std;

 

步骤 2: 定义Person类。

006 class Person
{
public:
Person()
{
cout << "请输入姓名:";
getline(cin, name);

cout << "请输入年龄:";
cin >> age;
}

virtual ~Person()
{
}

string GetName() const
{
return name;
}

int GetAge() const
{
return age;
}

private:
string name;
int age;
};

Person增加了一个成员数据:年龄/age,构造函数增加输入年龄的处理。另外,由于本例我们暂时不管对象“生死”问题了,所以在构造与析构函数中,我们分别去除“Wa~Wa~”“Wu~Wu~”的动静。再仔细一看,你会发现,“自我介绍”函数也被砍掉了。

GetName和GetAge都被设计成“常量成员函数”。

 

步骤 3: 定义Beauty类

037 class Beauty : public Person
{
public:
Beauty ()
{
cout << "请输入国籍:";
043 cin.sync();
044 getline(cin, nationality);

046 cout << "请输入三围数据(胸、腰、臀),数据以空格隔开,回车确认:";
047 cin >> bust >> waist >> hips;

cout << "请输入自我介绍内容:";
cin.sync();
getline(cin, introduction);
}

string GetNationality() const
{
return nationality;
}

int GetBust() const
{
return bust;
}

int GetWaist() const
{
return waist;
}

int GetHips() const
{
return hips;
}

void Introduction() const
{
cout << introduction << endl;
}

private:
std::string nationality; //国籍

int bust; //胸围
int waist; //腰围
int hips; //臀围
};

在 谈到“虚析构”函数时,我们谈过:“派生类的调用完自己的析构函数之后,会自动调用基类的析构函数”。构造函数的调用次序正好相反:派生类在构造自己之 前,会先调用基类默认构造函数,然后再调用自己的构造函数。假设有C派生自B,B派生自A。那么构造时,先构造A,再构造B,最后构造C;析构时,先析构 C,再析构B,最后析构A。

当 我们构造一个Beauty时,会将调用其基类:Person的构造函数,因此程序运行时,先要求输入姓名、年龄;然后调用Beauty自身的构造函数,于 是要求输入国籍,三围数据。043行的代码调用sync函数,原因正是因为前面我们输入姓名时,会留下一个换行符,需要sync来清除,否则再次调用 getline时,会直接读入一个空行。

 

〖小提示〗:普通人就没有三围吗?

为 什么不在Person中定义三围数据?难道普通人就没有三围吗?我们说过,程序在很大程度是,是在用代码映射真实的社会,但也请注意,这个映射并不是事无 具细的映射,那既不可能也无必要,我们只需要映射所要关注的部分即可。此处,普通人我们不关心他的三围,所以三围数据被加入Beauty类的定义,而不是 Person类。

 

步骤 4: 定义BeautiesManager

有了“美女”类,接下来就必须提供“大赛组委会”,其主要功能是管理这些美女,所以我们称之为:BeautiesManager。

下面是BeautiesManager的类型定义:

//美女管理类
class BeautiesManager
{
public:
void Input(); //输入新的美女
void Find() const; //按姓名查找美女

void Count() const //显示当前美女总数
{
cout << "当前美女个数:" << beauties.size() << endl;
}

void Introduction() const; //所有美女依次自我介绍
void Clear(); //清空当前所有美女

private:
vector<Beauty> beauties;
};

你 应该发现了,BeautiesManager类中5个成员函数中有4个光有声明,没有实现。C++允许我们直接在类定义中实现其成员函数,也允许我们在类 定义中仅仅声明一下成员函数的原型,然后在类之外实现。实现时,需要在函数名前面加上:“类名::”。可能你想到了“名字空间/namespace”,没 错,在某些时候你可以认为“类”也是一个名字空间。

 

步骤 5: 实现Input()

109 void BeautiesManager::Input()
{
Beauty b;
beauties.push_back(b);
}

为了及时了解当前代码是否正常工作,我们可以在main函数中写一些测试代码。这叫做“单元测试”。

115 int main()
{
BeautiesManager bm;
bm.Input();

return 0;
}

 

步骤 6: 实现Find()

115 void BeautiesManager::Find() const
{
cout << "请输入要查找的美女姓名:";
string name;

getline(cin, name);

122 int found = 0;

for (unsigned int i=0; i<beauties.size(); ++i)
{
if (beauties[i].GetName() == name)
{
++found;

cout << "找到啦!该美女的索引是: " << i << endl;

cout << "姓名:" << beauties[i].GetName() << endl
<< "年龄:" << beauties[i].GetAge() << endl
<< "国籍:" << beauties[i].GetNationality() << endl
<< "三围:" << beauties[i].GetBust() << ", "
<< beauties[i].GetWaist() << ", " << beauties[i].GetHips() << endl;
}
}

cout << "共找到:" << found << "位名为:" << name << "的美女!" << endl;
}

Find()是一个常量成员函数,在类外部实现时,仍然要记得加上“const”的修饰。

我 们通过一个for循环,遍历整个beauties,用每一位美女的名字和用户输入的姓名比较,如果相等,就输出当前美女的详细信息(不含自我介绍)。另 外,在循环之前,我们定义了一个整数变量,起始值为0,以后每当找到一位同名美女,就自增1。循环结束后,我们将输出这个数值。

这一次,我们的测试代码是:

143 int main()
{
BeautiesManager bm;
bm.Input();
bm.Input();
bm.Find();

return 0;
}

 

步骤 7: 实现Introduction()

在Beauty中我们定义过一个同名函数,在BeautiesManager中,它的职责也只是循环输出所有美女的自我介绍。

143 void BeautiesManager::Introduction() const
{
for (unsigned int i=0; i<beauties.size(); ++i)
{
cout << "现在出场的是:" << beauties[i].GetName() << endl;
beauties[i].Introduction();
}
}

152 int main()
{
BeautiesManager bm;
bm.Input();
bm.Input();
bm.Introduction();

return 0;
}

 

步骤 8: 实现Clear()

152 void BeautiesManager::Clear()
{
cout << "您确认要清除所有美女数据吗?该操作不可恢复! (y/n):";

156 char c;
157 cin >> c;

159 cin.sync();

161 if (c == 'y')
{
beauties.clear();
cout << "数据已清除!" << endl;
}
}

在清除所有数据前,郑重地询问一下用户——这永远是一个友好的设计。156行定义一个字符变量,157行通过cin读入用户的选择。为了避免给后续的输入造成干扰,我们主动在随后就调用sync清除掉用户输入‘y’或其它字母之后,留下的回车换行符。

161行的代码逻辑也很清晰,如果用户输入的字符是小写字母‘y’,代码不再犹豫,立即调用vector的clear()函数,清除所有元素。

下面是单元测试代码:

168 int main()
{
BeautiesManager bm;
bm.Input();
bm.Input();
bm.Introduction();
bm.Clear();
bm.Introduction();

return 0;
}

 

步骤 9: 提供主菜单函数

是时候把以上功能“串联”起来了。C++是一门支持多范型编程的语言,前面的代码我们采用了“面对对象”的范型,接下来的的代码相对简单,我们改用“面向过程”的范型。

我们需要一个“菜单”,让用户选择执行前述的哪样功能。

168 //显示主菜单:
int ShowMenu()
{
cout << "请选择:" << endl;
cout << "1----美女信息录入" << endl
<< "2----美女信息查找" << endl
<< "3----检查美女总数" << endl
<< "4----美女出场自我介绍" << endl
<< "5----清空全部美女数据" << endl
<< endl
<< "6----关于本程序" << endl
<< "7----退出" << endl;

int sel = 0;
cin >> sel;
cin.sync();

return sel;
}

测试代码更为简单:

188 int main()
{
int sel = ShowMenu();
cout << "sel = " << sel << endl;

return 0;
}

 

步骤 10: 实现 About()

菜单中,第6项提供了“关于本程序”的选择,嗯,这可是专业软件必不可少的功能之一(另外,前面的代码真有些累人,是该来个简单一点的了)。

188 void About()
{
cout << "《XXX国际美女大赛信息管理系统 Ver 1.0》" << endl
<< "作者:丁小明 Copyright 2008~???" << endl;
}

测试代码略。

 

步骤 11: 主函数

int main()
{
cout << "XXX国际美女大赛欢迎您!" << endl;

BeautiesManager bm;

while(true)
{
int sel = ShowMenu();

if ( 1 == sel)
{
bm.Input();
}
else if (2 == sel)
{
bm.Find();
}
else if (3 == sel)
{
bm.Count();
}
else if (4 == sel)
{
bm.Introduction();
}
else if (5 == sel)
{
bm.Clear();
}
else if (6 == sel)
{
About();
}
else if (7 == sel)
{
break;
}
else //什么也不是?
{
if (cin.fail ())
{
cin.clear(); //清除cin当前可能处于错误状态,需清除
cin.sync();
}

cout << "选择有误,请重选。" << endl;
}
}

return 0;
}

看上去很长,但逻辑不复杂:

  • while(true)结构,用来实现程序可以循环地执行。
  • 连续的if( )/else if( )结构,用来确定用户的选择是1-7中哪个数字。
  • 当用户输入非法字符时,落入错误处理。详情请参看“Hello Object多态版”。

 

3.12.5. 枚举/enum

程序员有时候应该做一个“完美主义者”。上述代码中,我们将变量sel连续地从1一直比较到7。其中1代表什么?2又代表什么?这会让代码阅读者感到困惑。为了更直观一些,我们可以采用“枚举”来代替具体的数字。

定义枚举的语法如下:

enum 枚举名称 { 枚举项1, 枚举项2, ……};

其中,如果不需要,枚举名称可以不写。另外,每一个枚举项又可以通过以下形式,直接设定它对应的数值。

枚举项1 = 数值

如果枚举项没有设定数值,则第一项被默认设置为0,后面的项目则是前一项的数值加1。

比如我们可以为“星期”定义一个枚举:

enum Week { Monday , Tuesday , Wednesday, Thursday, Friday, Saturday , Sunday};

现在,Monday的值为0,而Tuesday的值为1,直到最后“星期天/Sunday”的值为6。更直观一点,应该让Monday的值为1,所以可以这样修改:

enum Week { Monday = 1 , Tuesday , Wednesday, Thursday, Friday, Saturday , Sunday};

有了这个定义,我们就可以以代码中定义一个Week的变量:

Week today = Friday;

事实上,每个枚举项,都可以认为是某个整数的“别名”。回到本节实例,我们需要为用户的选择项1~7定义枚举项,并且我们不需要枚举名称。

请在main函数中的第一行,加入一个无名枚举的定义:

int main()
{
196 enum {sel_input = 1, sel_find, sel_count, sel_introduction, sel_clear, sel_about, sel_exit};

cout << "XXX国际美女大赛欢迎您!" << endl;
...
}

然后将后面代码中,参加比较的7个数字,更改为对应的枚举项。

……
while(true)
{
int sel = ShowMenu();

if ( sel_input == sel) //sel_input 就是 1
{
bm.Input();
}
else if (sel_find == sel)
{
bm.Find();
}

……
}

 

〖课堂作业〗:完成枚举改造

请用上述的枚举项,替换参加比较的1~7数字,并通过编译、运行检查是否正确修改。

[回到目录]
白话C++

你可能感兴趣的:(第3章 感受(一)——3.12. Hello STL 向量篇)