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

一、多态

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

函数重载

运算符重载

模板

虚函数

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

静态绑定

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

动态绑定

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


二、虚函数

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

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

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

只有通过基类指针或引用调用虚函数才能引发动态绑定
虚函数不能声明为静态


(1)、虚函数表指针

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

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


 

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
 
#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;
}

 

 

从零开始学C++之虚函数与多态(一):虚函数表指针、虚析构函数、object slicing与虚函数_第1张图片


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

从零开始学C++之虚函数与多态(一):虚函数表指针、虚析构函数、object slicing与虚函数_第2张图片

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


三、虚析构函数

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


 

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
 
#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;
}

 

 

从零开始学C++之虚函数与多态(一):虚函数表指针、虚析构函数、object slicing与虚函数_第3张图片

即通过delete 基类指针删除了派生类对象(执行派生类析构函数)。


四、object slicing与虚函数

首先看下图的继承体系:
从零开始学C++之虚函数与多态(一):虚函数表指针、虚析构函数、object slicing与虚函数_第4张图片


 

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
 
#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对象,向上转型
     //完完全全将派生类对象转化为了基类对象
     //包括虚函数表也变成基类的虚表
     delete pmydoc;

     return  0;
}

 

 

从零开始学C++之虚函数与多态(一):虚函数表指针、虚析构函数、object slicing与虚函数_第5张图片


由于Serialize是虚函数,故前3个testing输出都是CMyDoc::Serialize ...但第4个testing中发生了Object Slicing,即对象切割,将CMyDoc对象转换成基类CDocument对象时,调用了CDocument类的拷贝构造函数,CMyDoc类的额外成员如data2_消失,成为完全一个CDocument对象,包括虚表也变成基类的虚表,故输出的是CDocument::Serialize ...

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


 

参考:

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


你可能感兴趣的:(object)