从零开始学C++之虚函数与多态(一):虚函数表指针、虚析构函数、object slicing与虚函数、C++对象模型图

一、多态

多态性是面向对象程序设计的重要特征之一。 多态性是指发出同样的消息被不同类型的对象接收时有可能导致完全不同的行为。 多态的实现:

函数重载 运算符重载 模板 虚函数

(1)、静态绑定与动态绑定

静态绑定

绑定过程出现在编译阶段,在编译期就已确定要调用的函数。

动态绑定

绑定过程工作在程序运行时执行,在程序运行时才确定将要调用的函数。

二、虚函数

虚函数的概念:在基类中冠以关键字 virtual 的成员函数 虚函数的定义:

virtual 函数类型 函数名称(参数列表);

如果一个函数在基类中被声明为虚函数,则他在所有派生类中都是虚函数

只有通过基类指针或引用调用虚函数才能引发动态绑定,包括通过基类指针的反引用调用虚函数,因为反引用一个指针将返回所指对象的引用。 虚函数不能声明为静态

(1)、虚函数表指针

虚函数的动态绑定是通过虚函数表(在静态数据区)来实现的。(虚函数表存放虚函数的函数指针) 包含虚函数的类对象头4个字节存放指向虚函数表的指针

注意:若不是虚函数,一般的函数不会出现在虚函数表,因为不用通过虚函数表指针间接去访问。

由于vptr在对象中的偏移不会随着派生层次的增加而改变,而且改写的虚函数在派生类vtable中的位置与它在基类vtable中的位置始终保持一致,有了这两条保证,再加上被改写虚函数与其基类中对应虚函数的原型和调用规范都保持一致,自然就能轻松地调用起实际所指对象的虚函数了。

#include <iostream>
using namespace std;


class Base
{
public:
    virtual void Fun1()
    {
        cout << "Base::Fun1 ..." << endl;
    }

    virtual void Fun2()
    {
        cout << "Base::Fun2 ..." << endl;
    }

    void Fun3() //被Derived继承后被隐藏
    {
        cout << "Base::Fun3 ..." << endl;
    }
};

class Derived : public Base
{
public:
    /*virtual */
    void Fun1()
    {
        cout << "Derived::Fun1 ..." << endl;
    }

    /*virtual */void Fun2()
    {
        cout << "Derived::Fun2 ..." << endl;
    }

    void Fun3()
    {
        cout << "Derived::Fun3 ..." << endl;
    }
};

int main(void)
{
    Base *p;
    Derived d;

    p = &d;
    p->Fun1();      // Fun1是虚函数,基类指针指向派生类对象,调用的是派生类对象的虚函数(间接)
    p->Fun2();
    p->Fun3();      // Fun3非虚函数,根据p指针实际类型来调用相应类的成员函数(直接)

    Base &bs = d;
    bs.Fun1();
    bs.Fun2();
    bs.Fun3();

    d.Fun1();
    d.Fun2();
    d.Fun3();

    return 0;
}

sizeof(Base); 和 sizeof(Derived); 都是4个字节,其实就是虚表指针,据此可以画出对象的模型:

Derived类继承了Base类的虚函数Fun1,Fun2, 但又重新实现了,即覆盖了。程序中通过基类的指针或引用可以通过vptr间接访问到Derived::Fun1, Derived:::Fun2,但因为Fun3不是虚函数(基类的Fun3 被继承后被隐藏),故p->Fun3(); 和bs.Fun3(); 根据指针或引用的实际类型去访问,即访问到被Derived继承下来的基类Fun3。函数的覆盖与隐藏可以参考这里

三、几道c++面试题

1. 来看一道出错的题:

#include <iostream>
using namespace std;

class A
{
public:
    virtual void test()
    {
        cout<<"A::test()"<<endl;
    }
private:
    int i;
};

class B: public A
{
public:
    void test()
    {
        cout<<"B::test()"<<endl;
    }
private:
    int i;
};

void f(A* p, int len)
{
    for (int i = 0; i < len; i++)
    {
        p[i].test();
    }
}

int main(void)
{
    B b[3];
    f(b, 3);

    return 0;
}

只会输出一次B::test() 然后就崩溃了,因为第二次按A的大小去跨越,找到的不是第二个B的首地址。参考这里

而输出的是B::test() 是因为p[i] 可以看做指针的反引用,返回的是A对象的引用,故调用的是虚函数。

2. 再来看一道有些迷惑的题:

class A
{
public:
    A()
    {
        Print();
    }
    virtual void Print()
    {
        printf("A is constructed.\n");
    }
};

class B: public A
{
public:
    B()
    {
        Print();
    }

    virtual void Print()
    {
        printf("B is constructed.\n");
    }
};

int main(void)
{
    A *pA = new B();
    delete pA;

    return 0;
}

先后打印出两行:A is constructed. B is constructed. 调用B的构造函数时,先会调用B的基类A的构造函数。然后在A的构造函数里调用Print。由于此时实例的类型B的部分还没有构造好,本质上它只是A的一个实例,它的虚函数表指针指向的是类型A的虚函数表。因此此时调用的Print是A::Print,而不是B::Print。接着调用类型B的构造函数,并调用Print。此时已经开始构造B,因此此时调用的Print是B::Print。

同样是调用虚拟函数Print,我们发现在类型A的构造函数中,调用的是A::Print,在B的构造函数中,调用的是B::Print。因此虚函数在构造函数中,已经失去了虚函数的动态绑定特性。

这一点跟java是有所不同的,看下面的程序输出:

class Fu // extends Object {     Fu()     {         super();         show();         return;     }     void show()     {         System.out.println("fu show");     } } class Zi extends Fu {     int num = 8;     Zi()     {         super();         //-->通过super初始化父类内容时,子类的成员变量并未显示初始化。等super()父类初始化完毕后,         //才进行子类的成员变量显示初始化。         System.out.println("zi run...." + num);         num = 20;         return;     }     void show()     {         System.out.println("zi show..." + num);     } } class Welcome {     public static void main(String[] args)     {         Zi z = new Zi();         z.show();     } }

输出为:

zi show...0 zi run....8 zi show...20

java的method 默认都是dynamic binding的,在父类中调用show()已经是被覆盖了的子类.show().

如果在父类.show()前面加上private或者 private final,第一行输出为fu show.  此时父类.show()并没有被覆盖.

但如果只单独加final 修饰,会编译出错,因为不允许被覆盖.

3.再来看一道默认参数与虚函数相关的题:

class A
{
public:
    virtual void Fun(int number = 10)
    {
        std::cout << "A::Fun with number " << number;
    }
};

class B: public A
{
public:
    virtual void Fun(int number = 20)
    {
        std::cout << "B::Fun with number " << number;
    }
};

int main()
{
    B b;
    A &a = b;
    a.Fun();
}

输出B::Fun with number 10。由于a是一个指向B实例的引用,因此在运行的时候会调用B::Fun。但缺省参数是在编译期决定的。在编译的时候,编译器只知道a是一个类型a的引用,具体指向什么类型在编译期是不能确定的,因此会按照A::Fun的声明把缺省参数number设为10。

4. 在普通成员函数里调用虚函数?

class Base
{
public:
    void print()
    {
        doPrint();
    }

private:
    virtual void doPrint()
    {
        cout << "Base::doPrint" << endl;
    }
};

class Derived : public Base
{
private:
    virtual void doPrint()
    {
        cout << "Derived::doPrint" << endl;
    }
};

int _tmain(int argc, _TCHAR *argv[])
{
    Base b;
    b.print();

    Derived d;
    d.print();

    return 0;
}

输出两行,分别是Base::doPrint和Derived::doPrint。在print中调用doPrint时,doPrint()的写法和this->doPrint()是等价的,因此将根据实际的类型调用对应的doPrint。所以结果是分别调用的是Base::doPrint和Derived::doPrint2。

四、虚析构函数

何时需要虚析构函数? 当你可能通过基类指针删除派生类对象时 如果你打算允许其他人通过基类指针调用对象的析构函数(通过delete这样做是正常的),并且被析构的派生类对象是有重要的析构函数需要执行,就需要让基类的析构函数作为虚函数。

#include <iostream>
using namespace std;


class Base
{
public:
    virtual void Fun1()
    {
        cout << "Base::Fun1 ..." << endl;
    }

    virtual void Fun2()
    {
        cout << "Base::Fun2 ..." << endl;
    }

    void Fun3()
    {
        cout << "Base::Fun3 ..." << endl;
    }

    Base()
    {
        cout << "Base ..." << endl;
    }
    // 如果一个类要做为多态基类,要将析构函数定义成虚函数
    virtual ~Base()
    {
        cout << "~Base ..." << endl;
    }
};

class Derived : public Base
{
public:
    /*virtual */
    void Fun1()
    {
        cout << "Derived::Fun1 ..." << endl;
    }

    /*virtual */void Fun2()
    {
        cout << "Derived::Fun2 ..." << endl;
    }

    void Fun3()
    {
        cout << "Derived::Fun3 ..." << endl;
    }
    Derived()
    {
        cout << "Derived ..." << endl;
    }
    /*  virtual*/ ~Derived() //即使没有virtual修饰,也是虚函数
    {
        cout << "~Derived ..." << endl;
    }
};

int main(void)
{
    Base *p;
    p = new Derived;

    p->Fun1();
    delete p; //通过基类指针删除派生类对象

    return 0;
}

即通过delete 基类指针删除了派生类对象(执行派生类析构函数),此时就好像delete 派生类指针 效果一样。如果基类析构函数没有声明为virtual,

此时只会输出~Base。

五、object slicing与虚函数

首先看下图的继承体系:

#include <iostream>
using namespace std;

class CObject
{
public:
    virtual void Serialize()
    {
        cout << "CObject::Serialize ..." << endl;
    }
};

class CDocument : public CObject
{
public:
    int data1_;
    void func()
    {
        cout << "CDocument::func ..." << endl;
        Serialize();
    }
    virtual void Serialize()
    {
        cout << "CDocument::Serialize ..." << endl;
    }
    CDocument()
    {
        cout << "CDocument()" << endl;
    }
    ~CDocument()
    {
        cout << "~CDocument()" << endl;
    }
    CDocument(const CDocument &other)
    {
        data1_ = other.data1_;
        cout << "CDocument(const CDocument& other)" << endl;
    }
};

class CMyDoc : public CDocument
{
public:
    int data2_;
    virtual void Serialize()
    {
        cout << "CMyDoc::Serialize ..." << endl;
    }
};

int main(void)
{
    CMyDoc mydoc;
    CMyDoc *pmydoc = new CMyDoc;

    cout << "#1 testing" << endl;
    mydoc.func();

    cout << "#2 testing" << endl;
    ((CDocument *)(&mydoc))->func();

    cout << "#3 testing" << endl;
    pmydoc->func();

    cout << "#4 testing" << endl;
    ((CDocument)mydoc).func();      //mydoc对象强制转换为CDocument对象,向上转型
    // 将派生类对象转化为了基类对象
    //vptr 指向基类的虚函数表
    delete pmydoc;

    return 0;
}

由于Serialize是虚函数,根据this指针指向的真实对象,故前3个testing输出都是CMyDoc::Serialize ...但第4个testing中发生了Object Slicing,即对象切

割,将CMyDoc对象转换成基类CDocument对象时,调用了CDocument类的拷贝构造函数,CMyDoc类的额外成员如data2_消失,成为完全一个

CDocument对象,包括vptr 也指向基类的虚函数表,故输出的是CDocument::Serialize ...

此外还可以看到,调用了两次CDocument构造函数和一次CDocument 拷贝构造函数,CDocument析构函数被调用3次。

六、C++对象模型图

Rectangle 继承自Shape类,Shape的析构函数为虚函数,draw为纯虚函数

参考:

C++ primer 第四版 Effective C++ 3rd C++编程规范

《高质量程序设计指南》

http://zhedahht.blog.163.com/

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏有趣的Python

7-Java面向对象-多态

本次课程围绕: 什么是多态? 多态在程序设计中的优势? 在Java中如何实现多态?

1054
来自专栏小二的折腾日记

《effective C++》from line 1 to line 12

包含着最初的以c语言为基础的C,面向对象的C++,C++的泛型编程,以及STL。在我们使用的过程中,可能会穿插,但是我们需要根据不同的情况使用不同的策略。

673
来自专栏菩提树下的杨过

[基础]Javascript中的继承示例代码

面向对象的语言必须具备四个基本特征: 1.封装能力(即允许将基本数据类型的变量或函数放到一个类里,形成类的成员或方法) 2.聚合能力(即允许类里面再包含类,...

1868
来自专栏智能算法

Python学习(七)---- 面向对象学习(类)

原文地址: https://blog.csdn.net/fgf00/article/details/52449707 编辑:智能算法,欢迎关注!

702
来自专栏ccylovehs

JavaScript基础回顾一(类型、值和变量)

没有答对也不要灰心,本文会巩固你的基础知识,后续会有系列的基础回顾知识,以飨诸君!

542
来自专栏Java帮帮-微信公众号-技术文章全总结

第七天 自定义数据类型ArrayList集合【悟空教程】

1375
来自专栏Java帮帮-微信公众号-技术文章全总结

09(01)总结final,多态,抽象类,接口

1:final关键字(掌握) (1)是最终的意思,可以修饰类,方法,变量。 (2)特点: A:它修饰的类,不能被继承。 B:它修饰的方法,不能被重写。 ...

2835
来自专栏前端知识分享

第195天:js---函数对象详解(call、apply)

903
来自专栏前端架构与工程

【译】《Understanding ECMAScript6》- 第五章-Class

目录 ES5中的拟Class结构 Class声明 Class表达式 存储器属性 静态成员 派生类 new.target 总结 自JavaScript面世以来,许...

2096
来自专栏青枫的专栏

java基础学习_面向对象(下)02_day09总结

============================================================================= ==...

612

扫码关注云+社区