大纲
多继承
多继承概念
1 2 3 4
| class 派生类名 : 访问控制 基类名1 , 访问控制 基类名2 , … , 访问控制 基类名n { 数据成员和成员函数声明 };
|
多继承的简单应用
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
| #include <iostream>
using namespace std;
class Base1 {
public: Base1(int a) { this->a = a; }
void printA() { cout << "a = " << a << endl; }
private: int a; };
class Base2 {
public: Base2(int b) { 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) : Base1(a), Base2(b) { this->c = c; }
void printC() { cout << "c = " << c << endl; }
private: int c; };
int main() { Base3 base(1, 2, 3); base.printA(); base.printB(); base.printC(); return 0; }
|
程序运行的输出结果如下:
派生类的构造函数和成员访问
在多继承的派生类中,其构造函数和成员访问的特性如下:
- 拥有多个基类的派生类的构造函数,可以用初始化列表调用基类构造函数来初始化数据成员。
- 执行顺序与单继承构造函数情况类似,多个直接基类构造函数执行顺序取决于定义派生类时声明的各个继承基类的顺序。
- 一个派生类对象拥有多个直接或间接基类的成员。不同名成员访问不会出现二义性;如果不同的基类有同名成员,那么就会存在二义性,在派生类对象访问时应该加以识别,可以采用继承中的同名成员的处理方式来解决二义性。
虚继承
虚继承的概念
- 虚继承总结
- 如果一个派生类从多个基类继承,而这些基类又有一个共同的基类(公共基类),则在对该基类中声明的成员进行访问时,可能会产生二义性。
- 如果在多条继承路径上有一个公共的基类,那么在继承路径的某处汇合点,这个公共基类就会在派生类的对象中产生多个基类子对象
- 要使这个公共基类在派生类中只产生一个子对象,必须对这个基类声明为虚继承,使这个基类成为
虚基类
。 - 虚继承声明需要使用关键字:
virtual
- 虚继承的底层是靠虚基类指针(
vbptr
)和虚基类表来实现
虚继承的简单应用
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.printA(); base.printB(); base.printC(); base.printX(); 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 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| #include <iostream>
using namespace std;
class Animal {
public: int m_Age;
};
class Sheep : virtual public Animal {
};
class Camel : virtual public Animal {
};
class CamelSheep : public Sheep, public Camel {
};
int main() { CamelSheep camelSheep;
camelSheep.Sheep::m_Age = 10; camelSheep.Camel::m_Age = 20;
cout << camelSheep.m_Age << endl; cout << camelSheep.Sheep::m_Age << endl; cout << camelSheep.Camel::m_Age << endl;
return 0; }
|
程序运行的输出结果如下:
假设上述代码不使用虚继承,那么 camelSheep.m_Age
就会存在二义性,导致编译器执行编译时会出错,完整的代码如下:
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 Animal {
public: int m_Age;
};
class Sheep : public Animal {
};
class Camel : public Animal {
};
class CamelSheep : public Sheep, public Camel {
};
int main() { CamelSheep camelSheep;
camelSheep.Sheep::m_Age = 10; camelSheep.Camel::m_Age = 20;
cout << camelSheep.Sheep::m_Age << endl; cout << camelSheep.Camel::m_Age << endl;
return 0; }
|
程序运行的输出结果如下:
不适用场景的介绍
对于 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
| #include <iostream>
using namespace std;
class Base1 {
public: Base1(int a) { this->a = a; }
void print() { cout << "a = " << a << endl; }
private: int a; };
class Base2 {
public: Base2(int b) { this->b = b; }
void print() { cout << "b = " << b << endl; }
private: int b; };
class Base3 : virtual public Base1, virtual public Base2 {
public: Base3(int a, int b) : Base1(a), Base2(b) {
} };
int main() { Base3 base(1, 2);
base.Base1::print(); base.Base2::print(); return 0; }
|
程序运行的输出结果如下:
虚基类的工作原理
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
| #include <iostream>
using namespace std;
class Animal {
public: int m_Age;
};
class Sheep : virtual public Animal {
};
class Camel : virtual public Animal {
};
class CamelSheep : public Sheep, public Camel {
};
int main() { CamelSheep camelSheep;
camelSheep.Sheep::m_Age = 10; camelSheep.Camel::m_Age = 20;
cout << camelSheep.m_Age << endl; cout << camelSheep.Sheep::m_Age << endl; cout << camelSheep.Camel::m_Age << endl;
cout << "----------虚基类的内部工作原理分析-----------" << endl;
cout << *(int*)((int*)*(int*)&camelSheep + 1) << endl;
cout << *((int*)((int*)*((int*)&camelSheep + 1) + 1)) << endl;
cout << ((Animal*)((char*)&camelSheep + *(int*)((int*)*(int*)&camelSheep + 1)))->m_Age << endl;
return 0; }
|
在 Visual Studio 中,程序运行的输出结果如下:
1 2 3 4 5 6 7
| 20 20 20 ----------虚基类的内部工作原理分析----------- 8 4 20
|
使用 Visual Studio 工具分析 CamelSheep 类的内存布局:
1
| cl /d1 reportSingleClassLayoutCamelSheep main.cpp
|
得到的内存布局结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class CamelSheep size(12): +--- 0 | +--- (base class Sheep) 0 | | {vbptr} | +--- 4 | +--- (base class Camel) 4 | | {vbptr} | +--- +--- +--- (virtual base Animal) 8 | m_Age +---
CamelSheep::$vbtable@Sheep@: 0 | 0 1 | 8 (CamelSheepd(Sheep+0)Animal)
CamelSheep::$vbtable@Camel@: 0 | 0 1 | 4 (CamelSheepd(Camel+0)Animal) vbi: class offset o.vbptr o.vbte fVtorDisp Animal 8 0 4 0
|
多态
多态是面向对象的三大概念(如下)之一,按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会使用到多态。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
| #include <iostream>
using namespace std;
class Parent {
public: Parent(int a) { this->a = a; }
void print() { cout << "I'm parent, a = " << a << endl; }
private: int a; };
class Child : public Parent { public:
Child(int a, int c) : Parent(a) { this->c = c; }
void print() { cout << "I'm child, c = " << c << endl; }
private: int c; };
int main() { Child child(3, 7);
child.print();
child.Parent::print();
return 0; }
|
程序运行的输出结果如下:
1 2
| I'm child, c = 7 I'm parent, a = 3
|
函数重写与函数重载的区别
函数重载
- 必须在同一个类中进行
- 子类无法重载父类的函数,父类同名函数将被子类的覆盖
- 重载是在编译期间根据参数类型、个数和顺序决定函数的调用
函数重写
- 必须发生于父类与子类之间
- 父类与子类中的函数必须有完全相同的原型
- 使用
virtual
关键字声明之后,能够产生多态(如果不使用 virtual
关键字声明,那叫重定义)
虚函数
类型兼容原则遇上函数重写
当 类型兼容原则 遇上函数重写时,执行以下代码后会出现意外的现象,即被调用的永远是父类的函数。
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 Parent {
public: Parent(int a) { this->a = a; }
void print() { cout << "I'm parent, a = " << a << endl; }
private: int a; };
class Child : public Parent { public:
Child(int c) : Parent(c) { this->c = c; }
void print() { cout << "I'm child, c = " << c << endl; }
private: int c; };
int main() { Parent* p = NULL; Parent parent(6); Child child(5);
p = &parent; p->print();
p = &child; p->print();
return 0; }
|
程序运行的输出结果如下:
1 2
| I'm parent, a = 6 I'm parent, a = 5
|
C/C++ 是静态编译型语言,在执行编译时,编译器会自动根据指针的类型判断指向的是一个什么样的对象。但在编译 print()
函数的时候,编译器不可能知道指针 p
究竟指向了什么对象,因为程序还没有运行。同时编译译器没有理由报错,于是编译器认为最安全的做法是编译到父类的 print()
函数,因为父类和子类肯定都有相同的 print()
函数。这就是所谓的 静态多态
或 静态联编
,函数调用在程序执行之前就已经准备好了;有时候这也被称为 早绑定
,因为 print()
函数在程序编译期间就已经设置好了。这就引出了面向对象新的需求,希望根据实际的对象类型来判断重写函数的调用;如果父类指针指向的是父类对象则调用父类中定义的函数,如果父类指针指向的是子类对象则调用子类中定义的重写函数,如图所示。
虚函数的应用
C++ 中通过 virtual
关键字对多态进行支持,使用 virtual
关键字声明的函数被重写后即可展现多态特性,一般称之为 虚函数
。
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
| #include <iostream>
using namespace std;
class Parent {
public: Parent(int a) { this->a = a; }
virtual void print() { cout << "I'm parent, a = " << a << endl; }
private: int a; };
class Child : public Parent { public:
Child(int c) : Parent(c) { this->c = c; }
virtual void print() { cout << "I'm child, c = " << c << endl; }
private: int c; };
int main() { Parent* p = NULL; Parent parent(6); Child child(5);
p = &parent; p->print();
p = &child; p->print();
return 0; }
|
程序运行的输出结果如下:
1 2
| I'm parent, a = 6 I'm child, c = 5
|
此时,编译器看的是指针的内容,而不是它的类型。因此,由于 Parent
和 Child
类的对象的地址存储在 *p
中,所以会调用各自的 print()
函数。正如所看到的,父类 Parent
的每个子类都有一个 print()
函数的独立实现。这就是多态的一般使用方式,即使用一个父类的指针或引用去调用子类中被重写的方法。有了多态就可以有多个不同的实现类,它们都带有同一个名称但具有不同实现的函数,函数的参数甚至可以是相同的。
虚析构函数
虚析构函数的作用:为了避免内存泄漏,通过父类的指针,可以将所有子类对象的析构函数都执行一遍(释放所有的子类资源)。即虚析构函数使得在删除指向子类对象的父类指针时,可以调用子类的析构函数来实现释放子类中堆内存的目的,从而防止内存泄漏。
- 析构函数可以是虚的,虚析构函数用于指引
delete
运算符正确析构动态对象 - 构造函数不能是虚函数,因为建立一个派生类对象时,必须从类层次的根开始,沿着继承路径逐个调用基类的构造函数
虚析构函数的简单应用
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
| #include <iostream>
using namespace std;
class A {
public: A() { this->p = new char[20]; strcpy(p, "Hello A"); cout << "A 类的构造函数被调用" << endl; }
virtual ~A() { delete[] this->p; cout << "A 类的析构函数被调用" << endl; }
private: char* p; };
class B : public A {
public: B() { this->p = new char[20]; strcpy(p, "Hello B"); cout << "B 类的构造函数被调用" << endl; }
~B() { delete[] this->p; cout << "B 类的析构函数被调用" << endl; }
private: char* p; };
int main() { B* b = new B(); delete b;
cout << endl;
A* a = new B(); delete a; return 0; }
|
程序运行的输出结果如下:
1 2 3 4 5 6 7 8 9
| A 类的构造函数被调用 B 类的构造函数被调用 B 类的析构函数被调用 A 类的析构函数被调用
A 类的构造函数被调用 B 类的构造函数被调用 B 类的析构函数被调用 A 类的析构函数被调用
|
虚析构函数的作用总结
- (a) 如果基类的析构函数不加
virtual
关键字修饰,那么就是普通析构函数- 当基类中的析构函数没有声明为虚析构函数时,派生类开始从基类继承,基类的指针指向派生类的对象时,
delete
基类的指针时,只会调用基类的析构函数,不会调用派生类的析构函数
- (b) 如果基类的析构函数加
virtual
关键字修饰,那么就是虚析构函数- 当基类中的析构函数声明为虚析构函数时,派生类开始从基类继承,基类的指针指向派生类的对象时,
delete
基类的指针时,先调用派生类的析构函数,再调用基类中的析构函数
多态的理论基础
联编
:是指一个程序模块、代码之间互相关联的过程静态联编
:是程序的匹配、连接在编译阶段实现,也称为早期联编(早绑定)动态联编
:是指程序联编推迟到运行时进行,所以又称为晚期联编(迟绑定)- 虚函数、
switch
语句和 if
语句属于动态联编
多态理论联系实际应用(代码示例):
- C++ 与 C 相同,是静态编译型语言
- 在编译时,编译器会自动根据指针的类型判断指向的是一个什么样的对象,所以编译器认为父类指针指向的是父类对象
- 由于程序没有运行,所以不可能知道父类指针指向的具体是父类对象还是子类对象
- 从程序安全的角度,编译器假设父类指针只指向父类对象,因此编译的结果为调用父类的成员函数,这种特性就是
静态联编
多态成立的三个必要条件
- (a) 要有继承
- (b) 要有虚函数重写
- (c) 父类指针或引用指向子类对象
C++ 11 的 override 和 final
override 关键字
:用来检查函数是否重写,在子类中的函数声明里加上该关键字 virtual void fun() override {}
,编译器就会自动检查对应的函数是否重写了父类中的函数。final 关键字
:在类的声明中加上该关键字 class A final {};
,目的是为了不让这个类被继承。或者,在一个函数后加上该关键字,表示这个函数不能被重写 void fun() final {}
。