第3章 感受(一)——3.10. Hello object 多态版

白话C++

3.10. Hello object 多态版

公元前209年,7月。秦朝著名的两位民工,陈胜、吴广说了一句话:“将相王候,宁有种乎?”。他们在表达一种不满:难道那些有钱人或当官人,天生就和我们有不同人种吗?

我们的例子程序似乎也有这个问题。

Person xiaoA; 
Beauty jiaLing;

xiaoA天生就是普通人类,而jiaLing天生就是美人胚子。她们的“类型”不一样。

通常我们提出一个问题,就代表一个新需求出现了,然后我们就解决它。不过有关“权贵、财富、美貌、人种”的问题实在太复杂了,因此今天的代码仅仅对现状做一个小小改进。

C++规定,一个基类的“堆变量”,允许被实例为一个派生类的“对象”。比如:

Person  *someone;

表面上看,someone是一个普通人类(Person)。不过,C++规定它可以被创建成一个派生类(Beauty)的对象。

Person *someone; 
someone = new Beauty; //“new出来”的是“Beauty”

第一行,我们定义一个堆对象。不过具体它将是Person,还是Beauty,此时未确定,直到第二行,才通过new,创建了一个Beauty对象。

基类的对象变量,可以创建实际上是派生类的对象,这可是一项新鲜技术。我们快来试试。

请新建一个控制台应用。main.cpp代码内容来自前一样例,包括记得修改其编码为系统默认;然后将main函数内容修改如下:

int main()
{

036
Person *xiaoA = new Beauty;
037
xiaoA->name = "Xiao A";

039 xiaoA->Introduction();
040
delete xiaoA;
//------------------------------------------------
042 Beauty* zhiLing = new Beauty;
zhiLing->name = "志玲";
zhiLing->Introduction();
delete
zhiLing;

return
0;
}

以前,xiaoA一直是一个“栈对象”,但现在,036行中xiaoA被定义成一个Person类型的堆对象,但随后(就在同一行),它就被实际创建成Beauty类型对象。

042行开始完整地重复了之前有关zhiLing对象的创建,使用与释放的代码,作为对比,zhiLing在定义时,就是一个Beauty类型。

037行和039行中,“.”变成“->”;040行则用于删除xiaoA——这一切变化,都缘于xiaoA现在是一个堆对象了。

“最重要的是”,xiaoA含着眼泪对苍天呼喊:“我也是美女了!”。还是编译、并运行程序之后再说吧——苍天啊,你看到了什么?这一切又是为什么?

图26

(图 26 xiaoA是美女,但是她的问候方式还是……

小A在“析构函数”中,也就临死前的“呜~呜~”两声,倒也情真意切,因为她心有不甘啊!同样是美人,怎么差别就这么大呢?

3.10.1. 虚函数

xiaoA和zhiLing的本质区别在于: zhiLing是名实一致,而xiaoA却名实不一:

036 Person *xiaoA = new Beauty;

042 Beauty* zhiLing = new Beauty;

当然,这里的“名”并不是指“名字”,名字和一个人是不是美人无关,这一点我们很早就清楚。这里的“名”指的是一个对象定义时的“类型”。

虽然,xiaoA实质上创建成一个Beauty对象,但它在定义时指定的类型是Person。所以在这一行的程序:

039 xiaoA->Introduction();

xiaoA仍然被当成Person——苍天啊,原来在c++的世界里,也讲究“出身”论,虽然xiaoA明明长成了一个Beauty,可是C++没有忘记它的出生是一个普通的Person——于是它的自我介绍,也就仍然是普通人的方式了。

“出身决定一切”,或许在某些情况下有其用途,但在当前情况下,这显然不是我们所愿意接受的情况,还好C++为我们提供了解决方法。很简单,只要为基类Person的Introduction函数增加一个修饰词:virtual

有virtual这个关键词修改的成员函数,被称为“虚函数”。

C++规定:当一个基类中的某一个函数是虚函数,那么在其派生类(以及派生类的派生类……)中,如果有存在相同的函数,那么这些函数就算不加virtual修饰,也是虚函数。

这里的相同函数是指:函数名相同,并且函数参数列表相同(次序和类型相同),并且返回值也相同,或者是原返回类型的派生类。

结合本例,如果我们让Person(基类)的中的Introduction是虚函数,那么Beauty(派生类)中的Introduction也就成为虚函数,并且假设今后Beauty也有自己的派生类,那么这条规定同样有效。

C++规定:如果一个函数是虚函数,那么在调用该函数时,程序将确保使用该对象的实际创建的对象类型所拥有的那个函数。

说起来似乎复杂,但实际做起来很容易:

struct Person
{

//此处略去构造函数与析构函数

018 virtual void Introduction()
{

cout << "Hi, my name is " << name << "." << endl;
}


//……
};

全部的变化就是在018行添加virtual这个关键字。请记住它加的位置:在函数返回类型void之前。

现在编译并运行程序,xiaoA就可以按照美人的方式来自我介绍了。不过出于个人的C++编程风格, 我建议大家Beauty的Introduction前面也加上virtual。

 

hint〖小提示〗:派生类中的函数加不加virtual ?

C++ 程序的代码风格可以分成两派:一派是一个多余的字母都不写。另一派是愿意写一些多余的字词——如果这些字词有助于代码阅读理解的话。由于一个函数在基类中 加了virtual的话,在派生类中该函数就自然也是虚函数了,所以这时在派生类中针对该函数再写“virtual”,至少在语法上显得多余。

本书作者属于第二派,我向大家推荐我的风格:在派生类中,以至在派生类的派生类中,都明确地加上virtual来向代码阅读者表明一个事实:这个函数是虚拟的,请注意。

 

3.10.2. 虚“析构函数”

析构函数也是成员函数,因此虚函数的规则对它也起作用。不过析构函数和普通成员函数相比,有以下特殊之处:

第一、 派生类的析构函数名字肯定和基类叫法不一样,但这不影响虚函数的规则对它起作用。

第二、 析构函数的特别之处是它是对象在死亡之前必定要做的一件事。通常复杂的对象会在析构函数内释放额外占用的内存等资源。

第三、 派生类的调用完自己的析构函数之后,会自动调用基类的析构函数。其目的是为了确保基类的资源也能自动释放。

 

在我们的例子中,美人的死和普通的人死没有什么区别,干脆,我们给美人类专门设计一个死亡告别方式——我们准备为美人类提供自定义的析构函数。

struct Beauty : public Person
{
~
Beauty()
{

cout << "wu~wu~人生似蚍蜉、似朝露;似秋天的草,似夏日的花……" << endl;
}


//此处略去美人类的Introduction()函数
};

按Ctrl + F9编译当前程序,我们会看Code::Blocks消息栏出现几条编译警告:

warning: `struct Person' has virtual functions but non-virtual destructor

意思是,Person类型拥有虚函数(Introduction),但它的析构函数却不是虚的。这是一个警告,暂时我们不理会它,运行该程序,观察xiaoA在被释放时,屏幕将输出什么内容?

没错,Person的析构函数现在不是虚的,所以对于xiaoA这样定义为Person但实例化为Beauty的对象,则在释放它时:

040 delete xiaoA;

该行的屏幕输出,将仍然只有基类的:“Wu~Wu~”。

 

import〖重要〗:警告消息同样重要!

编译时出现warning类型的消息,通常表示程序仍然能完成编译,但是程序运行结果可能会有问题,比如本例中,程序虽然正常运行,但结果却不是我们想要的逻辑。

事实上,忽略warning是一个极其危险的行为,许许多的警告,往往都暗含着由于我们的一些不可忽略的疏忽,比如:本该是virtual的析构函数,却忘了加virtual修饰。

 

接下来,就让我们把基类Person的析构函数也修饰成为虚函数,同样地,出于一致编程风格,我们在~Beauty之前也加上virtual关键字。再次编译,运行。用了“virtual”,结果正确了,警告消息也没了……

3.10.3. 应用

“ 这个函数是虚拟的,请注意”——现在你一定能理解这句话的含意,因为我们已经尝试过两次了,事实证明一个函数“非虚拟”,或者是“虚拟”的,其间的差别真 的很大:如果不是虚拟的,那么出身决定一切;如果是,那么还可以后天努力。下面我们做一个有关virtual函数的实际应用。

在前面代码的基础上,我们继续修改main函数,目的是交互,让用户来决定一个人是不是美人。

这个例子不仅用到对象和虚函数,而且还将用上“条件分支/if”和“循环流程/while”等等我们已经接触过的内容。新内容则包括:在讲到while时,本例使用了break来打破死循环,以及使用continue用于直接进入下次循环。

除了getline可以读取一行字符串以外,我们还将在本例中学习到另一个读取输入的方法:流输入操作符/>> 。另外本例还将涉及cin.sync() 函数的使用。

int main()
{

041
while(true)
042
{
043
Person *someone;

045
cout << "请选择(1/2/3):" << endl
<<
"1----普通人" << endl
<<
"2----美人" << endl
048 << "3----退出" << endl;

050
int sel = 0;
051
cin >> sel;

053
if (cin.fail ())
054
{
055
cin.clear();
056
}

058 cin.sync();

060
if (3 == sel)
061
{
062
break;
063
}

065
if (1 == sel)
066
{
067
someone = new Person;
068 }
069 else if (2 == sel)
070
{
071
someone = new Beauty;
072
}
073
else //用户输入的,即不是1,也不是2,也不是3...
074 {
075
cout << "输入有误吧?请重新选择。" << endl;
076
continue;
077
}

cout << "请输入姓名:";

082 string name;
083 getline(cin, name);

084 someone->name = name;

cout << name << "的自我介绍:" << endl;
087 someone->Introduction();

089 delete someone;
090 }

092 return 0;
}

041行,我们再次看到while(true),这似乎又是一个死循环,因此Ctrl + C 对这个程序也有效。

043行,我们定义一个“Person”类别的堆变量:someone。这是一个关键,随后我们将根据用户的选择,来决定someone是普通人对象,还是美人对象。

045~048, 我们通过cout在屏幕上打印出一些提示信息,否则用户不知道输入什么。后面的代码的几处cout与此相似。唯一需要特别说明的,或许是这4行代码,实际 上只对应了一行C++语句。我们只使用了一个cout,并且也只有048行的行末有分号。我们故意折成几行,仅仅是为了让代码更美观,实际上这四行写成长 长的一行。

通过屏幕提示,用户现在知道可以输入1、2、3这三个数字来做出相应选择。050行我们定义了一个整数类型(int)变量sel,并初始化为0。然后紧接着下一行:

051 cin >> sel;

cin代表标准输入设备,也就是控制台。这些行代表示程序将停下来,等待用户从控制台通过键盘输入一个整数,程序将读入这个整数,并且保存到sel变量中。

在 此情况下,如果用户输入的不是数字,比如用户随便输入一个字符串:“abcd”,并按下回车,那么程序同样会结束等待(因为用户已经完成输入),但由于 “abcd”不是一个合法的整数,所以程序将出错。这种情况下,如果调用cin的成员函数fail ()这,将会返回“真”,表示cin现在处于“fail/失败”状态。

当cin处于“fail”状态时,所有对其的后续操作都会继续失败,除非程序调用它的clear()成员函数来清除“fail”状态。因此,053行~057行,是一个简单的判断,如果发现出错了,就清除cin的出错标志:

053 if (cin.fail ())
054
{
055
cin.clear();
056
}

 

058 行调用了cin.sync(),用以清除所有未读取的内容。假设用户输入的是1,那么通常用户需要按一个回车键以表示输入完成。此时,cin >> sel这一行读入了1,但回车键就会留在cin的缓冲区中。如果用户是随便地输入“abcd”,由于它们不是整数,cin没办法正确读入,这时候 “abcd”加上回车符全部滞留在cin的缓冲区中。如此,当后面我们要读取“姓名”时,这些滞留内容就会被读入,这显然不是我们的目的,所以此时调用 sync()非常必要。

--广告:本课程将于2009年出书,请关注www.d2school.com--

060行判断用户输入是不是整数3,如果是,表明用户要退出本程序。063行的break关键字用于完成此任务。break可以让程序跳出当前所处的循环,在本例子,程序跳出while循环之后,就落在main函数的最后一行(092行)。

065~077 行先判断sel值是不是1?是则实际创建一个Person对象;否则,继续判断sel值是不是2?是则实际创建一个Beauty对象;如果再不是,说明用 户输入的内容非法,程序将在屏幕上输出出错提示,并且通过conitnue关键字,直接继续下一次循环,即程序将要求用户重新输入。

082行定义了一个变量name,081行通过getline读入用户输入的姓名。之所以我们还是采用getline来读取用户输入,而不是这样:

cin >> name;

原因在于,>> 在读到空格时,就会认为本次读取完成。如此,当用户输入 “JingJing Guo”时,就会仅仅将“JingJing”读入,而漏掉空格之后的姓氏。

084行代码,将临时变量name的值,赋给当前someone->name。087行代码调用someone->Introduction(),完成自我介绍。

089行通过delete someone;“杀死”新创建出来的这个“人”。由于代码处于while循环中,因此程序将重复前述过程。注意,此时的delete操作显得非常重要,否则每次new出来的内存,都不会得到释放。

为了便于理解,我们给出一个完整的运行结果图,并加以注释。

tu27

(图 27 完整的多态版Hello OO运行结果)

3.10.4. 多态VS.非多态

如果您已经搞定前例的90多行代码,现在请考虑,假设该例子中不允许采用virtual函数,那么实现相同功能的代码,应该如何写?请大家动手实现,并思考C++的“多态”特性,在本例中带来哪些方便。

 

caution〖危险〗: 看懂代码 != 真懂代码

很多新学习者懒得动写代码——这差不多注定了他们不可能成为合格的程序员。又有些学习者对采用旧技术没有兴趣,他们说:“我都已经学会‘多态’了,何苦再用‘非多态’的方法写一遍呢?那多浪费时间啊”——这差不多注定了他们将成为对新知识一知半解的,半桶水程序员。

 

白话C++
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南郁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值