继承

访问属性

私有 private

仅能在类内访问,不可继承,子类不可访问。

保护 protected

仅能在类内访问,可继承,子类可访问。

公有 public

可在类内或类外访问,可继承,子类可访问。

继承方式

私有继承 private

父类中的public、protected成员以private属性继承至子类中。(访问权限降为private)

保护继承 protected

父类中的public、protected成员以protected属性继承至子类中。(访问权限降为protected)

公有继承 public

父类中的public、protected成员以原有属性继承至子类中。(保持原有访问权限)

类型转换

一个公有派生类的对象在使用上可以被当作基类的对象,反之则不可。
具体表现在:

  • 派生类的对象可以隐含转换为基类对象。
  • 派生类的对象可以初始化基类的引用。
  • 派生类的指针可以隐含转换为基类的指针。

通过基类对象名、指针只能使用从基类继承的成员,派生类新增的成员不能使用。

构造函数与析构函数

构造函数在创建该类对象时调用。必须使用new、malloc等创建该类对象的指针或引用或传值传参传入参数时才会调用,直接构建对象不会调用。

析构函数在删除该类对象时调用。必须使用delete、free删除该类对象的指针或引用或指针&引用超出作用域时才会调用,


多继承

构造顺序与析构顺序

构造函数依照继承父类的顺序调用,最后构造子类。

析构函数依照继承父类的逆序调用,最先析构子类。

例如:

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
class A
{
public:
char* a;
A(){
a = new char('A');
cout<<"Create A"<<endl;
}

~A() {
delete a;
cout << "Destroy A" << endl;
}
};

class B
{
public:
int* number;
B(int n):number(new int(n)){cout<<"Create B"<<endl;}

virtual ~B() {
delete number;
cout << "Destroy B" << endl;
}
};

class C : public A,public B
{
public:
C():A(),B(2){cout<<"Create C"<<endl;}

virtual ~C() {
cout << "Destroy C" << endl;
}
};

int main()
{
C* c2 = new C();
delete c2;
return 0;
}

输出:

1
2
3
4
5
6
Create A
Create B
Create C
Destroy C
Destroy B
Destroy A

菱形继承与虚基类

二义性

问题:当多继承的两个父类有相同名称的成员变量或函数时,会产生二义性问题。

二义性就是编译器不知道程序员指出的变量具体是哪个类中的变量,于是编译器就会报错。

可以在变量前使用类限定符,标识清楚是哪个类中的变量。

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
class A
{
public:
int number;
A(int n):number(n){
cout<<"Create A"<<endl;
}

~A() {
cout << "Destroy A" << endl;
}
};

class B
{
public:
int number;
B(int n):number(n){cout<<"Create B"<<endl;}

virtual ~B() {
cout << "Destroy B" << endl;
}
};

class C : public A,public B
{
public:
C(int n):A(n*n),B(n){cout<<"Create C"<<endl;}

virtual ~C() {
cout << "Destroy C" << endl;
}
};

int main()
{
C* c = new C(4);

//cout << "number in B = " << c->number << endl; Error 错误: C::number不明确
cout << "number in A = " << c->A::number << endl;
cout << "number in B = " << c->B::number << endl;

delete c;
return 0;
}

菱形继承

当子类继承了多个基类,而这些基类又继承自同一基类,就会造成子类中存在多个间接基类的成员。当访问该成员时会造成冗余,与数据不一致性。

添加一个A类和B类的共同基类。

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
class VirtualClass {
public:
int vInt;
VirtualClass(int n) : vInt(n){
cout << "Create VirtualClass" << endl;
}

~VirtualClass(){
cout << "Delete VirtualClass" << endl;
}
};

class A : public VirtualClass
{
public:
int number;
A(int n):number(n),VirtualClass(n+2){
cout<<"Create A"<<endl;
}

~A() {
cout << "Destroy A" << endl;
}
};

class B : public VirtualClass
{
public:
int number;
B(int n):number(n), VirtualClass(n + 1) {cout<<"Create B"<<endl;}

virtual ~B() {
cout << "Destroy B" << endl;
}
};

class C : public A,public B
{
public:
C(int n):A(n*n),B(n){cout<<"Create C"<<endl;}

virtual ~C() {
cout << "Destroy C" << endl;
}
};

int main()
{
C* c = new C(4);

//cout << "number in B = " << c->number << endl; Error 错误: C::number不明确
cout << "number in A = " << c->A::number << endl;
cout << "number in B = " << c->B::number << endl;
cout << "number in (A)BaseClass = " << c->A::vInt << endl;
cout << "number in (B)BaseClass = " << c->B::vInt << endl;
delete c;
return 0;
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Create VirtualClass
Create A
Create VirtualClass
Create B
Create C
number in A = 16
number in B = 4
number in (A)BaseClass = 18
number in (B)BaseClass = 5
Destroy C
Destroy B
Delete VirtualClass
Destroy A
Delete VirtualClass

根据输出和调试结果可以得出以下结论:
没有使用虚继承时,子类的内存中包含两个继承的基类,每个基类中又各有一份间接基类的对象数据

未使用虚继承时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 A : virtual public VirtualClass //使用虚继承
{
public:
int number;
A(int n):number(n),VirtualClass(n+2){
cout<<"Create A"<<endl;
}

~A() {
cout << "Destroy A" << endl;
}
};

class B : virtual public VirtualClass //使用虚继承
{
public:
int number;
B(int n):number(n), VirtualClass(n + 1) {cout<<"Create B"<<endl;}

virtual ~B() {
cout << "Destroy B" << endl;
}
};

class C : public A,public B
{
public:
C(int n):A(n*n),B(n),VirtualClass(n*n+n+2){cout<<"Create C"<<endl;} //添加VirtualClass的构造函数

virtual ~C() {
cout << "Destroy C" << endl;
}
};

使用虚继承后,C的内部结构发生了变化。子类对象中包含了A、B、VirtualClass的对象数据,A、B两个类中创建了VirtualClass的指针,指向C中的VirtualClass。避免了数据的冗余,解决了不一致性。

使用虚继承时C类对象的内存示意图

总结

  • 虚基类解决的问题:
    当派生类从多个基类派生,而这些基类又有共同基类,则在访问此共同基类中的成员时,将产生冗余,并有可能因冗余带来不一致性。

  • 虚基类的作用:
    主要用来解决多继承时可能发生的对同一基类继承多次而产生的二义性问题。
    为最远的派生类提供唯一的基类成员,而不重复产生多次复制

  • 一些需注意的点:
    在第一级继承时就要将共同基类设计为虚基类。
    虚基类的成员是由最远派生类(建立对象时所指定的类,示例中的C类)的构造函数通过调用虚基类的构造函数进行初始化的。


虚继承的底层原理

是时候谈一谈虚继承的底层原理了。

上一小节中的示意图明显展示了A、B类中各有一个指向同一个虚基类的指针。

准确说,这个指针就是虚基表指针(vbptr),指向的是虚基表。虚基表中的记录了到虚基类的内存偏移量,根据这一偏移量能够找到虚基类的指针,然后就可以访问数据了。

首先使用开发者命令查看C类的内部结构

Shell
1
cl /d1reportSingleClassLayoutC main.cpp
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
// 32位系统
class C size(24):
+---
0 | +--- (base class B)
0 | | {vfptr}
4 | | {vbptr}
8 | | number
| +---
12 | +--- (base class A)
12 | | {vbptr}
16 | | number
| +---
+---
+--- (virtual base VirtualClass)
20 | vInt
+---
// 虚函数表
C::$vftable@:
| &C_meta
| 0
0 | &C::{dtor}

//A的虚基表,记录A类到虚基类地址的偏差值,这里是8(20-12 = 8)
C::$vbtable@A@:
0 | 0
1 | 8 (Cd(A+0)VirtualClass)
//B的虚基表,记录B类到虚基类地址的偏差值,这里是16(20-4 = 16)
C::$vbtable@B@:
0 | -4
1 | 16 (Cd(B+4)VirtualClass)

接着,在VS中进行一次调试。找到C实例对象的地址

1
2
3
4
5
6
7
8
9
10
11
12
//下面是C对象的存储地址
0x008FFE44 b8 ab e1 00 ???.//c地址
0x008FFE48 cc ab e1 00 ???.// c->B的虚基表指针
0x008FFE4C 04 00 00 00 ....// c->B::number
0x008FFE50 c0 ab e1 00 ???.// c->A的虚基表指针
0x008FFE54 10 00 00 00 ....// c->A::number
0x008FFE58 16 00 00 00 ....//c->vInt地址

// A的虚基表指针指向的偏移量 16进制 转换为10进制为 8,与上面类内结构所示相同
0x00E1ABC0 00 00 00 00 08 00 00 00 ........
// B的虚基表指针指向的偏移量 16进制 转换为10进制为 16,与上面的类内结构所示相同
0x00E1ABCC fc ff ff ff 10 00 00 00 ?.......

结论

  • 可以看到当我们使用虚继承时,虚继承父类的子类中会生成一个虚基表指针,指向虚基表。
  • 我们可能使用父类(示例中的A类或B类)的引用指向子类(示例中的C类)。此时,访问间接父类的数据,就会通过父类中虚基表内存偏移量找到间接父类的指针,进一步访问其中的数据。

据此,可以再优化一下上面的内存结构图:

使用虚继承时C类对象的内存示意图