zoukankan      html  css  js  c++  java
  • 当多态遇上数组 ... [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
  • 相关阅读:
    GitHub统计
    不错的第三方控件
    仿射变换(CGAffineTransform)使用小结
    AffineTransform(仿射变换)
    使用CAShapeLayer实现复杂的View的遮罩效果
    使用CAShapeLayer实现一个音量大小动态改变的控件
    window10 Docker仓库访问
    postgresql从timestamp(6)复制到timestamp(0),时间会变
    在编译Dll文件的时候遇到dll 链接不一致的问题
    qtquery 取列的值
  • 原文地址:https://www.cnblogs.com/allenlooplee/p/129968.html
Copyright © 2011-2022 走看看