C++继承、多态与虚表分析
在面向对象的程序设计语言中,继承是一个非常重要的概念,使用继承和多态可以提高代码的可复用性和可维护性。本文主要总结了 C++ 中继承的实现细节,多态的概念和用途,以及虚函数继承时的虚表分析。
继承的概念
通过继承(inheritance)联系在一起的类构成一种层次关系,通常在层次关系的根部有一个基类(父类),其他类直接或间接地从基类继承而来,这些继承得到的类称为派生类(子类)。子类会继承父类的成员属性和成员方法。
下面是一个简单的实现继承的例子。
1 | class Base // 基类 |
继承的权限分析
类成员的权限有三种:private、public、protected
- private 私有成员,只能自身访问(当然C++中还有友元,这里先不考虑了)。
- protected 保护成员,只能自身和非私有继承子类访问。
- public 公有成员,可以在外部访问,可以由非私有继承的子类访问
这样描述起来有点乱,下面这张表对继承时的成员权限变化做出了总结
public 继承 | protected 继承 | private 继承 | |
---|---|---|---|
public 成员 | public | protected | private |
protected 成员 | private | private | private |
private 成员 | 不可访问 | 不可访问 | 不可访问 |
值得注意的还有一点,继承之后成员权限发生了变化,有些成员变得无法访问,但这并不意味着没有从父类那里继承对应的成员。
看下面的例子:
1 | class Base |
程序输出的结果为:
12
可见子类包含了父类的私有成员,但不能访问,并且通过字节对齐的规则可以知道,父类的成员在子类成员之前。(有关字节对齐的详细内容请移步这里)
同名隐藏
在子类中声明一个与父类成员同名的成员(函数指函数名和参数列表完全相同,否则就是重载)时,父类中的成员会被隐藏。
1 | class Base |
程序运行结果为:
Derived::fun()
Base::fun()
虚函数与函数重写
C++中可以用 virtual 关键字来声明虚函数,子类在继承父类后,可以重写此函数,这与上文提到的同名隐藏不同,重写就是完全覆盖父类中定义的函数。
1 | class Base |
程序运行结果为:
Derived::fun()
Base::fun()
emmmm,这样看来和同名隐藏也没什么区别嘛,那么虚函数的意义在哪里呢?
貌似是这样,再来看下面的代码:
1 | int main() |
在使用没有 virtual 的代码时,程序运行的结果为:
Base::fun()
在使用没有 virtual 的代码时,程序运行的结果为:
Derived::fun()
在使用了 virtual 时,使用父类的指针指向子类对象,可以访问到子类的成员方法。这就是多态的基本实现方式。
纯虚函数
在 C++ 中可以声明纯虚函数,与普通虚函数不同的是,纯虚函数在父类中并没有给出明确的定义(即没有函数体),这就要求在子类中给出纯虚函数的定义。含有纯虚函数的类不能被实例化。
1 | class Base |
虚析构的意义
顾名思义,虚析构就是虚的析构函数,如果一个类的析构函数中有必须执行的操作(比如动态空间的释放),并且这个类有可能被其他类继承,则必须将父类的析构函数声明为 virtual.
在下面的例子中,演示了析构函数是否为虚时的不同效果。
1 | class Base |
这是未声明为虚析构函数时的情况,程序运行的结果为
~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
36class 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()
虚继承
虚继承主要解决的是多重继承时的二义性问题。如图所示:
在类 D 中,会出现两份类 A 的成员,这显然是不合理的,不仅会浪费空间,还会导致二义性的问题,虚继承很好地解决了这一问题。
1 |
|
多态的作用
下面通过一个多态解决实际问题的案例来说明多态的作用
假设要开发一个画图程序,可以绘制不同的图形。
- 定义一个 Shap 类,作为所有图形类的基类。
- 每当需要增加一种图形时,不需要修改原先的代码,只需继承于 Shap 实现其中的 draw 方法即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class 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 | class Base |
程序运行的结果是:(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()
如果上面的指针变换过程看的一头雾水,可以对照下面的图进行分析。
当子类有函数重写时,类图及虚表结构图如下:
当多继承时,类图及虚表结构图如下:
最后附上一段多继承时验证虚表结构的代码
1 | class Base1 |
End,如有不当之处,望指正。