C++继承、多态与虚表分析

在面向对象的程序设计语言中,继承是一个非常重要的概念,使用继承和多态可以提高代码的可复用性和可维护性。本文主要总结了 C++ 中继承的实现细节,多态的概念和用途,以及虚函数继承时的虚表分析。

继承的概念

通过继承(inheritance)联系在一起的类构成一种层次关系,通常在层次关系的根部有一个基类(父类),其他类直接或间接地从基类继承而来,这些继承得到的类称为派生类(子类)。子类会继承父类的成员属性和成员方法。

下面是一个简单的实现继承的例子。

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
class Base   // 基类
{
public:
void fun1(){
cout << "Fun1()" << endl;
}
int m_a;
};

class Derived : public Base // 派生类,继承于 Base
{
public:
void fun2(){
cout << "Fun2()" << endl;
}
int m_b;
};

int main()
{
Derived derived;
derived.m_a = 10;
derived.m_b = 20;
derived.fun1();
derived.fun2();
return 0;
}

继承的权限分析

类成员的权限有三种:private、public、protected

  • private 私有成员,只能自身访问(当然C++中还有友元,这里先不考虑了)。
  • protected 保护成员,只能自身和非私有继承子类访问。
  • public 公有成员,可以在外部访问,可以由非私有继承的子类访问

这样描述起来有点乱,下面这张表对继承时的成员权限变化做出了总结

public 继承 protected 继承 private 继承
public 成员 public protected private
protected 成员 private private private
private 成员 不可访问 不可访问 不可访问

值得注意的还有一点,继承之后成员权限发生了变化,有些成员变得无法访问,但这并不意味着没有从父类那里继承对应的成员。

看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Base
{
private:
char m_c;
int m_a;
};

class Derived : public Base
{
private:
short m_b;
};

int main()
{
cout << sizeof(Derived) << endl;
return 0;
}

程序输出的结果为:

12

可见子类包含了父类的私有成员,但不能访问,并且通过字节对齐的规则可以知道,父类的成员在子类成员之前。(有关字节对齐的详细内容请移步这里

同名隐藏

在子类中声明一个与父类成员同名的成员(函数指函数名和参数列表完全相同,否则就是重载)时,父类中的成员会被隐藏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Base
{
public:
void fun() {
cout << "Base::fun()" << endl;
}
};

class Derived : public Base
{
public:
void fun() {
cout << "Derived::fun()" << endl;
}
};

int main()
{
Derived d;
d.fun(); // 调用子类的fun
d.Base::fun(); // 调用父类的fun
return 0;
}

程序运行结果为:

Derived::fun()
Base::fun()

虚函数与函数重写

C++中可以用 virtual 关键字来声明虚函数,子类在继承父类后,可以重写此函数,这与上文提到的同名隐藏不同,重写就是完全覆盖父类中定义的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Base
{
public:
virtual void fun() { // fun 被声明为虚函数
cout << "Base::fun()" << endl;
}
};

class Derived : public Base
{
public:
void fun() {
cout << "Derived::fun()" << endl;
}
};

int main()
{
Derived d;
d.fun(); // 调用子类的fun
d.Base::fun(); // 调用父类的fun
return 0;
}

程序运行结果为:

Derived::fun()
Base::fun()

emmmm,这样看来和同名隐藏也没什么区别嘛,那么虚函数的意义在哪里呢?

貌似是这样,再来看下面的代码:

1
2
3
4
5
6
7
int main()
{
Derived d;
Base *pb = &d;
pb->fun();
return 0;
}

在使用没有 virtual 的代码时,程序运行的结果为:

Base::fun()

在使用没有 virtual 的代码时,程序运行的结果为:

Derived::fun()

在使用了 virtual 时,使用父类的指针指向子类对象,可以访问到子类的成员方法。这就是多态的基本实现方式。

纯虚函数

在 C++ 中可以声明纯虚函数,与普通虚函数不同的是,纯虚函数在父类中并没有给出明确的定义(即没有函数体),这就要求在子类中给出纯虚函数的定义。含有纯虚函数的类不能被实例化。

1
2
3
4
5
6
7
8
9
10
11
12
class Base
{
public:
virtual void fun() = 0; // 纯虚函数
};
class Derived
{
public:
void fun() {
cout << "fun()" << endl;
}
};

虚析构的意义

顾名思义,虚析构就是虚的析构函数,如果一个类的析构函数中有必须执行的操作(比如动态空间的释放),并且这个类有可能被其他类继承,则必须将父类的析构函数声明为 virtual.

在下面的例子中,演示了析构函数是否为虚时的不同效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Base
{
public:
~Base() {
cout << "~Base()" << endl;
}
};
class Derived : public Base
{
public:
~Derived() {
cout << "~Derived()" << endl;
}
};
int main()
{
Base *pb = new Derived;
delete pb;
return 0;
}

这是未声明为虚析构函数时的情况,程序运行的结果为

~Base()

只调用了父类的析构函数,而子类中有可能的内存释放等必须要进行的操作就被没有执行。

当把父类的析构函数声明为 virtual 时(ps:代码就不贴了哈),程序运行结果为

~Derived()
~Base()

这样就正确地执行了子类的析构函数。实际编程中,建议把有可能被继承的类的析构函数都声明为 virtual,因为你永远不知道以后会不会有人在子类中写独有的析构方法。

多继承

在 C++ 中,一个类可以继承于多个父类。

  • 父类构造的顺序与继承时的顺序有关,与构造函数参数初始化刘表无关。
  • 父类析构的顺序是构造的反顺序。
  • 虚继承的顺序会提前。
    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
    class Base1
    {
    public:
    Base1() {
    cout << "Base1()" << endl;
    }
    virtual ~Base1() {
    cout << "~Base1()" << endl;
    }
    };
    class Base2
    {
    public:
    Base2() {
    cout << "Base2()" << endl;
    }
    virtual ~Base2() {
    cout << "~Base2()" << endl;
    }
    };

    class Derived : public Base1, public Base2
    {
    public:
    Derived() {
    cout << "Derived" << endl;
    }
    ~Derived() {
    cout << "~Derived()" << endl;
    }
    };
    int main()
    {
    Derived d;
    return 0;
    }
    程序运行的结果是:

    Base1()
    Base2()
    Derived()
    ~Derived()
    ~Base2()
    ~Base1()

虚继承

虚继承主要解决的是多重继承时的二义性问题。如图所示:

图1

在类 D 中,会出现两份类 A 的成员,这显然是不合理的,不仅会浪费空间,还会导致二义性的问题,虚继承很好地解决了这一问题。

图2

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

class A
{
public:
int a;
};


class B : virtual public A
{
public:
int b;
};

class C : virtual public A
{
public:
int c;
};

class D : public B, public C
{
public:
int d;
};

int main()
{
D d;
d.a = 0; // 如果不用虚继承,这句会报错
d.b = 1;
d.c = 2;
d.d = 3;
return 0;
}

多态的作用

下面通过一个多态解决实际问题的案例来说明多态的作用

假设要开发一个画图程序,可以绘制不同的图形。

  • 定义一个 Shap 类,作为所有图形类的基类。
  • 每当需要增加一种图形时,不需要修改原先的代码,只需继承于 Shap 实现其中的 draw 方法即可。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class Shap
    {
    public:
    virtual void draw() = 0;
    };

    class Rect : public Shap
    {
    public:
    void draw() {
    cout << "Draw a rectangle" << endl;
    }
    };

    class Circle: public Shap
    {
    public:
    void draw() {
    cout << "Draw a circle" << endl;
    }
    };

虚函数表结构分析

虚函数表是 C++ 虚函数,虚继承这一系列机制的底层实现方式,我们来一步步揭开它神秘的面纱。

当声明一个虚函数时,对应的类会多出一个虚表指针,指向虚函数表所在的内存空间,虚函数表保存了类中所有虚函数的函数指针,当一个类继承父类并重写其中的虚函数时,实际上就是修改了虚表中对应的函数指针的指向。

下面的程序对虚函数表的结构进行了验证。

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
class Base
{
public:
virtual void fun1() {
cout << "Base::fun1()" << endl;
}
virtual void fun2() {
cout << "Base::fun2()" << endl;
}
};

typedef void(*Fun)(void);
typedef void* Ptr;

int main()
{
cout << "size of Base : " << sizeof(Base) << endl;
cout << "size of Derived : " << sizeof(Derived) << endl;

Base b;
cout << "address of b'vir-table : " << (Ptr*)(&b) << endl;
cout << "address of b'first vir-function : " << (Ptr*)*(Ptr*)(&b) << endl;
cout << "address of b'second vir-function : " << (Ptr*)*(Ptr*)(&b) + 1 << endl;

Fun pFun = NULL;

pFun = (Fun)*(Ptr*)*(Ptr*)(&b);
pFun();
pFun = (Fun)*((Ptr*)*(Ptr*)(&b) + 1);
pFun();
return 0;
}

程序运行的结果是:(ps:当然,地址不会大家都一样)

size of Base : 8
size of Derived : 8
address of b’vir-table : 0x7ffdca2c0c50
address of b’first vir-function : 0x55c426c25d90
address of b’second vir-function : 0x55c426c25d98
Base::fun1()
Base::fun2()

如果上面的指针变换过程看的一头雾水,可以对照下面的图进行分析。

图3

当子类有函数重写时,类图及虚表结构图如下:

图4

图5

当多继承时,类图及虚表结构图如下:

图6

图7

最后附上一段多继承时验证虚表结构的代码

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
class Base1
{
public:
virtual void fun1() {
cout << "Base1::fun1()" << endl;
}
virtual void fun2() {
cout << "Base2::fun2()" << endl;
}
};

class Base2
{
public:
virtual void fun3() {
cout << "Base3::fun3()" << endl;
}
virtual void fun4() {
cout << "Base4::fun4()" << endl;
}
};


class Derived : public Base1, public Base2
{
public:
void fun1() {
cout << "Derived::fun1()" << endl;
}
void fun3() {
cout << "Derived::fun3()" << endl;
}
};

typedef void(*Fun)(void);
typedef void* Ptr;

int main()
{
cout << "size of Derived : " << sizeof(Derived) << endl;

Derived d;
cout << "address of d'vir-table1 : " << (Ptr*)(&d) << endl;
cout << "address of d'first vir-function : " << (Ptr*)*(Ptr*)(&d) << endl;
cout << "address of d'second vir-function : " << (Ptr*)*(Ptr*)(&d) + 1 << endl;

cout << "address of b'vir-table2 : " << (Ptr*)(&d) + 1 << endl;
cout << "address of d'first vir-function : " << (Ptr*)*((Ptr*)(&d) + 1) << endl;
cout << "address of d'second vir-function : " << (Ptr*)*((Ptr*)(&d) + 1) + 1 << endl;


Fun pFun = NULL;

pFun = (Fun)*(Ptr*)*(Ptr*)(&d);
pFun();
pFun = (Fun)*((Ptr*)*(Ptr*)(&d) + 1);
pFun();
pFun = (Fun)*((Ptr*)*((Ptr*)(&d) + 1));
pFun();
pFun = (Fun)*((Ptr*)*((Ptr*)(&d) + 1) + 1);
pFun();
return 0;
}

End,如有不当之处,望指正。