不要指望数组的多态性

Posted by zhangxiaojian on July 19, 2014

多态性使程序员可以使用父类的指针或者引用来访问子类对象,就好像父类有多种类型,但是假如我们将多态性和数组联系在一起,会发现一些让人头疼的问题。

我们简单定义一个父类,拥有一个int类型的数据成员,有一个虚函数把它打印出来:

class Base{
    public:
        Base(int v = 0):bvalue(v){}
        virtual void printValue()
        {
            std::cout<<"bvalue: "<<bvalue<<std::endl;
        }
    private:
        int bvalue;
};

然后再定义一个类似的子类:

class Derived:public Base{
    public:
        Derived(int v = 1):dvalue(v){}
        void printValue()
        {
            std::cout<<"dvalue"<<dvalue<<std::endl;
        }
    private:
        int dvalue;
};

程序的核心是一个函数,用来打印一个包含Base类型的object的数据成员,大概是这样:

void printBase(Base bArray[],int num)
{
    for(int i = 0;i < num;++i)
    {
        bArray[i].printValue();
    }
}

main函数中大概这样:

    Base bArray[5];
    Derived dArray[5];         //为了简洁,省去数组内object的插入及初始化
    
    printBase(bArray,5);
    printBase(dArray,5);

我们期望当把Derived类型的数组dArray传递给printBase函数的时候,它能够调用Derived的虚函数,打印出成员变量的值。就像把bArray传过去的时候表现的一样好。但是事与愿违,它甚至不能够执行。试一试吧:

img 这是什么情况?竟然会去读取地址0x00000000的内容,编译器当然会发出强烈的抗议。

问题就出在数组上!

我们知道数组名其实是一个指针,数组的中括号访问符实际上是指针加上偏移,所以,函数printBase中的语句:

    bArray[i].printValue();

其实是这么调用的:

    *(bArray + sizeof(Base) * i).printValue();

因为函数定义的参数类型是Base,因此sizeof里面调用的将是Base。虽然实际上是Derived类型。Base的sizeof是8,而Derived的sizeof是12,两个类的模型如下:

img

img

现在我们可以看看到底发生了什么事,为什么会访问非法地址0x00000000。

当我们将Derived类型的数组传给函数printBase的时候,会将数组dArray的指针传过去,for循环中第一次迭代的i为0,实际的调用就是这样:

    *bArray.printValue();

函数printvalue,是virtual类型的函数,会通过虚函数指针去寻找虚函数表,然后定位到实际所在类型的虚函数。而虚函数表如上图所示,存在内存的开头。此时bArray的值就是虚函数指针的值,因此第一次迭代完美执行,索引到Derived类的printValue函数。

但是当第二次执行的时候,就不像第一次那么顺利。此时i为1,实际调用的是这样:

    *(bArray + 8 * 1).printValue();

指针从一个Derived类型对象的起始位置加8,此时指向的位置实际上是存有dValue值的位置。但是编译器并不会意识到错误,它将dValue的值视为一个Base类型对像的虚函数指针。而dValue的值为0(构造函数默认把它初始化为0),就会访问非法地址0x00000000。下面是执行时候的汇编代码: r img

第三行就是罪魁祸首。

一切谜题都已解开,所以当数组和多态结合在一起的时候,不要期望它会表现的和预期的一样。