`
lovnet
  • 浏览: 6878141 次
  • 性别: Icon_minigender_1
  • 来自: 武汉
文章分类
社区版块
存档分类
最新评论

当多态遇上数组 ... [C++] (Rewritten)

阅读更多

当多态遇上数组 ... [C++] (Rewritten)

When Polymorphism Meets Arrays ... [C++] (Rewritten)

Rewriten on Thursday, March 31, 2005

Written by Allen Lee

犹如星空与海鸥,漫画里根本你我一生永不会聚头,但我誓要共你牵手。 —— 古巨基,《美雪,美雪》

1. 问答时间

第一题:实现多态的效果,我们需要具备哪些条件?

第二题:你认为以下代码是否有问题?

//Code#01

#include
iostream>

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_cast
A**>(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

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics