目录
一、多态
1.1.概念
通俗来讲,多态就是多种形态,具体点就是当不同的对象去完成某个行为会产生出不同的状态。
1.2.构成条件
必须有继承关系,子类必须重写父类的虚函数。
1.3.虚函数的重写(覆盖)
派生类中有一个跟基类完全相同的函数(即派生类函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的函数重写了基类的虚函数。可以在重写函数后面添加override关键字(可选),该关键字可以增强代码的可读性和安全性。
示例:
// 基类
class Person
{
public:
// 基类的虚函数
virtual void BuyTicket()
{
cout << "我是父类的买票函数" << endl;
}
};
// 派生类
class Student : public Person
{
// 对基类虚函数的重写
void BuyTicket()
{
cout << "我是子类的买票函数" << endl;
}
};
1.4.多态的分类
- 静态多态:函数重载 和 运算符重载属于静态多态,复用函数名;
- 动态多态:派生类和虚函数实现;
区别
- 静态多态的函数==地址早绑定== - 编译阶段确定函数地址 重载
- 动态多态的函数==地址晚绑定== - 运行阶段确定函数地址 重写 : 和虚函数完全一样< 返回值类型 函数名 参数列表>
二、纯虚函数和虚函数
(一)虚函数
1.1.定义和作用
虚函数是在基类中使用关键字 virtual 声明的成员函数,它允许派生类对其进行重写(Override),实现运行时多态。当通过基类指针或引用调用虚函数时,实际调用的是对象类型对应的派生类中的函数,这个过程称为动态绑定(Dynamic Binding)或晚绑定(Late Binding)。
1.2.实现原理
虚函数的实现原理基于虚函数表(Virtual Table,简称VTable)。每个使用虚函数的类都有一个虚函数表,该表是一个函数指针数组,存储了指向类的虚函数的指针。类的每个实例都包含一个指向其虚函数表的指针(vptr),通过这个指针可以找到并调用正确的虚函数实现。
当派生类覆盖(重写)基类的虚函数时,派生类的虚函数表中相应位置的函数指针会被更新为指向派生类中的函数。如果派生类没有重写虚函数,则派生类的虚函数表中会保留指向基类虚函数的指针。
#include <iostream>
using namespace std;
class Base {
public:
virtual void show() {
cout << "Base class show" << endl;
}
};
class Derived : public Base {
public:
void show() override {
cout << "Derived class show" << endl;
}
};
int main() {
Base* b = new Derived();
b->show(); // 输出:Derived class show
delete b;
return 0;
}
(二)纯虚函数
1.1.定义
纯虚函数是在基类中声明但不实现的虚函数,其声明方式是在虚函数声明的结尾处添加 = 0
。
1.2.作用
纯虚函数的主要作用是定义接口规范,强制要求派生类必须实现这些函数,从而实现接口的统一和标准化。
#include <iostream>
using namespace std;
//基类
class Person {
public:
virtual void draw() = 0; // 纯虚函数
};
//子类
class Student : public Person {
public:
//重写
void draw() override {
cout << "Drawing a circle" << endl;
}
};
int main() {
Person* p1 = new Student();
p1->draw(); // 输出:Drawing a circle
delete s1;
return 0;
}
三、抽象类
1.1.定义
类中如果包含至少一个纯虚函数,则该类成为抽象类(Abstract Class)。
1.2.特点
- 抽象类无法实例化对象
- 抽象类的子类必须重写抽象类的纯虚函数,否则也属于抽象类
- 在函数的形参中 , 还是可以用 抽象类作为 引用参数
四、多态实现原理
在了解底层逻辑之前,我们需要了解两种指针和一个概念,分别是vptr和vtable两种指针以及动态绑定概念。
1.1.虚函数表 vtable
定义:每个包含虚函数的类都会有一个虚函数表,它是指向虚函数的指针数组。
内容:表中存储了类的所有虚函数地址。当一个类被继承并重写了某些虚函数时,子类的虚函数表会存储它们新的实现。
1.2.虚指针 vptr
定义:每个对象在创建时会包含一个虚指针(vptr),它指向该对象所属类的虚函数表。
作用:通过 vptr,程序可以在运行时确定应该调用哪个具体的虚函数。
1.3.示例
#include <iostream>
#include <string>
using namespace std;
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
int main(int argc, char const *argv[])
{
Base b;
cout << sizeof(b) << endl;
return 0;
}
运行上面代码我们会发现b对象是16字节。
解释:
在 C++ 中,当一个类包含虚函数时,会发生以下情况:
- 对于 Base 类,因为它有一个虚函数
Func1()
,编译器会为该类创建一个虚函数表(vtable)。这个虚函数表是一个指针数组,用于存储虚函数的地址。通常,这个指针的大小是 8 字节(在 64 位系统中)。除了虚函数表指针外,Base
类还有一个私有成员变量_b
,它是int
类型,在大多数系统中,int
的大小是 4 字节。 - 编译器通常会对类的对象进行内存对齐,以提高内存访问效率。在这个例子中,为了让对象的大小是 8 的倍数(因为虚函数表指针是 8 字节),编译器会在
_b
后面填充 4 字节,使得整个对象的大小达到 16 字节。 - 因此,
sizeof(b)
的结果是 16 字节,其中包括 8 字节的虚函数表指针,4 字节的_b
成员变量,以及 4 字节的填充字节。
运行下面代码我们会发现b对象是16字节,d对象也是16字节。
解释:
对于 Base
类:
- 因为它有两个虚函数
Func1()
和Func2()
,编译器会为其创建一个虚函数表(vtable),这个虚函数表的指针大小在 64 位系统中是 8 字节。 - 它还有一个私有成员变量
_b
是int
类型,通常为 4 字节。 - 为了内存对齐(通常以 8 字节对齐),会填充 4 字节,所以
Base
类对象的大小是 16 字节。
对于 Derive
类:
- 它继承自
Base
,会继承Base
的虚函数表指针,大小为 8 字节。 - 它有自己的私有成员变量
_d
是int
类型,为 4 字节。 - 它重写了
Func1()
函数,但不会改变虚函数表的指针大小,只是修改了表中Func1()
函数的指向。 - 同样为了内存对齐,会填充 4 字节,所以
Derive
类对象的大小也是 16 字节。
// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
通过观察和测试,我们发现了以下几点问题:
-
派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
- 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
- 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
- 虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。
总结一下派生类的虚表生成:
a.先将基类中的虚表内容拷贝一份到派生类虚表中
b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。