当多态遇上数组 ... [C++] (Rewritten)
When Polymorphism Meets Arrays ... [C++] (Rewritten)
Rewriten on Thursday, March 31, 2005
Written by Allen Lee
犹如星空与海鸥,漫画里根本你我一生永不会聚头,但我誓要共你牵手。 —— 古巨基,《美雪,美雪》
1. 问答时间
第一题:实现多态的效果,我们需要具备哪些条件?
第二题:你认为以下代码是否有问题?
//Code#01
#includeiostream>
classA
{
public:
virtualvoidPrint()
{
std::cout"A.Print();"std::endl;
}
};
classB:publicA
{
public:
virtualvoidPrint()
{
std::cout"B.Print();"std::endl;
}
};
voidPrint(Aarr[],intcount)
{
for(inti=0;icount;++i)
{
arr[i].Print();
}
}
intmain()
{
constintCOUNT=3;
Aa_arr[COUNT];
Print(a_arr,COUNT);
Bb_arr[COUNT];
Print(b_arr,COUNT);
return0;
}
请你先自行思考一下上面两个问题。
2. 隐藏惊现!
Code #01能够正常编译并运行,而且程序输出也是我们所期望的。但请别过早开心,因为它里面隐藏着一个,只要条件满足就会引爆。是的,我是说“只要条件满足”,也就是现在条件还不满足。请再回顾Code #01,有没有觉得代码中的继承体系实在有点过分简单?好吧,我也不想卖关子了,现在就由我来触发里面所隐藏的。
//Code#02
classA
{
public:
A()
{
cout"A.A();"endl;
}
virtual~A()
{
cout"A.~A();"endl;
}
virtualvoidPrint()
{
cout"A.Print();"endl;
}
};
classB:publicA
{
public:
B()
:m_Data(299792458)
{
cout"B.B();"endl;
}
virtual~B()
{
cout"B.~B();"endl;
}
virtualvoidPrint()
{
cout"B.m_Data="m_Dataendl;
}
private:
longm_Data;
};
你能够看出Code #02和Code #01的这两个类有什么实质的不同吗?好吧,把Code #02的两个类替换Code #01的两个类,然后编译并运行你的程序,看看你有什么发现。我料到有些读者却是懒惰,所以把运行结果截图贴了一下:
请留意命令行界面输出结果,你认为程序中止那刻究竟发生了什么事呢?
3. 引发爆炸的微妙
从输出结果的截图中,你将不难看出,程序于中止时正尝试打印b_arr[1]的m_Data,但又因为某些原因无法在内存中进行定位,于是就向我发脾气了。如果你对继承机制有一定的了解,你也将能够看出此时a_arr的3个A对象和b_arr的3个B对象已经构造完毕。
然而,为什么程序无法对b_arr[1]定位呢?答案就在以下代码中(位于Code #01中):
//Code#03
voidPrint(Aarr[],intcount)
{
for(inti=0;icount;++i)
{
arr[i].Print();
}
}
我们知道,arr是个指针,那么你认为从arr所指的内存到arr+i所指的内存,指针要走多远呢?从Code #03的Print();中我们可以看出,这个距离(表面上)是i*sizeof(A)。
为什么说“表面上”呢?因为(Code #03的)Print();给我(们)的感觉是客户端会向其传递一个包含A的对象实例的数组,但如果存放在该数组里面的是A的派生类的对象实例呢?
当我们把b_arr传递给(Code #03的)Print()时,arr到arr+i的实际距离应该是i*sizeof(B)。对于Code #01,sizeof(A)和sizeof(B)是一样的,但对于Code #02,sizeof(A)就比sizeof(B)小了。当程序执行到Print(b_arr, COUNT);时,arr+1就指向了有问题的位置,你可以想象得到,实际上它指向本应指向的位置的前面。
扩展阅读:
《C++ Primer 中文版(第三版)》的“3.9.2 数组与指针”一节详细讲解了数组与指针之间的关系。[1]
《More Effective C++ 中文版》的《条款3:绝对不要以多态方式处理数组》一节详细剖析了该的机理。[2]
4. 尝试拆卸
现在我们知道这个存在的根源是,无法正确预知传递给(Code #03的)Print();的数组里所存放的对象的实际大小。(尽管我加了个逗号,但这句话读起来还是很考肺活量!)
问题男提出把指针放进数组,好吧,我们现在来修改一下Code #01的Print();和main();:
//Code#04
//SeeCode#02forclassAandclassB.
voidPrint(A*arr[],intcount)
{
for(inti=0;icount;++i)
{
arr[i]->Print();
}
}
intmain()
{
constintCOUNT=3;
A*a_arr[COUNT];
B*b_arr[COUNT];
for(inti=0;iCOUNT;++i)
{
a_arr[i]=newA;
b_arr[i]=newB;
}
Print(a_arr,COUNT);
Print(reinterpret_castA**>(b_arr),COUNT);
return0;
}
现在,把Code #02和Code #04合并起来,编译并运行后,程序的输出如下图所示:
从输出结果中,我们看到了预期多态的效果,然而,你认为到目前为止,我们的代码(合并Code #02和Code #04)还有没有别的问题?
5. 拆弹改进 #01
“不会吧?还有什么问题?”我相信有人会这样惊讶的。现在你回顾Code #02和Picture #02看看我们还有什么应该做的却又被漏掉了?
“析构函数没有被执行!”有人看出了。没错!别忘记a_arr和b_arr这两个数组里面的对象实例是使用new制造出来的,似乎我们还没有把这些对象实例所占用的资源还给系统,因此,我们的代码造成了内存泄漏!
发现这点很好,接下来我们要写一个清理垃圾并归还资源的函数:
//Code#05
voidDestroy(A*arr[],intcount)
{
for(inti=0;icount;++i)
{
deletearr[i];
}
}
这样,对象实例就被正常析构并把所占资源归还给系统了:
到目前为止,我们的代码(合并Code #02、Code #05和Code #04)的输出就是Picture #02和Picture #03两幅截图的合并(Picture #02在上面,Picture #03在下面)。
到目前为止一切都已经很好,不过不知道你又没有发现我们的代码(合并Code #02、Code #05和Code #04)存在着一些局限性呢?
6. 拆弹改进 #02
“开什么玩笑?还要怎么改进你才满意?”别这样呀,我并没有开玩笑,我是认真的。请想一下一下这种数组声明有什么局限性?
A* a_arr[COUNT];
“噢,是COUNT的确定时期!”很好,终于有人发现。没错,COUNT必须在编译期被确定下来,那么如果我们必须到运行时期才能确定COUNT呢?想象一下COUNT是某函数的客户端透过参数传递过来的:
voidF(intcount)
{
//Iwanttocreatearrayshere
//usingthevalueofparacount!
}
这样,我们只需用new来动态制造数组就行了:
voidF(intcount)
{
A**a_arr=newA*[count];
B**b_arr=newB*[count];
//Anythingelsehere
}
然而,这样一来,我们就需要修改一下Code #05的Destroy();了:
//Code#06
voidDestroy(A*arr[],intcount)
{
for(inti=0;icount;++i)
{
deletearr[i];
}
delete[]arr;
}
我相信坚持到这里的你绝对不会不明白为何我要这样修改Destroy();的 ^_^ 。嗯,现在,你再检查一下我们的代码,看看我们是否还漏了些什么。
7. 进一步测试
呵呵,先别晕,再坚持一下,好吗?你有没有发觉一路来,每一个数组里面所存放的对象实例都属于同一类类型的,但你知道现实中的你不一定那么好运的,现在我把main()修改一下:
//Code#07
//SeeCode#02forclassAandclassB.
//SeeCode#04forfunctionPrint();
//SeeCode#06forfunctionDestroy();
intmain()
{
constintCOUNT=3;
A**arr=newA*[COUNT];
for(inti=0;iCOUNT;++i)
{
if((i%2)==0)
{
arr[i]=newA;
}
else
{
arr[i]=newB;
}
}
Print(arr,COUNT);
Destroy(arr,COUNT);
return0;
}
好了,一切就绪,编译并运行一下,看看有什么结果。
看来,一切都如我们所期望的发展,很好!
8. 进一步思考
现在,本文开篇的第二题似乎被解决了,是吗?真的吗?我认为现实并非我们想的如此简单,你能想出当处于一个真实的环境中我们还需要注意一些什么吗?现实中,class A和class B将包含更多的细节,更复杂,如果程序没有正常归还资源,那么后果将不堪设想。试想一下,如果程序在没有完整构造和/或析构对象的情况下,突然抛出异常导致自身中止会怎么样?
扩展阅读:
《More Effective C++ 中文版》的《条款9:利用destructors避免泄漏资源》、《条款10:在constructors内阻止资源泄漏》和《条款11:禁止异常流出destructors之外》详细的讲解了我们将要考虑的这方面的异常处理问题。[2]
那么,本文开篇的第一题呢?呵呵,我并没有打算在这里正式作答,给出这道题主要是让你(读者)检查一下自己是否具备阅读本文的必要条件(如果你对本题毫无头绪,那么阅读本文将是一项罪过!)。当然,对于Code #02,我认为有两点需要注意的:
a) 如果你要建立继承体系,你应该分清C++的私有继承、保护继承和公有继承之间的区别,哪一种是用来实现多态效果的?什么情况下哪一种更适用?什么情况下我们应该(或不应该)使用继承?
扩展阅读:
《Exceptional C++ 中文版》的《第24条:继承的使用和滥用》一节详细的讲解了使用继承应该注意的事宜。[3]
b) 继承体系中的类的析构函数应该被声明为virtual,否则,(Code #06的)Destroy();将仅仅调用基类(于本文的例子是class A)的析构函数。这样,如果派生类的析构函数进行了一些重要资源的清理和回收,那么将无可避免地被忽略,从而造成资源泄漏。
9. 写在后面的话
自从我Post了本文的第一个版本后,收到了很多关于其错漏的反馈,我也为那篇不够水平的烂文深感抱歉。为了补偿过失,我决定重写本文。这次我尽了最大的努力去收集和试验相关的材料,并写成本文。当然,如果你在阅读的过程中发现(任何)问题,包括不解与错漏,请务必指出,我会尽力改进文章的质量的。
See also:
-
[1] Stanley B Lippman, Josee Lajoie 著;潘爱民 张丽 译;《C++ Primer 中文版(第三版)》;中国电力出版社,2002
-
[2] Scott Meyers 著;侯 捷 译;《More Effective C++中文版》;中国电力出版社,2003
-
[3] Herb Sutter 著;卓小涛 译;《Exceptional C++ 中文版》;中国电力出版社,2003
分享到:
相关推荐
实现多态2023.12.10
C++实验四多态程序设计).pdf
当我们使用`Base`指针调用`print()`时,实际上会执行`Derived`类的版本,即使指针指向的是`Derived`对象。 除了虚函数,C++还提供了纯虚函数的概念,用于创建抽象基类。纯虚函数在函数声明前加上`= 0`,如`virtual ...
虚方法实现多态2023.12.10
Java基础多态PPT教学课件.pptx
1.多态含义和注意.mp4
C++多态的思维导图
多态练习文本文档.txt
Python自学教程-04-代码实现多态.ev4.rar
02 多态案例-计算器.cpp
第继承和多态PPT学习教案.pptx
类继承与多态PPT学习教案.pptx
LabVIEW编程的实用技巧系列第2讲——多态VI的创建.avi
类的继承与多态PPT学习教案.pptx
特征之继承与多态PPT学习教案.pptx
javaJava的继承与多态PPT教案学习.pptx
在这个例子中,`drawShapes` 方法可以接受任何类型为 `Shape` 的数组,即使数组中包含 `Circle` 和 `Rectangle` 对象。这是因为它们都是 `Shape` 类的子类。 3. **接口的多态性**:除了类的继承外,Java还支持接口...
轻松学Java之继承与多态PPT学习教案.pptx
多态VI.vi 字符判断.vi 布尔.vi 布尔运算.vi 抽取数组.vi 控件 1.ctl 控件 2.ctl 数值控件.vi 数组与数组.vi 数组与数组大小不同.vi 数组交织.vi 数组创建.vi 数组初始化.vi 数组大小举例.vi 数组子集.vi 数组拆分....
当用父类引用指向子类对象时,调用的成员方法会根据实际的对象类型来决定执行哪个版本,这就是多态的动态绑定特性。 3. **接口与多态**:除了类继承,Java还支持接口继承,通过实现接口,类也可以表现出多态性。...