当多态遇上数组 ... [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>

class A
{
public:
    
virtual void Print()
    
{
        std::cout 
<< "A.Print();" << std::endl;
    }

}
;

class B : public A
{
public:
    
virtual void Print()
    
{
        std::cout 
<< "B.Print();" << std::endl;
    }

}
;

void Print(A arr[], int count)
{
    
for (int i = 0; i < count; ++i)
    
{
        arr[i].Print();
    }

}


int main()
{
    
const int COUNT = 3;

    A a_arr[COUNT];
    Print(a_arr, COUNT);

    B b_arr[COUNT];
    Print(b_arr, COUNT);

    
return 0;
}

请你先自行思考一下上面两个问题。

 

2. 隐藏炸弹惊现!

Code #01能够正常编译并运行,而且程序输出也是我们所期望的。但请别过早开心,因为它里面隐藏着一个炸弹,只要条件满足就会引爆。是的,我是说“只要条件满足”,也就是现在条件还不满足。请再回顾Code #01,有没有觉得代码中的继承体系实在有点过分简单?好吧,我也不想卖关子了,现在就由我来触发里面所隐藏的炸弹。

// Code #02

class A
{
public:
    A()
    
{
        cout 
<< "A.A();" << endl;
    }


    
virtual ~A()
    
{
        cout 
<< "A.~A();" << endl;
    }


    
virtual void Print()
    
{
        cout 
<< "A.Print();" << endl;
    }

}
;

class B : public A
{
public:
    B()
        : m_Data(
299792458)
    
{
        cout 
<< "B.B();" << endl;
    }


    
virtual ~B()
    
{
        cout 
<< "B.~B();" << endl;
    }


    
virtual void Print()
    
{
        cout 
<< "B.m_Data = " << m_Data << endl;
    }


private:
    
long m_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

void Print(A arr[], int count)
{
    
for (int i = 0; i < count; ++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
// See Code #02 for class A and class B.

void Print(A* arr[], int count)

    
for (int i = 0; i < count; ++i)
    

        arr[i]
->Print();
    }

}


int main()
{
    
const int COUNT = 3;

    A
* a_arr[COUNT];
    B
* b_arr[COUNT];
    
for (int i = 0; i < COUNT; ++i)
    
{
        a_arr[i] 
= new A;
        b_arr[i] 
= new B;
    }


    Print(a_arr, COUNT);
    Print(reinterpret_cast
< A** >(b_arr), COUNT);

    
return 0;
}

现在,把Code #02和Code #04合并起来,编译并运行后,程序的输出如下图所示:

从输出结果中,我们看到了预期多态的效果,然而,你认为到目前为止,我们的代码(合并Code #02和Code #04)还有没有别的问题?

 

5. 拆弹改进 #01

“不会吧?还有什么问题?”我相信有人会这样惊讶的。现在你回顾Code #02和Picture #02看看我们还有什么应该做的却又被漏掉了?

“析构函数没有被执行!”有人看出了。没错!别忘记a_arr和b_arr这两个数组里面的对象实例是使用new制造出来的,似乎我们还没有把这些对象实例所占用的资源还给系统,因此,我们的代码造成了内存泄漏!

发现这点很好,接下来我们要写一个清理垃圾并归还资源的函数:

// Code #05

void Destroy(A* arr[], int count)
{
    
for (int i = 0; i < count; ++i)
    
{
        delete arr[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是某函数的客户端透过参数传递过来的:

void F(int count)
{
    
// I want to create arrays here
    
// using the value of para count!
}

这样,我们只需用new来动态制造数组就行了:

void F(int count)
{
    A
** a_arr = new A*[count];
    B
** b_arr = new B*[count];

    
// Anything else here
}

然而,这样一来,我们就需要修改一下Code #05的Destroy();了:

// Code #06

void Destroy(A* arr[], int count)
{
    
for (int i = 0; i < count; ++i)
    
{
        delete arr[i];
    }


    delete[] arr;
}

我相信坚持到这里的你绝对不会不明白为何我要这样修改Destroy();的 ^_^ 。嗯,现在,你再检查一下我们的代码,看看我们是否还漏了些什么。

 

7. 进一步测试

呵呵,先别晕,再坚持一下,好吗?你有没有发觉一路来,每一个数组里面所存放的对象实例都属于同一类类型的,但你知道现实中的你不一定那么好运的,现在我把main()修改一下:

// Code #07
// See Code #02 for class A and class B.
// See Code #04 for function Print();
// See Code #06 for function Destroy();

int main()
{
    
const int COUNT = 3;

    A
** arr = new A*[COUNT];
    
for (int i = 0; i < COUNT; ++i)
    
{
        
if ((i % 2== 0)
        
{
            arr[i] 
= new A;
        }

        
else
        
{
            arr[i] 
= new B;
        }

    }


    Print(arr, COUNT);

    Destroy(arr, COUNT);

    
return 0;
}

好了,一切就绪,编译并运行一下,看看有什么结果。

看来,一切都如我们所期望的发展,很好!

 

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

 

posted @ 2005-04-01 10:27  Allen Lee  阅读(3168)  评论(7编辑  收藏  举报