1. 多态的基本概念
1.1 多态概念
对于同一种指令,不同的对象会产生不同的行为
1.2 静态多态
包括:函数重载、运算符重载、模板
发生的时机:编译时
1.3 动态多态
发生的时机:运行时,使用虚函数进行体现
2. 虚函数
概念:是一个 成员函数 ,并且在前面加上
virtual
关键字
性质:如果在基类中定义类虚函数,那么在派生类中该函数就是虚函数,即使在派生类中没有加virtual
重定义(重写、覆盖):派生类要保证该虚函数的名字、返回类型、参数列表(包括参数的类型、个数、顺序)都要和基类的相同,唯一可以不同的就只有函数体
虚函数原理(重要):当基类定义了虚函数,就会在该类创建的对象的存储布局的前面,新增一个虚函数指针,该指针指向虚函数表(简称虚表),类似数组的形式,虚表中存的是虚函数的入口地址(有多少虚函数都会入虚表)。
当派生类继承基类的时候,会满足吸收的特点,那么派生类也会有该虚函数,所以派生类创建的对象的内存布局的前面,也会新增一个虚函数指针,该指针指向派生类自己的虚函数表(简称虚表),虚表中存的是派生类的虚函数的入口地址,如果此时派生类重写了从基类那里吸收过来的虚函数,那么就会用派生类自己的虚函数的入口地址覆盖从基类那里吸收过来的虚函数的入口地址。
![]()
虚函数机制被激活的条件(重要):
1. 基类中定义虚函数
2. 派生类中重写该虚函数
3. 创建派生类对象
4. 用基类指针指向(引用绑定)派生类对象
5. 使用基类指针(引用)调用该虚函数
2.1 哪些函数不能定义为虚函数
- 普通函数(自由函数、全局函数):虚函数必须是成员函数,而普通函数是非成员函数
- 内联成员函数:内联成员函数在进行替换时,发生时机在编译的时候,而虚函数要体现多态,发生时机在运行的时候;如果将内联函数设置为虚函数,那次是就会失去内联的含义
- 静态成员函数:静态成员函数发生实际在编译的时候,而虚函数要体现多态,发生时机在运行的时候;静态成员函数是共享的,被该类的所有成员共享,并且没有this指针
- 友元函数:如果友元函数本身是一个普通函数,那么友元函数不能被设置为虚函数;如果友元函数本身是另一个类的成员函数,是可以被设置为虚函数的,但友元关系不能被继承,所以不能体现多态,没啥用
- 构造函数:构造函数不能被继承,但是虚函数是可以被继承的;构造函数发生的时机在编译的时候,而虚函数要体现多态,发生时机在运行的时候;如果构造函数被设置为虚函数,那么要体现出多态,就需要放在虚表中,而要使用虚函数指针找到虚表,而如果构造函数都不调用,那对象是没有完全创建出来的,对象都不完整,此时有没有虚函数指针都不一定。
2.2 虚函数的访问
- 使用指针与引用可以体现出动态多态
- 使用对象不能体现动态多态,体现的是虚函数作为普通函数的特征
- 在其他普通成员函数访问虚函数有可能体现动态多态
![]()
- 在构造函数与析构函数访问虚函数不能体现动态多态
2.3 纯虚函数
抽象类的第一种形式
// 声明纯虚函数的类叫做抽象类,不能创建对象
// 抽象类是作为接口使用的
class Base
{
public:
// 纯虚函数,只有定义,交给派生类去实现
virtual void show() const = 0;
virtual void print() const = 0;
};
class Derived // 有纯虚函数未实现,仍为抽象类
: public Base
{
public:
virtual void show() const override // override 表示重写,其修饰的函数形式必须与被其重写的函数一样
{
cout << "void Derived::show() const" << endl;
}
};
class Derived2
: public Derived
{
public:
virtual void print() const override
{
cout << "Derived2::print()" << endl;
}
};
void test()
{
Derived2 derived2;
Base *pbase = &derived2; // 抽象类不能创建对象,但能创建该种类型的指针或引用
Base &ref = derived2;
}
抽象类的第二种形式
class Base
{
protected: // 将基类构造函数设置为protected,不能创建对象,但派生类可以访问
Base()
{
cout << "Base()" << endl;
};
};
class Derived
: public Base
{
public:
Derived()
: Base()
{
cout << "Derived()" << endl;
}
}
2.4 虚析构函数
- 为了解决堆对象使用多态时,delete调用的是基类的析构函数而产生的内存泄漏,需要将基类的析构函数设置为虚函数
- 当把基类的析构函数设置为虚函数后,派生类的析构函数会自动称为虚函数,此时可以看成派生类重写了基类的析构函数
- 为什么命名两个析构函数的名字不一样却能够重写呢?因为对于任何一个类而言,析构函数只有一个,具有唯一性,那么编译器会将析构函数改成一个同一的名字~destructor
2.5 三个基本概念
- 重载:发生在同一个作用域中,函数的名字相同,但参数列表不一样(包括参数个数、参数类型、参数顺序)
- 重写(重定义,覆盖):发生在基类和派生类中,必须是虚函数,函数名相同,参数列表也相同
- 隐藏:发生在基类和派生类中,派生类中的函数和基类中的 函数名字 相同。(和是不是虚函数、参数列表是不是一样没有关系),派生类的数据成员也可以隐藏基类中的同名数据成员,想使用的话加上作用域限定符::
2.6 虚表的存在
对普通单继承而言,虚表只有一张,位于只读段,被类的所有对象共享
![]()
2.7 多基派生
3. 虚拟继承
3.1 虚的含义:存在、间接、共享
虚拟继承的共享性:如果继承链上存在虚拟继承的基类,则最底层的子类要负责完成该虚基类部分成员的构造,即它要显式调用虚基类的构造函数来完成初始化