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
37
38
39
40
41
42
43
44
45
46
47
48
#include <iostream>

using namespace std;

// 定义基类(父类)
class Parent {

public:
Parent(int a = 0, int b = 0) {
this->a = a;
this->b = b;
}

void print() {
cout << "a=" << this->a << ", b=" << this->b << endl;
}

public:
int a;
int b;
};

// 定义派生类(子类),继承方式是 public
class Child : public Parent {

public:

Child(int a = 0, int b = 0, int c = 0) {
// 直接访问基类(父类)的 public 成员变量
this->a = a;
this->b = b;
this->c = c;
}

void echo() {
cout << "a=" << this->a << ", b=" << this->b << ", c=" << this->c << endl;
}

private:
int c;
};

int main() {
Child child(1, 2, 3);
child.print(); // 直接调用基类(父类)的 public 成员函数
child.echo(); // 直接调用派生类(子类)的 public 成员函数
return 0;
}

程序运行的输出结果如下:

1
2
a=1, b=2
a=1, b=2, c=3

派生类的构造过程

  • 派生类对象构造的过程:

    • 派生类先调用基类的构造函数,初始化从基类继承来的成员。
    • 派生类后调用自己的构造函数,初始化派生类自己特有的成员。
  • 派生类对象析构的过程:

    • 派生类先调用自己的析构函数,释放派生类成员可能占用的外部资源(堆内存,文件等)。
    • 派生类后调用基类的析构函数,释放派生类内存中从基类继承来的成员可能占用的外部资源(堆内存、文件等)。

总结

在 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
37
38
39
40
41
#include <iostream>

using namespace std;

class Base {

public:
Base(int data) : ma(data) {
cout << "Base()" << endl;
}

~Base() {
cout << "~Base()" << endl;
}

protected:
int ma;

};

class Device : public Base {

public:
// 显式调用 Base 的构造函数初始化 ma
Device(int data) : Base(data), mb(data) {
cout << "Device()" << endl;
}

~Device() {
cout << "~Device()" << endl;
}

private:
int mb;

};

int main() {
Device d(2); // 先构造的后析构,即后构造的先析构
return 0;
}

程序运行的输出结果如下:

1
2
3
4
Base()
Device()
~Device()
~Base()

重载、重写、隐藏

  • 重载关系

    • 一组函数要重载,必须处在同一个作用域当中,且函数名字相同,但参数列表不同。
  • 隐藏关系

    • 在继承结构当中,派生类的同名成员会将基类的同名成员给隐藏掉,这里的隐藏是指作用域的隐藏。
  • 重写关系

    • 基类和派生类的函数,其函数名、返回值以及参数列表都相同,而且基类的方法是虚函数,那么派生类的方法就会被编译器自动处理成虚函数,它们之间成为重写(覆盖)关系。

案例代码一

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
#include <iostream>

using namespace std;

class Base {

public:
Base(int data = 10) : ma(data) {

}

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

void show(int a) {
cout << "Base::show(int)" << endl;
}

private :
int ma;

};

class Device : public Base {

public:
Device(int data = 20) : Base(data), mb(data) {

}

void show() {
cout << "Device::show()" << endl;
}

private:
int mb;

};

int main() {
Device device;
device.show();
// device.show(20); // 错误写法,派生类的 show() 函数将基类的 show(int a) 函数隐藏了,无法正常调用基类的同名函数
device.Base::show(20); // 正确写法,派生类加上作用域可以正常调用基类的同名函数
return 0;
}

程序运行的输出结果如下:

1
2
Device::show()
Base::show()

案例代码二

  • 在 C++ 的继承结构中,通常会说成是从上(基类)到下(派生类)的结构

    • 从上到下,即基类对象 -> 派生类对象
    • 从下到上,即派生类对象 -> 基类对象
  • C++ 中的多态表现

    • 基类指针(引用)-> 派生类对象

特别注意

在 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
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
#include <iostream>

using namespace std;

class Base {

public:
Base(int data = 10) : ma(data) {

}

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

void show(int a) {
cout << "Base::show(int)" << endl;
}

private :
int ma;

};

class Device : public Base {

public:
Device(int data = 20) : Base(data), mb(data) {

}

void show() {
cout << "Device::show()" << endl;
}

private:
int mb;

};

void test01() {
cout << "=========== test01() ===========" << endl;
Base base;
Device device;
base = device; // 正确写法,类型从下到上的转换
// device = base; // 错误写法,类型从上到下的转换
base.show();
base.show(30);
}

void test02() {
cout << "=========== test02() ===========" << endl;
Base base;
Device device;

Base *_base = &device; // 正确写法,类型从下到上的转换
// Device *_device = &base; // 错误写法,类型从上到下的转换
_base->show();
_base->show(40);
}

int main() {
test01();
test02();
return 0;
}

程序运行的输出结果如下:

1
2
3
4
5
6
=========== test01() ===========
Base::show()
Base::show(int)
=========== test02() ===========
Base::show()
Base::show(int)

虚函数

虚函数的概念

  • (1) 当一个类里面定义了虚函数,那么在编译阶段,编译器会给这个类的类型产生一个唯一的 vftable 虚函数表,虚函数表中主要存储的内容是 RTTI 指针和虚函数的地址(如图所示)。当程序运行时,每一张虚函数表都会加载到内存的 .rodata 区。

  • (2) 当一个类里面定义了虚函数,那么这个类定义的对象在其运行时,内存中开始的部分会多存储一个 vfptr 虚函数指针(占 4 字节大小),它指向相应类型的虚函数表 vftable。一个类定义 N 个对象,它们的 vfptr 虚函数指针指向的都是同一张虚函数表。

  • (3) 如果派生类中的函数和从基类继承来的某个函数,其函数名、返回值、参数列表都相同,而且基类的函数是 virtual 关键字修饰的,那么派生类的这个函数会被编译器自动处理成虚函数。

  • (4) 一个类里面虚函数的个数,不影响类对象的内存大小(vfptr 虚函数指针永远只占用 4 个字节大小),影响的是虚函数表的大小。

  • (5) 在构造函数中调用虚函数,不会发生动态绑定。简而言之,在构造函数调用的函数,都是静态绑定。

哪些函数不能实现成虚函数

  • (1) 构造函数不能实现成虚函数。
  • (2) static 关键字修饰的函数不能实现成虚函数。

静态绑定(普通函数)

  • 静态(编译时期)绑定,使用的是普通函数
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
#include <iostream>
#include <typeinfo>

using namespace std;

class Base {

public:
Base(int data = 10) : ma(data) {

}

// 普通函数(非虚函数)
void show() {
cout << "Base::show()" << endl;
}

// 普通函数(非虚函数)
void show(int a) {
cout << "Base::show(int a)" << endl;
}

private:
int ma;

};

class Device : public Base {

public:
Device(int data = 20) : Base(data), mb(data) {

}

void show() {
cout << "Device::show()" << endl;
}

private:
int mb;

};

int main() {
Device device(50);

Base *pb = &device;
pb->show(); // 静态(编译时期)绑定(函数的调用)

cout << sizeof(Base) << endl;
cout << sizeof(Device) << endl;

cout << typeid(pb).name() << endl; // class Base *
cout << typeid(*pb).name() << endl; // class Base

return 0;
}

程序运行的输出结果如下:

1
2
3
4
5
Base::show()
4
8
class Base *
class Base

动态绑定(虚函数)

  • 动态(运行时期)绑定,使用的是虚函数
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
#include <iostream>
#include <typeinfo>

using namespace std;

class Base {

public:
Base(int data = 10) : ma(data) {

}

// 虚函数
virtual void show() {
cout << "Base::show()" << endl;
}

// 虚函数
virtual void show(int a) {
cout << "Base::show(int a)" << endl;
}

private:
int ma;

};

class Device : public Base {

public:
Device(int data = 20) : Base(data), mb(data) {

}

void show() {
cout << "Device::show()" << endl;
}

private:
int mb;

};

int main() {
Device device(50);

Base *pb = &device;
pb->show(); // 动态(运行时期)绑定(函数的调用)

cout << sizeof(Base) << endl;
cout << sizeof(Device) << endl;

cout << typeid(pb).name() << endl; // class Base *
cout << typeid(*pb).name() << endl; // class Device

return 0;
}

程序运行的输出结果如下:

1
2
3
4
5
Device::show()
16
16
class Base *
class Device

查看类的内存布局

在 Visual Studio 开发人员命令提示窗口内,可以使用以下命令查看类的内存布局信息,其中 YYY 是类的名称,xxx 是 C++ 源文件的名称。

1
cl /d1 reportSingleClassLayoutYYY xxx.cpp
  • 在上述动态绑定(带虚函数)的代码中,Base 类的内存布局如下:

  • 在上述动态绑定(带虚函数)的代码中,Device 类的内存布局如下:

提示

更多关于 Visual Studio 命令的详细使用教程,请阅读 《使用 VS 查看对象模型以及结构体内存对齐》

虚函数调用深入理解

思考问题

在 C++ 中,是不是虚函数的调用一定就是动态绑定?

在 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
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 Base {

public:
Base(int data = 0) : ma(data) {

}

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

protected:
int ma;

};

class Device : public Base {

public:
Device(int data = 0) : Base(data), mb(data) {

}

void show() {
cout << "Device::show()" << endl;
}

private :
int mb;

};

int main() {
Base base;
Device device;

cout << "=====================" << endl;

base.show(); // 静态绑定
device.show(); // 静态绑定

cout << "=====================" << endl;

Base *bptr1 = &base;
bptr1->show(); // 动态绑定(必须由指针或者引用调用虚函数)

Base *bptr2 = &device;
bptr2->show(); // 动态绑定(必须由指针或者引用调用虚函数)

Base &b1 = base;
base.show(); // 动态绑定(必须由指针或者引用调用虚函数)

Base &b2 = device;
b2.show(); // 动态绑定(必须由指针或者引用调用虚函数)

cout << "=====================" << endl;

Device *dptr = &device;
dptr->show(); // 动态绑定(虚函数通过指针或者引用变量调用,才会发生动态绑定)

Device &d = device;
d.show(); // 动态绑定(虚函数通过指针或者引用变量调用,才会发生动态绑定)

Device *dptr2 = (Device *) &base; // 强制类型转换(不安全)
dptr2->show(); // 动态绑定

return 0;
}

程序运行的输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
=====================
Base::show()
Device::show()
=====================
Base::show()
Device::show()
Base::show()
Device::show()
=====================
Device::show()
Device::show()
Base::show()

虚析构函数

虚析构函数的概念

  • (1) 析构函数可以是虚的,虚析构函数主要用于指引 delete 运算符正确析构动态对象。
  • (2) 构造函数不能是虚函数,因为建立一个派生类对象时,必须从类层次的根开始,沿着继承路径逐个调用基类的构造函数。
  • (3) 虚析构函数使得在删除指向派生类对象的基类指针时,可以调用派生类的析构函数来实现释放派生类中堆内存的目的,从而防止内存泄漏。
  • (4) 当基类的指针(引用)指向在堆上 new 出来的派生类对象的时候,基类的析构函数必须实现成虚析构函数。在调用虚析构函数的时候,必定会发生动态绑定,否则会导致派生类的析构函数无法被调用。

虚析构函数的使用

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
#include <iostream>

using namespace std;

class Base {

public:
Base(int data) : ma(data) {
cout << "Base()" << endl;
}

// 虚析构函数
virtual ~Base() {
cout << "~Base()" << endl;
}

// 虚函数
virtual void show() {
cout << "Base::show()" << endl;
}

private :
int ma;

};

class Device : public Base {

public:
Device(int data) : Base(data), mb(data) {
cout << "Device()" << endl;
}

// 当基类的析构函数是 virtual 虚函数,那么派生类的析构函数会自动成为虚函数
~Device() {
cout << "~Device()" << endl;
}

private:
int mb;

};

int main() {
Base *pb = new Device(10);
pb->show();
delete pb;

return 0;
}

程序运行的输出结果如下:

1
2
3
4
5
Base()
Device()
Base::show()
~Device()
~Base()

多态

多态的概念

  • 静态(编译时期)的多态

    • 函数重载
    • 模板(类模板与函数模板)
  • 动态(运行时期)的多态

    • 在类的继承结构中,基类指针(引用)指向派生类对象,通过该指针(引用)调用同名函数(虚函数),基类指针指向哪个派生类对象,就会调用哪个派生类对象的同名函数(虚函数)。
    • 动态多态的底层是通过动态绑定来实现的,涉及虚函数指针(vfptr)和虚函数表(vftable)。

多态的案例代码一

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
73
74
75
76
77
#include <iostream>
#include <cstring>

using namespace std;

class Animal {

public:
Animal(string name) : _name(name) {

}

// 虚函数
virtual void bark() {

}

protected:
string _name;

};

class Cat : public Animal {

public:
Cat(string name) : Animal(name) {

}

void bark() {
cout << _name << " bark: miao miao" << endl;
}

};

class Dog : public Animal {

public:
Dog(string name) : Animal(name) {

}

void bark() {
cout << _name << " bark: wang wang" << endl;
}

};

class Pig : public Animal {

public:
Pig(string name) : Animal(name) {

}

void bark() {
cout << _name << " bark: heng heng" << endl;
}

};

void bark(Animal &animal) {
// 动态多态(底层是通过动态绑定来实现的)
animal.bark();
}

int main() {
Cat cat = Cat("Cat");
Dog dog = Dog("Dog");
Pig pig = Pig("Pig");

bark(cat);
bark(dog);
bark(pig);

return 0;
}

程序运行的输出结果如下:

1
2
3
Cat bark: miao miao
Dog bark: wang wang
Pig bark: heng heng

多态的案例代码二

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
73
74
75
76
77
#include <iostream>
#include <cstring>

using namespace std;

class Animal {

public:
Animal(string name) : _name(name) {

}

// 虚函数
virtual void bark() {

}

protected:
string _name;

};

class Cat : public Animal {

public:
Cat(string name) : Animal(name) {

}

void bark() {
cout << _name << " bark: miao miao" << endl;
}

};

class Dog : public Animal {

public:
Dog(string name) : Animal(name) {

}

void bark() {
cout << _name << " bark: wang wang" << endl;
}

};

class Pig : public Animal {

public:
Pig(string name) : Animal(name) {

}

void bark() {
cout << _name << " bark: heng heng" << endl;
}

};

void bark(Animal *animal) {
// 动态多态(底层是通过动态绑定来实现的)
animal->bark();
}

int main() {
Cat cat = Cat("Cat");
Dog dog = Dog("Dog");
Pig pig = Pig("Pig");

bark(&cat);
bark(&dog);
bark(&pig);

return 0;
}

程序运行的输出结果如下:

1
2
3
Cat bark: miao miao
Dog bark: wang wang
Pig bark: heng heng

纯虚函数和抽象类

纯虚函数和抽象类的概念

基本概念:

  • (a) 纯虚函数是一个在基类中声明的虚函数,且在基类中没有被定义,要求任何派生类都必须定义自己的版本。
  • (b) 纯虚函数为各派生类提供一个公共界面,可以实现接口的封装和设计、软件的模块功能划分等。
  • (c) 纯虚函数的声明形式: virtual 类型 函数名 ( 参数表 ) = 0;
  • (d) 一个拥有纯虚函数的基类,通常称之为 “抽象类”。

使用限制:

  • (a) 可以声明抽象类的指针和引用。
  • (b) 抽象类不能创建对象(实例化)。
  • (c) 抽象类不能作为函数的参数类型和返回值类型。
  • (d) 如果基类中存在纯虚函数,那么派生类必须实现所有的纯虚函数,否则这个派生类也是一个抽象类。

特别注意

抽象类可以有构造函数。尽管抽象类无法被直接实例化(因为它包含至少一个纯虚函数),但它仍然可以定义构造函数,用于初始化抽象类的成员变量或执行其他构造逻辑。这样,当抽象类被继承并通过派生类构造时,抽象类的构造函数会被调用。同理,抽象类也可以有析构函数。

纯虚函数和抽象类的案例一

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#include <iostream>

using namespace std;

// 抽象类
class Figure {

public:
// 声明纯虚函数,计算面积
virtual double getArea() = 0;
};

class Circle : public Figure {

public:

Circle(double r) {
this->r = r;
}

// 实现纯虚函数,计算圆的面积
virtual double getArea() {
double area = 3.14 * r * r;
cout << "圆的面积: " << area << endl;
return area;
}

private:
double r;
};

class Triangle : public Figure {

public:
Triangle(double a, double b) {
this->a = a;
this->b = b;
}

// 实现纯虚函数,计算三角形的面积
virtual double getArea() {
double area = a * b / 2;
cout << "三角形的面积: " << area << endl;
return area;
}

private:
double a;
double b;
};

class Square : public Figure {

public:
Square(double a, double b) {
this->a = a;
this->b = b;
}

// 实现纯虚函数,计算四边形的面积
virtual double getArea() {
double area = a * b;
cout << "四边形的面积: " << area << endl;
return area;
}

private:
double a;
double b;
};

void printArea(Figure* base) {
base->getArea();
}

int main() {
// Figure f; // 错误写法,抽象类不能实例化

Triangle Triangle(20, 30);
Square square(50, 60);

// 可以声明抽象类的指针
Figure* pBase = new Circle(5.3);
pBase->getArea();

// 可以声明抽象类的引用
Figure& base = square;
base.getArea();

printArea(&Triangle);

return 0;
}

程序运行的输出结果如下:

1
2
3
圆的面积: 88.2026
四边形的面积: 3000
三角形的面积: 300

纯虚函数和抽象类的案例二

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include <iostream>
#include <cstring>

using namespace std;

// 抽象类
class Animal {

public:
// 抽象类可以有构造函数,用于初始化其成员变量
// 当抽象类被继承并通过派生类构造时,抽象类的构造函数会被调用
Animal(string name) : _name(name) {
cout << "Animal()" << endl;
}

// 抽象类可以有析构函数,用于释放资源
// 当抽象类被继承并通过派生类析构时,抽象类的析构函数会被调用
~Animal() {
cout << "~Animal()" << endl;
}

// 纯虚函数
virtual void bark() = 0;

protected:
string _name;

};

class Cat : public Animal {

public:
Cat(string name) : Animal(name) {

}

void bark() {
cout << _name << " bark: miao miao" << endl;
}

};

class Dog : public Animal {

public:
Dog(string name) : Animal(name) {

}

void bark() {
cout << _name << " bark: wang wang" << endl;
}

};

class Pig : public Animal {

public:
Pig(string name) : Animal(name) {

}

void bark() {
cout << _name << " bark: heng heng" << endl;
}

};

void bark(Animal &animal) {
// 动态多态(底层是通过动态绑定来实现的)
animal.bark();
}

int main() {
// 错误写法,抽象类不能实例化
// Animal animal("Animal");

Cat cat = Cat("Cat");
Dog dog = Dog("Dog");
Pig pig = Pig("Pig");

bark(cat);
bark(dog);
bark(pig);

return 0;
}

程序运行的输出结果如下:

1
2
3
4
5
6
7
8
9
Animal()
Animal()
Animal()
Cat bark: miao miao
Dog bark: wang wang
Pig bark: heng heng
~Animal()
~Animal()
~Animal()

虚继承与虚基类

虚继承与虚基类的概念

  • 基本概念

    • 虚继承的声明需要使用关键字 virtual
    • 在 C++ 中,被虚继承的类通常称作为虚基类。
    • 虚继承的底层是靠虚基类指针(vbptr)和虚基类表(vbtable)来实现。
    • 如果一个派生类从多个基类继承,而这些基类又有一个共同的基类(公共基类),则在对该基类中声明的成员进行访问时,可能会产生二义性,需要使用虚继承来解决二义性问题。
    • 如果在多条继承路径上有一个公共的基类,那么在继承路径的某处汇合点,这个公共基类就会在派生类的对象中产生多个基类子对象。要使这个公共基类在派生类中只产生一个子对象,必须对这个基类的继承声明为虚继承,使这个基类成为 虚基类
  • 适用场景

    • 虚继承只适用于有共同基类(公共基类)的多继承场景(比如菱形继承),如下图所示:

  • 不适用场景
    • 对于 V 字形的多继承场景,虚继承是没办法解决二义性问题的,如下图所示:

虚继承与虚基类使用案例

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
73
74
75
76
#include <iostream>

using namespace std;

class Base {

public:
Base(int x) {
this->x = x;
cout << "Base 类的构造函数被调用" << endl;
}

void printX() {
cout << "x = " << x << endl;
}

private:
int x;
};

// 声明虚继承
class Base1 : virtual public Base {

public:
Base1(int a, int x) : Base(x) {
this->a = a;
}

void printA() {
cout << "a = " << a << endl;
}

private:
int a;
};

// 声明虚继承
class Base2 : virtual public Base {

public:
Base2(int b, int x) : Base(x) {
this->b = b;
}

void printB() {
cout << "b = " << b << endl;
}

private:
int b;
};

class Base3 : public Base1, public Base2 {

public:
// 由于父类和虚基类没有默认的无参构造函数,所以这里的派生类需要在初始化列表中,显式调用父类、虚基类的有参构造函数
Base3(int a, int b, int c, int x) : Base1(a, x), Base2(b, x), Base(x) {
this->c = c;
}

void printC() {
cout << "c = " << c << endl;
}

private:
int c;
};

int main() {
Base3 base(1, 2, 3, 4); // 虚基类Base的构造函数只会被调用一次
base.printA();
base.printB();
base.printC();
base.printX(); // 当不声明虚继承的时候,此写法会产生二义性,C++编译器会出现编译错误
return 0;
}

程序运行的输出结果如下:

1
2
3
4
5
Base 类的构造函数被调用
a = 1
b = 2
c = 3
x = 4

虚继承的类内存布局之一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>

using namespace std;

// 虚基类(被虚继承的基类称为虚基类)
class A {

private:
int ma;

};

// 虚继承
class B : virtual public A {

private :
int mb;

};

int main() {
cout << "size : " << sizeof(B) << endl;
return 0;
}

程序运行的输出结果如下:

1
size : 16

在 Visual Studio 中查看上述 B 派生类的内存布局,可以得到以下结果,其中 vbptr 是虚基类指针(Virtual Base Ptr),而 vbtable 是虚基类表(Virtual Base Table)。

虚继承的类内存布局之二

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
#include <iostream>

using namespace std;

// 虚基类(被虚继承的类称为虚基类)
class A {

public:
// 虚函数
virtual void show() {
cout << "call A::show()" << endl;
}

private:
int ma;

};

// 虚继承
class B : virtual public A {

public:
void show() {
cout << "call B::show()" << endl;
}

private :
int mb;

};

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

// A *a = new B();
// a->show(); // 正常调用,发生动态绑定
// delete a; // 调用失败,异常终止执行,这是因为基类指针指向的是派生类中基类那部分数据的起始地址,导致无法正常释放堆上分配的内存空间

B b;
A &a = b;
b.show(); // 正常调用,发生动态绑定,且栈上分配的内存空间出了作用域之后可以正常被自动释放

return 0;
}

程序运行的输出结果如下:

1
2
size : 32
call B::show()

在 Visual Studio 中查看上述 B 派生类的内存布局,可以得到以下结果;其中 vbptr 是虚基类指针(Virtual Base Ptr),而 vbtable 是虚基类表(Virtual Base Table);vfptr 是虚函数指针(Virtual Function Ptr),vftable 是虚函数表(Virtual Function Table)。

特别注意

基类指针(引用)指向派生类对象时,永远指向的是派生类中基类那部分数据的起始地址。

菱形继承的问题

菱形继承的概念

在 C++ 的多继承中,有一种特殊的继承结构,那就是菱形继承,如下图所示:

在 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
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
73
#include <iostream>

using namespace std;

class A {

public:
A(int data) : ma(data) {
cout << "A()" << endl;
}

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

public:
int ma;

};

class B : public A {

public:
B(int data) : A(data), mb(data) {
cout << "B()" << endl;
}

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

public:
int mb;

};

class C : public A {

public:
C(int data) : A(data), mc(data) {
cout << "C()" << endl;
}

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

public:
int mc;

};

class D : public B, public C {

public:
D(int data) : B(data), C(data), md(data) {
cout << "D()" << endl;
}

~D() {
cout << "~D()" << endl;
}

public:
int md;

};

int main() {
D d(20);
// d.ma; // 错误写法,存在二义性
return 0;
}

程序运行的输出结果如下:

1
2
3
4
5
6
7
8
9
10
A()
B()
A()
C()
D()
~D()
~C()
~A()
~B()
~A()

从上述的执行结果可以发现基类 A 被构造了两次,这执行效率是比较低的;而且派生类 D 无法正常访问基类 A 中的成员变量 ma,因为存在二义性。

菱形继承问题的解决

在 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
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
73
74
75
76
#include <iostream>

using namespace std;

// 虚继类
class A {

public:
A(int data) : ma(data) {
cout << "A()" << endl;
}

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

public:
int ma;

};

// 虚继承
class B : virtual public A {

public:
B(int data) : A(data), mb(data) {
cout << "B()" << endl;
}

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

public:
int mb;

};

// 虚继承
class C : virtual public A {

public:
C(int data) : A(data), mc(data) {
cout << "C()" << endl;
}

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

public:
int mc;

};

class D : public B, public C {

public:
D(int data) : A(data), B(data), C(data), md(data) {
cout << "D()" << endl;
}

~D() {
cout << "~D()" << endl;
}

public:
int md;

};

int main() {
D d(20);
cout << d.ma << endl; // 正确写法,不存在二义性
return 0;
}

程序运行的输出结果如下:

1
2
3
4
5
6
7
8
9
A()
B()
C()
D()
20
~D()
~C()
~B()
~A()

从上述的执行结果可以发现基类 A 只构造了一次,而且派生类 D 可以正常访问基类 A 中的成员变量 ma,因为不存在二义性。

C++ 的四种类型转换

在 C++ 中,有以下 4 种类型转换:

  • static_cast:静态类型转换,如 int 转换成 char
  • dynamic_cast:动态类型转换,主要用在类的继承结构中,可以支持 RTTI 类型识别的上下类型转换,如父类和子类之间的多态类型转换
  • const_cast:常量类型转换,用于赋予或者去除类型的 const 只读属性
  • reinterpreter_cast:重新解释类型(强制类型转换)
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
#include <iostream>

using namespace std;

// 抽象类
class Base {

public:
// 纯虚函数
virtual void show() = 0;
};

class Device1 : public Base {

public:
void show() {
cout << "call Device1::show()" << endl;
}

};

class Device2 : public Base {

public:
void show() {
cout << "call Device2::show()" << endl;
}

void print() {
cout << "call Device2::print()" << endl;
}

};

void show(Base *base) {
// 判断 base 指针指向的是不是 Device2 对象
// 如果是,则 dynamic_cast 转换成功,并返回 Device2 对象的地址,否则返回 nullptr
Device2 *pDevice2 = dynamic_cast<Device2 *>(base);
if (pDevice2 == nullptr) {
base->show();
} else {
pDevice2->print();
}
}

int main() {

// const_cast:去掉常量属性的类型转换
const int a = 10;
int *p1 = const_cast<int * > (&a);
cout << *p1 << endl;

// static_cast:提供编译器认为安全的类型转换
int b = 65;
char c = static_cast<char>(b);
cout << c << endl;

// reinterpret_cast:类似于 C 语言风格的类型转换
int d = 20;
int *p2 = reinterpret_cast<int *> (d);

// dynamic_cast:主要用在类的继承结构中,可以支持 RTTI 类型识别的上下类型转换
Device1 device1;
Device2 device2;
show(&device1);
show(&device2);

return 0;
}

程序运行的输出结果如下:

1
2
3
4
10
A
call Device1::show()
call Device2::print()

虚函数高频面试题

虚函数高频面试题之一

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
#include <iostream>

using namespace std;

class Animal {

public:
Animal(string name) : _name(name) {

}

// 虚函数
virtual void bark() {

}

protected:
string _name;

};

class Cat : public Animal {

public:
Cat(string name) : Animal(name) {

}

void bark() {
cout << _name << " bark: miao miao" << endl;
}

};

class Dog : public Animal {

public:
Dog(string name) : Animal(name) {

}

void bark() {
cout << _name << " bark: wang wang" << endl;
}

};

int main() {
Animal *p1 = new Cat("Cat");
Animal *p2 = new Dog("Dog");

int *p11 = (int *) p1; // p11[0] 指向的是 Cat 的前 4 个字节(存放的是 vfptr)
int *p22 = (int *) p2; // p22[0] 指向的是 Dog 的前 4 个字节(存放的是 vfptr)

int tmp = p11[0]; // 交换两个对象的虚函数指针(vfptr)的地址
p11[0] = p22[0];
p22[0] = tmp;

p1->bark(); // p1 -> Cat vfptr -> Dog vftable,输出:Cat bark: wang wang
p2->bark(); // p2 -> Dog vfptr -> Cat vftable,输出:Dog bark: miao miao

delete p1;
delete p2;

return 0;
}

程序运行的输出结果如下:

1
2
Cat bark: wang wang
Dog bark: miao miao

虚函数高频面试题之二

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
#include <iostream>

using namespace std;

class Base {

public:
// 虚函数
virtual void show(int i = 10) {
cout << "call Base::show i : " << i << endl;
}

};

class Device : public Base {

public:
void show(int i = 20) {
cout << "call Device::show i : " << i << endl;
}

};

int main() {
Base *b = new Device();

// 函数调用,参数压栈是在编译时期决定的,因此这里参数压栈时使用的是基类中虚函数的默认参数值
// 动态绑定,p -> Device vfptr -> Device vftable -> Device::show(),输出: call Device::show i : 10
b->show();
delete b;

return 0;
}

程序运行的输出结果如下:

1
call Device::show i : 10

虚函数高频面试题之三

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
#include <iostream>

using namespace std;

class Base {

public:
// 虚函数
virtual void show() {
cout << "call Base::show()" << endl;
}

};

class Device : public Base {

private:
// 私有方法
void show() {
cout << "call Device::show()" << endl;
}

};

int main() {
Base *b = new Device();

// 可以正常调用,发生动态(运行时期)绑定,输出:call Device::show()
// 成员函数能不能被调用,就是说函数的访问权限是不是 public,这是在编译阶段就需要确定好的,而动态绑定是发生在运行时期
b->show();
delete b;

return 0;
}

程序运行的输出结果如下:

1
call Device::show()

虚函数高频面试题之四

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
#include <iostream>
#include <cstring>

using namespace std;

class Base {

public:
Base() {
cout << "call Base()" << endl;
clear();
}

virtual void show() {
cout << "call Base::show()" << endl;
}

void clear() {
// 将当前对象的内存区域全部置为 0
// 相当于当前对象内存中的 vfptr 虚函数指针被修改了,不再指向 vftable 虚函数表
memset(this, 0, sizeof(*this));
}

};

class Device : public Base {

public:
Device() {
cout << "call Device()" << endl;
}

void show() {
cout << "call Device::show()" << endl;
}

};

int main() {
Base *bptr1 = new Base();
bptr1->show(); // 动态绑定,异常终止执行
delete bptr1;

Base *bptr2 = new Device();
bptr2->show(); // 动态绑定,正常执行结束
delete bptr2;

return 0;
}