继承多态
# 第 10 章 C++ 继承多态
# 目录介绍
# 10.1 继承
# 10.1.1 继承概念
继承 是面向对象编程(OOP)的核心特性之一,它允许一个类(派生类)基于另一个类(基类)创建,从而复用基类的成员并扩展其功能。
- 基类(父类):被继承的类。
- 派生类(子类):继承基类的类。
派生类可以访问基类的成员(根据访问权限),并可以添加新的成员或重写基类的成员函数。
# 10.1.2 继承语法
class BaseClass {
// 基类成员
};
class DerivedClass : access-specifier BaseClass {
// 派生类成员
};
2
3
4
5
6
7
访问修饰符:可以是 public、protected 或 private,决定基类成员在派生类中的访问权限。
access-specifier:访问修饰符,可以是 public、protected 或 private,决定基类成员在派生类中的访问权限。
class 派生类名 : 访问修饰符 基类名 {
// 派生类的成员
};
2
3
# 10.1.3 继承访问控制
继承后的可访问性是指派生类(子类)对基类(父类)成员的访问权限(public,protected,private)。
| 继承方式 | public 成员在派生类中 | protected 成员在派生类中 | private 成员在派生类中 |
|---|---|---|---|
| 公有继承 public | public | protected | 不可访问 |
| 保护继承 protected | protected | protected | 不可访问 |
| 私有继承 private | private | private | 不可访问 |
总结
- 保护继承将基类的
public和protected成员在派生类中变为protected成员。 - 保护继承适用于需要限制基类成员访问权限的场景。
- 在实际开发中,保护继承的使用较少,通常优先考虑公有继承或组合(Composition)。
# 10.1.4 继承中对象模型
问题:从父类继承过来的成员,哪些属于子类对象中?
class Base {
public:
int a;
protected:
int b;
private:
int c; //私有成员只是被隐藏了,但是还是会继承下去
};
//公共继承
class Son : public Base {
public:
int d;
};
void test01() {
cout << "sizeof int = " << sizeof(int) << endl;
cout << "sizeof Son = " << sizeof(Son) << endl;
}
int main() {
test01();
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
然后打印一下结果,如下:
sizeof int = 4
sizeof Son = 16
2
由结果可知:单继承中,派生类对象包含基类部分和派生类部分。
# 10.1.5 继承中构造和析构顺序
子类继承父类后,当创建子类对象,也会调用父类的构造函数
问题:父类和子类的构造和析构顺序是谁先谁后?
class Base {
public:
Base() {
cout << "Base构造函数!" << endl;
}
~Base() {
cout << "Base析构函数!" << endl;
}
};
class Son : public Base {
public:
Son() {
cout << "Son构造函数!" << endl;
}
~Son() {
cout << "Son析构函数!" << endl;
}
};
void test01() {
//继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反
Son s;
}
int main() {
test01();
return 0;
}
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
打印结果如下所示:
Base构造函数!
Son构造函数!
Son析构函数!
Base析构函数!
2
3
4
总结:继承中,先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反。
# 10.1.6 继承同名成员处理方式
问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢?
- 访问子类同名成员 直接访问即可
- 访问父类同名成员 需要加作用域
class Base {
public:
Base() {
a = 100;
}
void func() {
cout << "Base - func()调用" << a << endl;
}
void func(int a) {
cout << "Base - func(int a)调用" << a << endl;
}
public:
int a;
};
class Son : public Base {
public:
Son() {
a = 200;
}
//当子类与父类拥有同名的成员函数,子类会隐藏父类中所有版本的同名成员函数
//如果想访问父类中被隐藏的同名成员函数,需要加父类的作用域
void func() {
cout << "Son - func()调用" << a << endl;
}
public:
int a;
};
void test01() {
Son s;
cout << "Son下的a = " << s.a << endl;
cout << "Base下的a = " << s.Base::a << endl;
s.func();
s.Base::func();
s.Base::func(10);
}
int main() {
test01();
return EXIT_SUCCESS;
}
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
打印结果如下所示:
Son下的a = 200
Base下的a = 100
Son - func()调用200
Base - func()调用100
Base - func(int a)调用10
2
3
4
5
总结:
- 子类对象可以直接访问到子类中同名成员
- 子类对象加作用域可以访问到父类同名成员
- 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数
# 10.1.7 继承同名静态成员处理
问题:继承中同名的静态成员在子类对象上如何进行访问?
静态成员和非静态成员出现同名,处理方式一致
- 访问子类同名成员 直接访问即可
- 访问父类同名成员 需要加作用域
class Base {
public:
static void func() {
cout << "Base - static void func()" << a << endl;
}
static void func(int a) {
cout << "Base - static void func(int a)" << a << endl;
}
static int a;
};
int Base::a = 100;
class Son : public Base {
public:
static void func() {
cout << "Son - static void func()" << a << endl;
}
static int a;
};
int Son::a = 200;
//同名成员属性
void test01() {
//通过对象访问
cout << "通过对象访问: " << endl;
Son s;
cout << "Son 下 a = " << s.a << endl;
cout << "Base 下 a = " << s.Base::a << endl;
//通过类名访问
cout << "通过类名访问: " << endl;
cout << "Son 下 a = " << Son::a << endl;
cout << "Base 下 a = " << Son::Base::a << endl;
}
//同名成员函数
void test02() {
//通过对象访问
cout << "通过对象访问: " << endl;
Son s;
s.func();
s.Base::func();
cout << "通过类名访问: " << endl;
Son::func();
Son::Base::func();
//出现同名,子类会隐藏掉父类中所有同名成员函数,需要加作作用域访问
Son::Base::func(100);
}
int main() {
//test01();
test02();
return 0;
}
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
# 10.1.8 综合案例与思考
#include <iostream>
#include <string>
using namespace std;
class Person {
public:
string name;
int age;
Person(const string& n, int a) : name(n), age(a) {
cout << "Person构造: " << name << endl;
}
~Person() { cout << "Person析构: " << name << endl; }
void introduce() { cout << "我是" << name << ", " << age << "岁" << endl; }
void introduce(const string& title) {
cout << title << name << ", " << age << "岁" << endl;
}
protected:
string idCard = "身份证号保护中";
private:
string password = "密码私有";
};
class Student : public Person {
public:
string school;
int grade;
Student(const string& n, int a, const string& s, int g)
: Person(n, a), school(s), grade(g) {
cout << "Student构造: " << name << endl;
}
~Student() { cout << "Student析构: " << name << endl; }
// 同名成员函数,隐藏了父类的 introduce
void introduce() {
cout << school << grade << "年级学生 " << name << endl;
// 访问 protected 成员
cout << " [保护信息] " << idCard << endl;
// cout << password << endl; // 错误!private 不可访问
}
void callParentIntroduce() {
Person::introduce(); // 访问父类同名函数
Person::introduce("同学: "); // 访问父类重载版本
}
};
// 静态成员继承示例
class Base {
public:
static int count;
static void showCount() { cout << "Base::count = " << count << endl; }
};
int Base::count = 100;
class Derived : public Base {
public:
static int count; // 同名静态成员
static void showCount() { cout << "Derived::count = " << count << endl; }
};
int Derived::count = 200;
int main() {
cout << "=== 构造析构顺序 ===" << endl;
Student stu("张三", 15, "清华附中", 9);
cout << "\n=== 继承中对象模型 ===" << endl;
cout << "sizeof(Person) = " << sizeof(Person) << endl;
cout << "sizeof(Student) = " << sizeof(Student) << endl;
cout << "\n=== 同名成员处理 ===" << endl;
stu.introduce(); // 子类版本
stu.callParentIntroduce(); // 通过作用域访问父类版本
cout << "\n=== 同名静态成员 ===" << endl;
Derived::showCount(); // Derived的
Derived::Base::showCount(); // 通过作用域访问Base的
cout << "\n=== 析构顺序(与构造相反)===" << endl;
return 0;
}
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
案例知识融合:本案例将继承的核心知识融为一体:继承语法与访问控制(public/protected/private成员在子类中的可见性)、继承中的对象模型(子类包含父类所有成员,sizeof验证)、构造析构顺序(先父后子构造,先子后父析构)、同名成员处理(子类隐藏父类同名函数,通过作用域访问父类)以及同名静态成员的处理方式。
思考题:
- 如果把
class Student : public Person改为class Student : private Person,外部还能通过stu.name访问吗?protected 继承呢? - 子类
introduce()隐藏了父类所有同名函数(包括重载版本),这是设计还是缺陷?如何用using声明解决? - 父类的 private 成员在子类中是"不可访问"还是"不存在"?通过 sizeof 如何验证?
# 10.2 多重继承
# 10.2.1 多重继承概念
多重继承是指一个类可以从多个基类继承属性和行为。与单继承(一个类只能继承一个基类)不同,多重继承允许一个类同时继承多个基类,从而组合多个类的功能。
# 10.2.2 多重继承语法
class DerivedClass : access-specifier BaseClass1, access-specifier BaseClass2, ... {
// 派生类的成员
};
2
3
DerivedClass:派生类。BaseClass1, BaseClass2, ...:多个基类。access-specifier:访问修饰符(public、protected或private)。
多继承可能会引发父类中有同名成员出现,需要加作用域区分。
# 10.2.3 多重继承示例
#include <iostream>
// 基类1
class Animal {
public:
void eat() {
std::cout << "Animal is eating." << std::endl;
}
};
// 基类2
class Mammal {
public:
void breathe() {
std::cout << "Mammal is breathing." << std::endl;
}
};
// 派生类,继承自 Animal 和 Mammal
class Dog : public Animal, public Mammal {
public:
void bark() {
std::cout << "Dog is barking." << std::endl;
}
};
int main() {
Dog dog;
dog.eat(); // 继承自 Animal
dog.breathe(); // 继承自 Mammal
dog.bark(); // 派生类自己的方法
return 0;
}
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
输出:
Animal is eating.
Mammal is breathing.
Dog is barking.
2
3
多重继承特点
- 组合多个类的功能:派生类可以继承多个基类的成员函数和成员变量。
- 访问控制:通过访问修饰符(
public、protected、private)控制基类成员的访问权限。 - 构造函数调用顺序:基类的构造函数按照继承顺序调用,析构函数按照相反顺序调用。
- 可能引发问题:多重继承可能导致菱形继承问题和命名冲突。
# 10.2.4 多重继承二义性
在C++的多重继承中,当派生类从多个基类中继承相同的成员函数或成员变量时,可能会导致二义性问题。这种情况下,编译器无法确定应该使用哪个基类的成员,从而导致编译错误。
二义性问题主要有两种情况:
- 成员函数二义性:当派生类从多个基类中继承了相同的成员函数时,如果派生类直接调用该成员函数,编译器无法确定应该使用哪个基类的成员函数。这种情况下,可以使用作用域解析运算符(::)来指定要调用的基类成员函数。
- 成员变量二义性:当派生类从多个基类中继承了相同的成员变量时,如果派生类直接访问该成员变量,编译器无法确定应该使用哪个基类的成员变量。这种情况下,可以使用作用域解析运算符(::)来指定要访问的基类成员变量。
class Base1 {
public:
void display() {
std::cout << "Base1 display()" << std::endl;
}
Base1() {
std::cout << "Base1 constructor" << std::endl;
}
};
class Base2 {
public:
void display() {
std::cout << "Base2 display()" << std::endl;
}
Base2() {
std::cout << "Base2 constructor" << std::endl;
}
};
class DerivedYc : public Base1, public Base2 {
public:
void display() {
std::cout << "Derived display()" << std::endl;
}
DerivedYc() {
std::cout << "Derived constructor" << std::endl;
}
};
void test() {
DerivedYc d;
d.display(); // 编译错误,二义性调用
d.Base1::display(); // 使用作用域解析运算符调用 Base1 类中的 display() 函数
d.Base2::display(); // 使用作用域解析运算符调用 Base2 类中的 display() 函数
}
int main() {
test();
return 0;
}
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
在这个示例中,Derived类从Base1和Base2类中继承了相同的display()函数。
当通过Derived类的对象调用display()函数时,会导致编译错误,因为编译器无法确定应该使用哪个基类的成员函数。
为了解决这个问题,可以使用作用域解析运算符来指定要调用的基类成员函数。
# 10.2.5 菱形继承
菱形继承概念: 两个派生类继承同一个基类,又有某个类同时继承者两个派生类,导致某个类中包含多个相同的基类子对象。这种继承被称为菱形继承,或者钻石继承。
菱形继承问题:
- 羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性。
- 草泥马继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
这个时候类 Animal 中的成员变量和成员函数继承到类 SheepTuo 中变成了两份,一份来自 Animal-->Sheep-->SheepTuo 这条路径,另一份来自 Animal-->Tuo-->SheepTuo 这条路径。
示例:
class Animal {
public:
int age;
};
//此时公共的父类Animal称为虚基类
class Sheep : public Animal {};
class Tuo : public Animal {};
class SheepTuo : public Sheep, public Tuo {};
int main() {
SheepTuo st;
st.Sheep::age = 100;
st.Tuo::age = 200;
cout << "st.Sheep::age = " << st.Sheep::age << endl;
cout << "st.Tuo::age = " << st.Tuo::age << endl;
//cout << "st.age = " << st.age << endl; //因为SheepTuo的父类Sheep和Tuo都有age,编译器不知道选用哪一个,所以产生了歧义。
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
打印结果如下所示:
st.Sheep::age = 100
st.Tuo::age = 200
2
总结:菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义。并且调用变量时存在二义性(编译器不知道选择那个)。
# 10.2.6 虚继承
虚继承(Virtual Inheritance)是一种继承方式,用于解决多继承中的菱形继承问题(Diamond Inheritance Problem)。
菱形继承问题是指当一个派生类同时继承自两个或多个基类,而这些基类又共同继承自同一个基类时,派生类中会存在多个对同一个基类成员的拷贝,导致二义性和冗余。
虚继承通过在继承关系中使用virtual关键字来解决菱形继承问题。在虚继承中,派生类对共同基类的继承是虚拟的,只会保留一个共同基类的实例,从而避免了多个拷贝和二义性。
class Animal {
public:
int age;
};
//继承前加virtual关键字后,变为虚继承
//此时公共的父类Animal称为虚基类
class Sheep : virtual public Animal {};
class Tuo : virtual public Animal {};
class SheepTuo : public Sheep, public Tuo {};
int main() {
SheepTuo st;
st.Sheep::age = 100;
st.Tuo::age = 200;
cout << "st.Sheep::age = " << st.Sheep::age << endl;
cout << "st.Tuo::age = " << st.Tuo::age << endl;
cout << "st.age = " << st.age << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
打印结果如下所示:
st.Sheep::age = 200
st.Tuo::age = 200
st.age = 200
2
3
# 10.2.7 注意事项
- 命名冲突:如果多个基类中有同名成员,派生类需要使用作用域解析运算符
::来指定访问哪个基类的成员。class Base1 { public: void print() { std::cout << "Base1" << std::endl; } }; class Base2 { public: void print() { std::cout << "Base2" << std::endl; } }; class Derived : public Base1, public Base2 { public: void show() { Base1::print(); // 调用 Base1 的 print Base2::print(); // 调用 Base2 的 print } };1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 - 设计复杂性:多重继承会增加代码的复杂性,建议谨慎使用。
- 优先使用组合:如果多重继承导致设计复杂,可以考虑使用组合(将类作为成员变量)代替继承。
# 10.2.8 多重继承场景
- 接口实现:一个类可以实现多个接口(抽象类)。
- 功能组合:将多个类的功能组合到一个类中。
- 框架设计:在框架设计中,多重继承可以用于扩展功能。
# 10.2.9 综合案例与思考
#include <iostream>
using namespace std;
// 虚基类
class Device {
public:
int id;
Device(int i) : id(i) { cout << "Device构造 id=" << id << endl; }
virtual ~Device() { cout << "Device析构" << endl; }
void powerOn() { cout << "设备" << id << "开机" << endl; }
};
// 虚继承 —— 解决菱形继承
class Printer : virtual public Device {
public:
Printer(int i) : Device(i) { cout << "Printer构造" << endl; }
void print() { cout << "打印文档" << endl; }
};
class Scanner : virtual public Device {
public:
Scanner(int i) : Device(i) { cout << "Scanner构造" << endl; }
void scan() { cout << "扫描文档" << endl; }
};
// 多重继承 + 虚继承
class MultiFunctionDevice : public Printer, public Scanner {
public:
// 虚继承时,最终派生类负责初始化虚基类
MultiFunctionDevice(int i) : Device(i), Printer(i), Scanner(i) {
cout << "MultiFunctionDevice构造" << endl;
}
void copy() {
scan();
print();
cout << "复印完成" << endl;
}
};
int main() {
cout << "=== 创建多功能设备 ===" << endl;
MultiFunctionDevice mfd(1001);
cout << "\n=== 使用功能 ===" << endl;
mfd.powerOn(); // 只有一份 Device,无二义性
mfd.print();
mfd.scan();
mfd.copy();
cout << "\n=== 验证只有一份基类数据 ===" << endl;
mfd.Printer::id = 100;
cout << "通过Scanner访问id: " << mfd.Scanner::id << endl; // 同一个id
cout << "直接访问id: " << mfd.id << endl;
cout << "\n=== 析构 ===" << endl;
return 0;
}
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
案例知识融合:本案例通过多功能办公设备模拟了多重继承的典型场景:MultiFunctionDevice 同时继承 Printer 和 Scanner,它们共同继承自 Device,形成菱形继承。使用 virtual 虚继承确保只有一份 Device 基类实例,消除二义性。验证了通过不同路径访问同一个 id 变量的结果一致性,以及虚继承中由最终派生类负责初始化虚基类的规则。
思考题:
- 如果去掉
virtual关键字,mfd.id的访问会出现什么错误?为什么? - 虚继承有性能开销吗?底层是通过什么机制(虚基类指针/虚基类表)实现的?
- 实际开发中,除了虚继承,还有什么设计方式可以避免菱形继承问题?
# 10.3 多态
# 10.3.1 多态概念
多态是指通过基类的指针或引用调用派生类的重写函数,实现“一个接口,多种实现”。多态分为两种:
- 编译时多态:通过函数重载和运算符重载实现。
- 运行时多态:通过虚函数和继承实现。
面向对象程序设计语言有封装、继承和多态三种机制,这三种机制能够有效提高程序的可读性、可扩充性和可重用性。
多态(polymorphism):同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。
多态可以分为编译时的多态和运行时的多态。
- 前者主要是指函数的重载(包括运算符的重载)、对重载函数的调用,在编译时就能根据实参确定应该调用哪个函数,因此叫编译时的多态;
- 后者则和继承、虚函数等概念有关。
# 10.3.2 多态语法
- 虚函数是在基类中声明为
virtual的函数,允许派生类重写(Override)该函数。 - 通过基类指针或引用调用虚函数时,实际调用的是派生类的重写函数。
class BaseClass {
public:
virtual void function() {
// 基类实现
}
};
class DerivedClass : public BaseClass {
public:
void function() override {
// 派生类实现
}
};
2
3
4
5
6
7
8
9
10
11
12
13
# 10.3.3 多态案例
多态满足条件:1.有继承关系;2.子类重写父类中的虚函数
多态使用条件:父类指针或引用指向子类对象
class Animal {
public:
void eat() {
cout << "动物在吃东西" << endl;
}
//Speak函数就是虚函数
//函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。
virtual void speak() {
cout << "动物在说话" << endl;
}
};
class Cat : public Animal {
public:
void eat() {
cout << "小猫吃鱼" << endl;
}
void speak() {
cout << "小猫在说话" << endl;
}
};
class Dog : public Animal {
public:
void eat() {
cout << "小狗吃屎" << endl;
}
void speak() {
cout << "小狗在说话" << endl;
}
};
//我们希望传入什么对象,那么就调用什么对象的函数
//如果函数地址在编译阶段就能确定,那么静态联编
//如果函数地址在运行阶段才能确定,就是动态联编
void DoSpeak(Animal & animal){
animal.eat();
animal.speak();
}
int main() {
Cat cat;
DoSpeak(cat);
Dog dog;
DoSpeak(dog);
return 0;
}
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
打印结果如下所示:
动物在吃东西
小猫在说话
动物在吃东西
小狗在说话
2
3
4
# 10.3.4 多态的分类
静态多态和动态多态区别:
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
#include <iostream>
using namespace std;
class Base {
public:
// 普通函数 - 静态绑定
void normalFunc() {
cout << "Base::normalFunc()" << endl;
}
// 虚函数 - 动态绑定
virtual void virtualFunc() {
cout << "Base::virtualFunc()" << endl;
}
};
class Derived : public Base {
public:
void normalFunc() {
cout << "Derived::normalFunc()" << endl;
}
void virtualFunc() override {
cout << "Derived::virtualFunc()" << endl;
}
};
void demonstrateBinding() {
cout << "=== 绑定机制演示 ===" << endl;
Derived derived;
Base* basePtr = &derived;
// 静态绑定 - 编译时确定
cout << "静态绑定结果: ";
basePtr->normalFunc(); // 输出: Base::normalFunc()
// 动态绑定 - 运行时确定
cout << "动态绑定结果: ";
basePtr->virtualFunc(); // 输出: Derived::virtualFunc()
cout << "\n=== 对象直接调用 ===" << endl;
derived.normalFunc(); // 输出: Derived::normalFunc()
derived.virtualFunc(); // 输出: Derived::virtualFunc()
}
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
两种多态区别
| 特性 | 静态多态 | 动态多态 |
|---|---|---|
| 绑定时间 | 编译时绑定(早绑定) | 运行时绑定(晚绑定) |
| 实现机制 | 函数重载、运算符重载、模板 | 虚函数、继承 |
| 性能 | 高效,无运行时开销 | 有轻微性能开销(虚表查找) |
| 灵活性 | 相对固定 | 高度灵活,支持运行时决策 |
| 关键字 | 不需要特殊关键字 | 需要virtual关键字 |
# 10.3.5 多态和虚函数
虚函数:基类中提供默认实现,派生类可以选择性地重写(override)该函数。
#include <iostream>
class Animal {
public:
virtual void speak() {
std::cout << "Animal speaks." << std::endl;
}
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Dog barks." << std::endl;
}
};
class Cat : public Animal {
public:
void speak() override {
std::cout << "Cat meows." << std::endl;
}
};
int main() {
Animal* animal1 = new Dog();
Animal* animal2 = new Cat();
animal1->speak(); // 输出: Dog barks.
animal2->speak(); // 输出: Cat meows.
delete animal1;
delete animal2;
return 0;
}
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
# 10.3.4 多态和析构函数
# 10.3.6 综合案例与思考
#include <iostream>
#include <vector>
#include <memory>
using namespace std;
// 基类:图形
class Shape {
public:
virtual double area() const = 0; // 纯虚函数
virtual string name() const = 0;
virtual void draw() const {
cout << "绘制 " << name() << " 面积=" << area() << endl;
}
// 虚析构函数(多态基类必须)
virtual ~Shape() {
cout << name() << " 析构" << endl;
}
};
class Circle : public Shape {
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override { return 3.14159 * radius * radius; }
string name() const override { return "圆"; }
};
class Rectangle : public Shape {
double w, h;
public:
Rectangle(double w, double h) : w(w), h(h) {}
double area() const override { return w * h; }
string name() const override { return "矩形"; }
};
class Triangle : public Shape {
double base, height;
public:
Triangle(double b, double h) : base(b), height(h) {}
double area() const override { return 0.5 * base * height; }
string name() const override { return "三角形"; }
};
// 多态调用:统一接口,不同行为
void printShapeInfo(const Shape& shape) {
shape.draw(); // 运行时决定调用哪个版本
}
int main() {
// 使用基类指针管理不同派生类
vector<unique_ptr<Shape>> shapes;
shapes.push_back(make_unique<Circle>(5.0));
shapes.push_back(make_unique<Rectangle>(4.0, 6.0));
shapes.push_back(make_unique<Triangle>(3.0, 8.0));
cout << "=== 多态遍历 ===" << endl;
double totalArea = 0;
for (const auto& shape : shapes) {
printShapeInfo(*shape);
totalArea += shape->area();
}
cout << "总面积: " << totalArea << endl;
// 验证静态绑定 vs 动态绑定
cout << "\n=== 绑定方式对比 ===" << endl;
Circle c(10);
Shape* ptr = &c;
Shape& ref = c;
ptr->draw(); // 动态绑定 -> Circle::draw()
ref.draw(); // 动态绑定 -> Circle::draw()
cout << "\n=== 虚析构演示 ===" << endl;
Shape* p = new Circle(1);
delete p; // 虚析构确保正确调用 Circle 的析构函数
cout << "\n=== shapes 容器销毁 ===" << endl;
return 0;
}
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
案例知识融合:本案例通过图形系统综合展示了多态的核心:纯虚函数定义接口(Shape是抽象类不可实例化)、派生类用 override 重写虚函数、基类指针/引用调用派生类函数实现运行时多态、虚析构函数确保通过基类指针 delete 时正确释放派生类资源。使用 unique_ptr 管理多态对象生命周期是现代C++的最佳实践。
思考题:
- 如果
Shape的析构函数不是虚函数,delete p会发生什么?为什么多态基类必须有虚析构? override关键字的作用是什么?如果派生类的函数签名和基类不一致(如忘了 const),不加 override 会怎样?- 虚函数的底层实现机制是什么(虚函数表vptr/vtable)?每个对象的 vptr 占多少空间?
# 10.4 抽象类
# 10.4.1 抽象类概念
抽象类是一种特殊的类,它不能被实例化,只能作为其他类的基类。抽象类的主要目的是为派生类提供一个统一的接口或行为规范。
抽象类通常包含至少一个纯虚函数,这使得它无法直接创建对象。
# 10.4.2 抽象类定义
抽象类通过声明纯虚函数来定义。纯虚函数是一个没有实现的虚函数,其声明以 = 0 结尾。
class AbstractClass {
public:
// 纯虚函数
virtual void pureVirtualFunction() = 0;
// 普通成员函数
void normalFunction() {
std::cout << "This is a normal function." << std::endl;
}
};
2
3
4
5
6
7
8
9
10
# 10.4.3 抽象类特点
- 不能实例化:抽象类不能直接创建对象。例如:
AbstractClass obj; // 错误:无法实例化抽象类1 - 必须被继承:抽象类通常作为基类,派生类必须实现所有的纯虚函数。
- 可以包含普通成员函数:抽象类可以包含普通成员函数和成员变量。
- 可以包含虚函数:抽象类可以包含虚函数,这些虚函数可以有默认实现。
# 10.4.4 纯虚函数
纯虚函数是抽象类的核心特性。它没有函数体,派生类必须重写它。
virtual 返回类型 函数名(参数列表) = 0;
示例
class Shape {
public:
// 纯虚函数
virtual void draw() = 0;
};
2
3
4
5
虚函数是"可以重写"的函数(有默认实现),纯虚函数是"必须重写"的函数(无实现,定义接口规范)。
| 特性 | 虚函数 (Virtual Function) | 纯虚函数 (Pure Virtual Function) |
|---|---|---|
| 语法 | virtual void func(); | virtual void func() = 0; |
| 实现 | 基类中有函数体 | 基类中无函数体(=0) |
| 重写要求 | 派生类可选重写 | 派生类必须重写 |
| 类性质 | 可以是具体类 | 使类成为抽象类 |
| 实例化 | 可以创建对象 | 不能创建抽象类对象 |
# 10.4.5 派生类实现抽象类
派生类必须实现抽象类中的所有纯虚函数,否则派生类也会成为抽象类。
class Circle : public Shape {
public:
void draw() override {
std::cout << "Drawing a circle." << std::endl;
}
};
class Square : public Shape {
public:
void draw() override {
std::cout << "Drawing a square." << std::endl;
}
};
2
3
4
5
6
7
8
9
10
11
12
13
# 10.4.6 使用抽象类
抽象类通常用于多态场景,通过基类指针或引用调用派生类的实现。
int main() {
Shape* shape1 = new Circle();
Shape* shape2 = new Square();
shape1->draw(); // 输出: Drawing a circle.
shape2->draw(); // 输出: Drawing a square.
delete shape1;
delete shape2;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
# 10.4.7 抽象类与接口
在 C++ 中,没有专门的 interface 关键字,但可以通过抽象类实现接口的功能。通常,接口是一个只包含纯虚函数的抽象类。
class IPrintable {
public:
virtual void print() = 0;
virtual ~IPrintable() = default; // 虚析构函数
};
2
3
4
5
# 10.4.8 看个案例
这里面三种写法有什么区别?当然实际情况只可以用其中一种。这里只是用来分析。
class IPollStrategy {
public:
virtual ~IPollStrategy(){};
virtual ~IPollStrategy();
virtual ~IPollStrategy() = 0;
};
2
3
4
5
6
1.
virtual ~IPollStrategy(){};
含义: 这是一个 有实现的虚析构函数。它被声明为 virtual,表示这是一个虚函数,允许派生类重写它。它有一个空的函数体 {},表示这是一个具体的实现。
作用:确保派生类对象在销毁时能够正确调用析构函数。允许 IPollStrategy 类被实例化(即可以创建 IPollStrategy 的对象)。
class IPollStrategy {
public:
virtual ~IPollStrategy(){};
};
class Derived : public IPollStrategy {
public:
~Derived() override {
// 可以选择是否重写析构函数
}
};
int main() {
IPollStrategy* obj = new Derived();
delete obj; // 正确调用析构函数
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2.
virtual ~IPollStrategy();
含义: 这是一个 纯虚析构函数的声明。它被声明为 virtual,表示这是一个虚函数,允许派生类重写它。它没有函数体,表示这是一个纯虚函数。
作用: 使 IPollStrategy 类成为 抽象类,不能直接实例化。强制派生类实现析构函数。
注意事项:纯虚析构函数 必须在类外提供实现,否则会导致链接错误。
class IPollStrategy {
public:
virtual ~IPollStrategy() = 0; // 纯虚析构函数
};
// 在类外提供纯虚析构函数的实现
IPollStrategy::~IPollStrategy() {}
class Derived : public IPollStrategy {
public:
~Derived() override {
// 必须实现析构函数
}
};
int main() {
// IPollStrategy* obj = new IPollStrategy(); // 错误:不能实例化抽象类
IPollStrategy* obj = new Derived();
delete obj; // 正确调用析构函数
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
3.
virtual ~IPollStrategy() = 0;
含义: 这是一个 纯虚析构函数的定义。它被声明为 virtual,表示这是一个虚函数,允许派生类重写它。= 0 表示这是一个纯虚函数。
作用: 使 IPollStrategy 类成为 抽象类,不能直接实例化。强制派生类实现析构函数。
注意事项: 纯虚析构函数 必须在类外提供实现,否则会导致链接错误。
class IPollStrategy {
public:
virtual ~IPollStrategy() = 0; // 纯虚析构函数
};
// 在类外提供纯虚析构函数的实现
IPollStrategy::~IPollStrategy() {}
class Derived : public IPollStrategy {
public:
~Derived() override {
// 必须实现析构函数
}
};
int main() {
// IPollStrategy* obj = new IPollStrategy(); // 错误:不能实例化抽象类
IPollStrategy* obj = new Derived();
delete obj; // 正确调用析构函数
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
三种写法的区别
| 特性 | virtual ~IPollStrategy(){}; | virtual ~IPollStrategy(); | virtual ~IPollStrategy() = 0; |
|---|---|---|---|
| 是否有实现 | 有实现(空函数体 {}) | 无实现(纯虚函数) | 无实现(纯虚函数) |
| 是否为抽象类 | 不是抽象类,可以实例化 | 是抽象类,不能实例化 | 是抽象类,不能实例化 |
| 派生类是否需要实现 | 派生类可以选择是否重写析构函数 | 派生类必须实现析构函数 | 派生类必须实现析构函数 |
| 是否需要在类外实现 | 不需要 | 需要 | 需要 |
virtual ~IPollStrategy(); 和 virtual ~IPollStrategy() = 0;:两者本质上是相同的,但 = 0 是更标准的写法,明确表示纯虚函数。
在实际开发中,通常使用 virtual ~IPollStrategy() = 0; 来定义抽象类,因为它更清晰且符合 C++ 的语法规范。
# 10.4.9 综合案例与思考
#include <iostream>
#include <vector>
#include <memory>
using namespace std;
// 接口(纯抽象类):可序列化
class ISerializable {
public:
virtual string serialize() const = 0;
virtual ~ISerializable() = default;
};
// 接口:可打印
class IPrintable {
public:
virtual void print() const = 0;
virtual ~IPrintable() = default;
};
// 抽象基类:包含部分实现
class Document : public ISerializable, public IPrintable {
protected:
string title;
string author;
public:
Document(const string& t, const string& a) : title(t), author(a) {}
// 实现 IPrintable(通用部分)
void print() const override {
cout << "[" << getType() << "] " << title << " by " << author << endl;
}
// 留给子类实现的纯虚函数
virtual string getType() const = 0;
virtual ~Document() = default;
};
class PDFDocument : public Document {
int pages;
public:
PDFDocument(const string& t, const string& a, int p)
: Document(t, a), pages(p) {}
string getType() const override { return "PDF"; }
string serialize() const override {
return "PDF|" + title + "|" + author + "|pages=" + to_string(pages);
}
};
class WordDocument : public Document {
bool hasMacros;
public:
WordDocument(const string& t, const string& a, bool m)
: Document(t, a), hasMacros(m) {}
string getType() const override { return "Word"; }
string serialize() const override {
return "Word|" + title + "|" + author + "|macros=" + (hasMacros ? "yes" : "no");
}
};
int main() {
// Document doc("test", "yc"); // 错误!抽象类不能实例化
vector<unique_ptr<Document>> docs;
docs.push_back(make_unique<PDFDocument>("C++入门", "张三", 300));
docs.push_back(make_unique<WordDocument>("设计文档", "李四", true));
cout << "=== 多态打印 ===" << endl;
for (const auto& doc : docs) {
doc->print();
}
cout << "\n=== 序列化 ===" << endl;
for (const auto& doc : docs) {
cout << doc->serialize() << endl;
}
// 通过接口类型使用
cout << "\n=== 接口多态 ===" << endl;
ISerializable* serializable = docs[0].get();
cout << serializable->serialize() << endl;
return 0;
}
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
案例知识融合:本案例展示了抽象类与接口在实际设计中的应用。ISerializable 和 IPrintable 是纯接口(只含纯虚函数),Document 是抽象基类(含部分实现+纯虚函数),PDFDocument 和 WordDocument 是具体实现类。这种设计遵循了"依赖抽象而非具体"的原则,新增文档类型只需继承 Document 并实现纯虚函数,无需修改已有代码(开闭原则)。
思考题:
- 如果
PDFDocument没有实现serialize(),会发生什么?它自己能被实例化吗? - C++ 没有
interface关键字,但可以用纯抽象类模拟接口。与 Java 的 interface 相比,C++ 的方式有什么优势和劣势? - 虚析构函数写成
virtual ~ISerializable() = default;和写成virtual ~ISerializable() {}有什么区别?
# 10.5 综合案例
# 10.5.1 计算器类
案例描述:分别利用普通写法和多态技术,设计实现两个操作数进行运算的计算器类
多态的优点:1.代码组织结构清晰;2.可读性强;3.利于前期和后期的扩展以及维护
示例:普通写法
class Calculator {
public:
int getResult(string oper) {
if (oper == "+") {
return num1 + num2;
} else if (oper == "-") {
return num1 - num2;
} else if (oper == "*") {
return num1 * num2;
}
//如果要提供新的运算,需要修改源码
return 0;
}
public:
int num1;
int num2;
};
void test01() {
//普通实现测试
Calculator c;
c.num1 = 10;
c.num2 = 10;
cout << c.num1 << " + " << c.num2 << " = " << c.getResult("+") << endl;
cout << c.num1 << " - " << c.num2 << " = " << c.getResult("-") << endl;
cout << c.num1 << " * " << c.num2 << " = " << c.getResult("*") << endl;
}
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 AbstractCalculator {
public :
virtual int getResult() {
return 0;
}
int num1;
int num2;
};
//加法计算器
class AddCalculator : public AbstractCalculator {
public:
int getResult() {
return num1 + num2;
}
};
//减法计算器
class SubCalculator : public AbstractCalculator {
public:
int getResult() {
return num1 - num2;
}
};
//乘法计算器
class MulCalculator : public AbstractCalculator {
public:
int getResult() {
return num1 * num2;
}
};
void test02() {
//创建加法计算器
AbstractCalculator *abc = new AddCalculator;
abc->num1 = 10;
abc->num2 = 10;
cout << abc->num1 << " + " << abc->num2 << " = " << abc->getResult() << endl;
delete abc; //用完了记得销毁
//创建减法计算器
abc = new SubCalculator;
abc->num1 = 10;
abc->num2 = 10;
cout << abc->num1 << " - " << abc->num2 << " = " << abc->getResult() << endl;
delete abc;
//创建乘法计算器
abc = new MulCalculator;
abc->num1 = 10;
abc->num2 = 10;
cout << abc->num1 << " * " << abc->num2 << " = " << abc->getResult() << endl;
delete abc;
}
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
# 10.5.2 制作饮品
案例描述:制作饮品的大致流程为:煮水 - 冲泡 - 倒入杯中 - 加入辅料
利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡,茶叶,奶茶等。
//抽象制作饮品
class AbstractDrinking {
public:
//烧水
virtual void Boil() = 0;
//冲泡
virtual void Brew() = 0;
//倒入杯中
virtual void PourInCup() = 0;
//加入辅料
virtual void PutSomething() = 0;
//规定流程
void MakeDrink() {
Boil();
Brew();
PourInCup();
PutSomething();
}
};
//制作咖啡
class Coffee : public AbstractDrinking {
public:
//烧水
virtual void Boil() {
cout << "煮农夫山泉!" << endl;
}
//冲泡
virtual void Brew() {
cout << "冲泡咖啡!" << endl;
}
//倒入杯中
virtual void PourInCup() {
cout << "将咖啡倒入杯中!" << endl;
}
//加入辅料
virtual void PutSomething() {
cout << "加入牛奶!" << endl;
}
};
//制作茶水
class Tea : public AbstractDrinking {
public:
//烧水
virtual void Boil() {
cout << "煮自来水!" << endl;
}
//冲泡
virtual void Brew() {
cout << "冲泡茶叶!" << endl;
}
//倒入杯中
virtual void PourInCup() {
cout << "将茶水倒入杯中!" << endl;
}
//加入辅料
virtual void PutSomething() {
cout << "加入枸杞!" << endl;
}
};
//业务函数
void DoWork(AbstractDrinking *drink) {
drink->MakeDrink();
//delete drink; //报错, delete called on 'AbstractDrinking' that is abstract but has non-virtual destructor
}
void test() {
DoWork(new Coffee);
cout << "--------------" << endl;
DoWork(new Tea);
}
int main() {
test();
return 0;
}
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
打印结果如下所示:
煮农夫山泉!
冲泡咖啡!
将咖啡倒入杯中!
加入牛奶!
--------------
煮自来水!
冲泡茶叶!
将茶水倒入杯中!
加入枸杞!
2
3
4
5
6
7
8
9
# 10.5.3 组装电脑
案例描述:
- 电脑主要组成部件为 CPU(用于计算),显卡(用于显示),内存条(用于存储)
- 将每个零件封装出抽象基类,并且提供不同的厂商生产不同的零件,例如Intel厂商和Lenovo厂商
- 创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口
- 测试时组装三台不同的电脑进行工作
#include<iostream>
using namespace std;
//抽象CPU类
class CPU
{
public:
//抽象的计算函数
virtual void calculate() = 0;
};
//抽象显卡类
class VideoCard
{
public:
//抽象的显示函数
virtual void display() = 0;
};
//抽象内存条类
class Memory
{
public:
//抽象的存储函数
virtual void storage() = 0;
};
//电脑类
class Computer
{
public:
Computer(CPU * cpu, VideoCard * vc, Memory * mem)
{
m_cpu = cpu;
m_vc = vc;
m_mem = mem;
}
//提供工作的函数
void work()
{
//让零件工作起来,调用接口
m_cpu->calculate();
m_vc->display();
m_mem->storage();
}
//提供析构函数 释放3个电脑零件
~Computer()
{
//释放CPU零件
if (m_cpu != NULL)
{
delete m_cpu;
m_cpu = NULL;
}
//释放显卡零件
if (m_vc != NULL)
{
delete m_vc;
m_vc = NULL;
}
//释放内存条零件
if (m_mem != NULL)
{
delete m_mem;
m_mem = NULL;
}
}
private:
CPU * m_cpu; //CPU的零件指针
VideoCard * m_vc; //显卡零件指针
Memory * m_mem; //内存条零件指针
};
//具体厂商
//Intel厂商
class IntelCPU :public CPU
{
public:
virtual void calculate()
{
cout << "Intel的CPU开始计算了!" << endl;
}
};
class IntelVideoCard :public VideoCard
{
public:
virtual void display()
{
cout << "Intel的显卡开始显示了!" << endl;
}
};
class IntelMemory :public Memory
{
public:
virtual void storage()
{
cout << "Intel的内存条开始存储了!" << endl;
}
};
//Lenovo厂商
class LenovoCPU :public CPU
{
public:
virtual void calculate()
{
cout << "Lenovo的CPU开始计算了!" << endl;
}
};
class LenovoVideoCard :public VideoCard
{
public:
virtual void display()
{
cout << "Lenovo的显卡开始显示了!" << endl;
}
};
class LenovoMemory :public Memory
{
public:
virtual void storage()
{
cout << "Lenovo的内存条开始存储了!" << endl;
}
};
void test01()
{
//第一台电脑零件
CPU * intelCpu = new IntelCPU;
VideoCard * intelCard = new IntelVideoCard;
Memory * intelMem = new IntelMemory;
cout << "第一台电脑开始工作:" << endl;
//创建第一台电脑
Computer * computer1 = new Computer(intelCpu, intelCard, intelMem);
computer1->work();
delete computer1;
cout << "-----------------------" << endl;
cout << "第二台电脑开始工作:" << endl;
//第二台电脑组装
Computer * computer2 = new Computer(new LenovoCPU, new LenovoVideoCard, new LenovoMemory);;
computer2->work();
delete computer2;
cout << "-----------------------" << endl;
cout << "第三台电脑开始工作:" << endl;
//第三台电脑组装
Computer * computer3 = new Computer(new LenovoCPU, new IntelVideoCard, new LenovoMemory);;
computer3->work();
delete computer3;
}
## 10.6 继承和多态底层原理
### 10.6.1 继承的内存模型
**派生类对象在内存中包含完整的基类子对象**:
```cpp
class Base {
int x; // 4字节
double y; // 8字节
};
class Derived : public Base {
int z; // 4字节
};
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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
内存布局:
Derived对象的内存:
偏移量 内容 说明
0x00 x (int) 来自Base,4字节
0x04 padding 对齐到8字节边界
0x08 y (double) 来自Base,8字节
0x10 z (int) Derived自己的,4字节
0x14 padding 对齐到8字节(sizeof = 24)
2
3
4
5
6
7
关键点:基类子对象在前,派生类新增成员在后。这保证了将Derived*转换为Base*时,指针值不需要调整(对于单继承)。
多重继承的内存布局:
class A { int a; }; // sizeof = 4
class B { double b; }; // sizeof = 8
class C : public A, public B { int c; };
// C的内存布局:
// [A子对象: a(4)] [padding(4)] [B子对象: b(8)] [c(4)] [padding(4)]
// 总大小 = 24
2
3
4
5
6
7
多重继承中,将C*转换为B*时,编译器需要调整指针偏移:
C obj;
B* bp = &obj; // bp = &obj + sizeof(A) + padding = &obj + 8
// bp 和 &obj 的值不同!
2
3
这就是为什么多重继承下的指针转换不是简单的类型转换,编译器必须计算正确的偏移量。
# 10.6.2 虚函数表与动态绑定
虚函数的底层——vptr和vtable:
class Animal {
public:
virtual void speak() { cout << "..." << endl; }
virtual void eat() { cout << "eating" << endl; }
int age;
};
class Cat : public Animal {
public:
void speak() override { cout << "meow" << endl; }
// eat() 没有重写,继承基类版本
};
2
3
4
5
6
7
8
9
10
11
12
编译器生成的数据结构:
Animal的vtable(虚函数表,编译期生成,存储在只读数据段):
+--------+----------------------------+
| slot 0 | &Animal::speak() |
| slot 1 | &Animal::eat() |
+--------+----------------------------+
Cat的vtable:
+--------+----------------------------+
| slot 0 | &Cat::speak() ← 重写了 |
| slot 1 | &Animal::eat() ← 继承的 |
+--------+----------------------------+
Animal对象的内存布局:
+--------+----------------------------+
| vptr | → 指向Animal的vtable | 8字节
| age | 具体值 | 4字节
+--------+----------------------------+
Cat对象的内存布局:
+--------+----------------------------+
| vptr | → 指向Cat的vtable | 8字节(构造时设置)
| age | 具体值 | 4字节
+--------+----------------------------+
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
虚函数调用的汇编过程:
Animal* p = new Cat();
p->speak(); // 调用哪个speak?
2
; p->speak() 的汇编:
mov rax, QWORD PTR [rdi] ; 1. 从对象开头读取vptr
mov rax, QWORD PTR [rax] ; 2. 从vtable中读取第0个函数指针
call rax ; 3. 间接调用
2
3
4
这就是动态绑定的三步:①读vptr → ②查vtable → ③间接调用。相比直接调用(call 函数地址),多了两次内存访问,这就是虚函数的运行时开销。
构造函数中设置vptr:
Cat() {
// 编译器在构造函数开头插入:
this->vptr = &Cat_vtable;
}
2
3
4
继承链上每个构造函数都会设置vptr。Base的构造函数先将vptr设为Base_vtable,然后Cat的构造函数再改为Cat_vtable。这就是为什么在构造函数中调用虚函数不会触发多态——此时vptr还指向当前类的vtable。
# 10.6.3 虚继承的底层实现
普通菱形继承的内存问题:
class A { int a; };
class B : public A { int b; };
class C : public A { int c; };
class D : public B, public C { int d; };
// D的内存布局(无虚继承):
// [B子对象: [A子对象: a] b] [C子对象: [A子对象: a] c] [d]
// a出现了两次!
2
3
4
5
6
7
8
虚继承通过虚基类指针(vbptr)解决:
class B : virtual public A { int b; };
class C : virtual public A { int c; };
class D : public B, public C { int d; };
// D的内存布局(虚继承):
// [B子对象: vbptr_B, b] [C子对象: vbptr_C, c] [d] [共享的A子对象: a]
2
3
4
5
6
vbptr(Virtual Base Pointer)指向一个虚基类表(vbtable)- 虚基类表中存储了从当前位置到虚基类子对象的偏移量
- 虚基类子对象被放在对象末尾,所有继承路径共享这一份
- 访问虚基类成员时,需要通过vbptr查表计算偏移,有额外开销
这就是为什么虚继承有性能代价:每次访问虚基类成员都需要额外的间接寻址。
# 10.6.4 RTTI与dynamic_cast的代价
RTTI(Run-Time Type Information):
C++在vtable中存储了类型信息(type_info对象),typeid和dynamic_cast依赖于此。
Animal* p = new Cat();
cout << typeid(*p).name() << endl; // 输出Cat的类型名
Cat* cp = dynamic_cast<Cat*>(p); // 安全向下转换
2
3
4
dynamic_cast的底层过程:
- 通过vptr找到vtable
- 从vtable中获取
type_info - 沿继承链遍历,检查目标类型是否是实际类型的基类或本身
- 如果是多重继承,还需要计算指针偏移
dynamic_cast的时间复杂度与继承链深度相关,在性能关键路径上应该避免使用。替代方案包括:
- 虚函数(让多态自然工作,不需要判断类型)
static_cast(如果你确定类型,零开销但不安全)- 访问者模式(双重分发,避免类型判断)
# 10.7 继承和多态训练题
训练题1:实现图形继承体系
要求:设计一个图形继承体系,基类Shape提供面积和周长的纯虚函数接口,实现Circle、Rectangle、Triangle三个派生类,并用多态遍历计算总面积。
#include <iostream>
#include <vector>
#include <memory>
#include <cmath>
using namespace std;
class Shape {
public:
virtual double area() const = 0;
virtual double perimeter() const = 0;
virtual string type() const = 0;
virtual ~Shape() = default;
void describe() const {
cout << type() << ": 面积=" << area()
<< " 周长=" << perimeter() << endl;
}
};
class Circle : public Shape {
double r_;
public:
explicit Circle(double r) : r_(r) {}
double area() const override { return M_PI * r_ * r_; }
double perimeter() const override { return 2 * M_PI * r_; }
string type() const override { return "圆"; }
};
class Rectangle : public Shape {
double w_, h_;
public:
Rectangle(double w, double h) : w_(w), h_(h) {}
double area() const override { return w_ * h_; }
double perimeter() const override { return 2 * (w_ + h_); }
string type() const override { return "矩形"; }
};
class Triangle : public Shape {
double a_, b_, c_;
public:
Triangle(double a, double b, double c) : a_(a), b_(b), c_(c) {}
double area() const override {
double s = (a_ + b_ + c_) / 2;
return sqrt(s * (s - a_) * (s - b_) * (s - c_));
}
double perimeter() const override { return a_ + b_ + c_; }
string type() const override { return "三角形"; }
};
int main() {
vector<unique_ptr<Shape>> shapes;
shapes.push_back(make_unique<Circle>(5));
shapes.push_back(make_unique<Rectangle>(4, 6));
shapes.push_back(make_unique<Triangle>(3, 4, 5));
double totalArea = 0;
for (const auto& s : shapes) {
s->describe();
totalArea += s->area();
}
cout << "总面积: " << totalArea << endl;
return 0;
}
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
练习重点:纯虚函数定义接口、override重写、unique_ptr管理多态对象、虚析构函数。
训练题2:观察虚函数表
要求:编写代码验证vptr和vtable的存在,观察虚函数调用的间接性。
#include <iostream>
using namespace std;
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
int data = 42;
};
class Derived : public Base {
public:
void func1() override { cout << "Derived::func1" << endl; }
virtual void func3() { cout << "Derived::func3" << endl; }
};
int main() {
Base b;
Derived d;
// 1. vptr在对象开头
cout << "=== vptr位置 ===" << endl;
cout << "Base对象地址: " << &b << endl;
cout << "Base::data地址: " << &b.data << endl;
cout << "vptr占据了前" << ((char*)&b.data - (char*)&b) << "字节" << endl;
// 2. 不同类的vptr不同
cout << "\n=== vptr值(vtable地址)===" << endl;
void* vptr_base = *(void**)&b;
void* vptr_derived = *(void**)&d;
cout << "Base的vtable: " << vptr_base << endl;
cout << "Derived的vtable: " << vptr_derived << endl;
cout << "vtable不同? " << (vptr_base != vptr_derived ? "是" : "否") << endl;
// 3. 通过vtable手动调用虚函数
cout << "\n=== 手动通过vtable调用 ===" << endl;
using FuncPtr = void(*)(void*);
void** vtable = *(void***)&d;
FuncPtr f1 = (FuncPtr)vtable[0]; // func1
FuncPtr f2 = (FuncPtr)vtable[1]; // func2
f1(&d); // Derived::func1
f2(&d); // Base::func2(未重写)
// 4. sizeof验证vptr占用空间
cout << "\n=== sizeof验证 ===" << endl;
cout << "sizeof(Base) = " << sizeof(Base) << " (vptr8 + data4 + pad4 = 16)" << endl;
cout << "sizeof(Derived) = " << sizeof(Derived) << endl;
return 0;
}
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
练习重点:理解vptr在对象内存中的位置、不同类有不同的vtable、通过vtable指针手动调用虚函数验证动态绑定机制。
训练题3:模板方法模式实践
要求:使用继承和多态实现模板方法设计模式——基类定义算法框架,派生类实现具体步骤。
#include <iostream>
#include <string>
#include <chrono>
using namespace std;
// 模板方法基类:数据处理流水线
class DataProcessor {
public:
// 模板方法:定义算法框架(不可重写)
void process(const string& input) {
cout << "=== 开始处理 ===" << endl;
string data = readData(input);
string parsed = parseData(data);
string result = transformData(parsed);
outputResult(result);
cout << "=== 处理完成 ===" << endl;
}
virtual ~DataProcessor() = default;
protected:
// 这些步骤由派生类实现
virtual string readData(const string& source) = 0;
virtual string parseData(const string& raw) = 0;
virtual string transformData(const string& parsed) = 0;
// 钩子方法:有默认实现,派生类可选择重写
virtual void outputResult(const string& result) {
cout << "输出: " << result << endl;
}
};
class CsvProcessor : public DataProcessor {
protected:
string readData(const string& source) override {
cout << "读取CSV: " << source << endl;
return "name,age\nAlice,30\nBob,25";
}
string parseData(const string& raw) override {
cout << "解析CSV为结构化数据" << endl;
return "[{name:Alice,age:30},{name:Bob,age:25}]";
}
string transformData(const string& parsed) override {
cout << "过滤age>28的记录" << endl;
return "[{name:Alice,age:30}]";
}
};
class JsonProcessor : public DataProcessor {
protected:
string readData(const string& source) override {
cout << "读取JSON: " << source << endl;
return R"({"users": [{"name": "Charlie"}]})";
}
string parseData(const string& raw) override {
cout << "解析JSON" << endl;
return "[Charlie]";
}
string transformData(const string& parsed) override {
cout << "转换为大写" << endl;
return "[CHARLIE]";
}
void outputResult(const string& result) override {
cout << "[JSON格式输出] " << result << endl;
}
};
int main() {
CsvProcessor csv;
csv.process("data.csv");
cout << endl;
JsonProcessor json;
json.process("data.json");
// 多态调用
cout << "\n=== 多态调用 ===" << endl;
DataProcessor* processors[] = {&csv, &json};
for (auto* p : processors) {
p->process("source");
cout << endl;
}
return 0;
}
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
练习重点:模板方法模式(基类定义框架+派生类实现步骤)、纯虚函数与钩子方法的区别、多态在设计模式中的应用。
# 10.8 综合思考题
虚函数的性能代价:虚函数调用比普通函数调用多了两次内存间接访问(读vptr + 读vtable slot)。在现代CPU上,这通常只有1-2个时钟周期的额外开销,但虚函数调用无法被内联,这才是真正的性能瓶颈。请思考:为什么虚函数不能内联?编译器的"去虚拟化(devirtualization)"优化是如何在某些场景下消除虚函数开销的?
CRTP(奇异递归模板模式)——静态多态:CRTP通过模板实现编译期多态,避免虚函数开销:
template<class T> class Base { void interface() { static_cast<T*>(this)->impl(); } }。请对比CRTP和虚函数多态的优缺点。CRTP适合什么场景?为什么标准库的std::enable_shared_from_this使用CRTP?C++20的Concepts vs 虚函数:C++20引入的Concepts可以约束模板参数必须满足某些接口,类似于"编译期接口"。请思考:Concepts能否完全替代虚函数?它们分别适用于哪些场景?"编译期多态"和"运行期多态"的根本区别是什么?
多重继承争议:Java和C#只支持单继承(接口除外),Go只支持组合,而C++支持多重继承。请分析多重继承的利弊。为什么现代语言倾向于避免多重继承?C++中有什么最佳实践来安全使用多重继承?(提示:只从接口类多重继承,最多一个实现类)
# 10.Y 新手陷阱 Top 5
| # | 陷阱 | 说明 |
|---|---|---|
| 1 | 基类析构非 virtual | delete base_ptr; 不调用派生类析构 → 资源泄漏 |
| 2 | 构造/析构里调用虚函数 | 调到的是当前类版本,不是子类(对象未完全构造) |
| 3 | 切片(slicing) | Base b = derived; 派生类信息丢失;用引用/指针传 |
| 4 | 多重继承的菱形继承 | 用虚继承 virtual public,且最派生类负责构造虚基类 |
| 5 | 不写 override | 基类签名变化时编译器无法帮你检查 |