标题:多态与虚函数


什么是多态

C++语言中,多态是一个非常重要的概念。那么什么是多态呢,有什么例子来举个多态的例子呢??这是在C++面试里经常会被面试官问到的一个问题多态性指的是:从相同的基类中继承而来的不同的类,由于对某些函数的实现作了部分修改,使得从这些基本类派生出来的类所定义的对象对于同一个消息会进行不同的操作。多态性是通过虚拟函数实现的。通过基类指针(或引用)来请求使用虚拟函数时,C++会在与对象关联的派生类中选择正确的改写过的函数。

可以通过下面的例子来理解多态:

 

class CShape

{

public:

    CShape();

    ~CShape();

    virtual float getArea();

}

class CCircle:public CShape

{

public:

    CCircle(float r)

    {

        m_r = r;

    }

    ~CCircle();

    float getArea()

    {

        return 3.14 * m_r * m_r;

    }

private:

    float m_r;

}

class CRect:public CShape

{

public:

CRect(float x, float y)

{

     m_x = x;

     m_y = y;

}

~CRect();

float getArea()

{

     return m_x * m_y;

}

private:

    float m_x;

    float m_y;

}

 

CShape *p1 = new CCircle(1.0);

p1->getArea();

CShape *p2 = new CRect(1.0, 2.0);

p2->getArea();

 

delete p1;

delete p2;

 

CCircleCRect分别继承自CShape,并各自改写(override)了getArea()方法。尽管它们都是调用了同样的虚方法getArea(),但由于多态性,在p1->getArea()p2->getArea()调用时,表现出了不同的行为(各自的面积计算公式)。这就是多态。

虚函数机制

编译器在执行过程中遇到virtual关键字的时候,将自动安装动态联编需要的机制,首先为这些包含virtual函数的类建立一张虚拟函数表vtable。在这些虚拟函数表中,编译器将依次按照函数声明次序放置类的特定虚函数的地址。同时在每个带有虚函数的类中放置一个称之为vpointer的指针,简称vptr,这个指针指向这个类的vtable。每一个类别只能有一个虚拟函数表,如果该类没有虚拟函数,则不存在虚拟函数表。C++编译时候编译器会在含有虚函数的类中加上一个指向虚拟函数表的指针vptr。从一个类别诞生的每一个对象,将获取该类别中的vptr指针,这个指针同样指向类的vtable

现在研究如下代码:

 

class Shape

{

public

     virtual double GetArea();

     virtual void Draw();

     int Func();

}

 

class Circle: public Shape

{

public:

     Shape(double rval)

     {

         r = rval;

     }

     double GetArea();

     void Print();

private:

     double r;

}

 

double Circle::GetArea()

{

     return 3.14 * r * r;

}

 

Circle c;

Shape *pShape = &c;

pShape->GetArea();

 

    如图1所示,类Circle派生于类Shape。其中在Shape中声明了两个虚函数:GetArea()Draw()

                                                                    图1 Circle派生于类Shape

    如图2所示,编译器在编译上面这段代码的时候将为shapecircle两个对象分别建立一个虚拟函数表(vtable),这些表依次填充派生类对象和基类对象中声明的所有的虚函数地址。如果派生类本身没有重新定义基类的虚函数,那么填充的就是基类的虚函数地址。这样一旦如果函数调用一个派生类不存在的方法时候能够自动调用基类方法。然后编译器在每个类中放置一个vptr,一般置于对象的起始位置,继而在对象的构造函数中将vptr初始化为本类的vtable的地址。C++ 编译程序时候按下面的步骤进行工作:

1)为各类建立虚拟函数表,如果没有虚函数则不建立。

2)暂时不连接虚函数,而是将各个虚函数的地址放入虚拟函数表中。

3)直接连接各静态函数。

 

                                                        图2 Shape类与Circle类的虚拟函数表关系

    可见,为了支持虚拟函数机制,C++增加了虚拟函数表,这样也增加了资源的消耗。在Microsoft MFC消息映射程序设计中,设计者们为了避免虚拟函数带来的资源消耗,而故意绕开了虚拟函数机制,采用了特殊的消息映射机制。

Virtualinline

虚函数调用,编译器的内联处理程序(inline)对它是无效的。这是因为:关键字“virtual”意味着等到运行时再决定调用哪个函数,“inline”的意思是在编译期间将调用之处用被调函数来代替,如果编译器甚至还不知道哪个函数将被调用,当然就不能责怪它拒绝生成内联调用了。

实际上inline指令就像register关键字,它只是对编译器的一种提示,而不是命令。也就是说,只要编译器愿意,它就可以随意地忽略掉你的指令,事实上编译器常常会这么做。例如,大多数编译器拒绝内联"复杂"的函数(例如,包含循环和递归的函数);还有,以上可以归结为:一个给定的内联函数是否真的被内联取决于所用的编译器的具体实现。幸运的是,大多数编译器都可以设置诊断级,当声明为内联的函数实际上没有被内联时,编译器就会为你发出警告信息。

思考题:

虚函数与多态是面向对象程序设计的一个重要特征。在一些C++的编程面试中,也经常考查,而且常常以编程的形式出现在考题里面。下面就以一个例子来应用虚函数的机制。该例是本人应届毕业的时候面试某知名外企遇到的一个面试问题。

如图3所示,为一个算术表达式树结构:

                                                                     图3 表达式树结构

请试设计一个算法,求每个结点的值。如果结点的类型为值结点,则返回其本身;如果为操作符号结点,则返回其左子树的值与右子树值的计算结果。如结点f的返回值为3。结点d的值为27,结点b的值为25,结点a的值为35



看文字不过瘾?点击我,进入周哥教IT视频教学
麦洛科菲长期致力于IT安全技术的推广与普及,我们更专业!我们的学员已经广泛就职于BAT360等各大IT互联网公司。详情请参考我们的 业界反馈 《周哥教IT.C语言深学活用》视频

我们的微信公众号,敬请关注