多态性(polymorphism)是面向对象编程的一个重要概念,它允许基类的指针或引用在运行时可以指向不同的派生类对象,并根据实际对象的类型调用相应的成员函数。
简单来说,多态是不同继承关系的类对象,调用同一函数,产生了不同的行为。
同时,必须满足以下构成条件才称为多态:
存在继承关系
使用基类的指针或引用调用函数
通过将派生类对象的地址赋值给基类指针或引用,可以在运行时根据实际对象的类型来动态调用适当的函数。这样才能实现多态性,使得同名函数在不同的派生类对象中产生不同的行为。
图例:

定义:
class Base {
public:
virtual void func() { // 使用 virtual 关键字声明虚函数
// 函数实现
}
};
虚函数的重写是指 派生类中重新实现(覆盖)基类中已经声明为虚拟的函数。
一般重写函数时可以加上override关键字
代码举例:
// 基类
class Base
{
public:
virtual void func()
{
std::cout << "Base::func()" << std::endl;
}
};
// 派生类
class Derived : public Base
{
public:
// void func() override{}
void func() // 省略了override关键字
{
std::cout << "Derived::func()" << std::endl;
}
};
int main()
{
// 使用基类指针指向派生类对象
Base* basePtr = new Derived();
// 动态调用虚函数,根据实际对象类型选择不同的实现
basePtr->func(); // 输出: Derived::func()
delete basePtr;
return 0;
}
其中,虚函数的重写有两种特殊情况:
但是 返回类型是 基类虚函数返回类型的子类型
代码举例:
class A{}; // 基类
class B : public A {}; // 派生类
// 基类Person
class Person {
public:
virtual A* f() {return new A;}
};
// 派生类Student
class Student : public Person {
public:
virtual B* f() {return new B;}
};
如果 基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写。
(当编译器编译类的时候,它会自动为析构函数生成一个唯一的名称,以便在程序中正确地调用析构函数,所以重写条件成立)
代码举例:
class Base {
public:
virtual ~Base() {
cout << "Base的析构函数" << endl;
}
};
class Derived : public Base {
public:
~Derived() {
cout << "Derived的析构函数" << endl;
}
};
int main() {
Base* ptr = new Derived();
delete ptr;
return 0;
}
输出结果:
Derived的析构函数
Base的析构函数
override 关键字
介绍虚函数重写时我们提到:
一般函数重写时我们加上override关键字,其作用在于:
代码举例:
class Base {
public:
virtual void foo() {
std::cout << "Base 的 foo 函数" << std::endl;
}
};
class Derived : public Base {
public:
void foo() override {
std::cout << "Derived 的 foo 函数" << std::endl;
}
};
final 关键字
final关键字用于在C++中声明不可继承的类、禁止重写的虚函数或禁止Lambda函数进行捕获。这里我们介绍其在虚函数中的用法:
在虚函数的声明中使用final关键字可以 阻止派生类对该虚函数进行重写
代码举例:
class Base {
public:
virtual void foo() final {
// 函数定义
}
};
class Derived : public Base {
public:
// 下面的代码会引发编译错误,因为Derived试图重写被标记为final的foo函数
void foo() override {
// 函数定义
}
};

对于重写,派生类可以不必使用 virtual 关键字。
因为在派生类中重写一个虚函数时,它会自动成为虚函数,无需再次使用 virtual 关键字进行声明。
在虚函数的后面写上 =0 ,这个函数称为 纯虚函数。
包含纯虚函数的类叫做抽象类(也叫接口类)。
// 抽象类
class AbstractClass {
public:
// 纯虚函数,没有实现
virtual void pureVirtualFunc() = 0;
// 普通成员函数,有实现
void commonFunc() {
std::cout << "这是一个普通的成员函数" << std::endl;
}
};
// 派生类
class ConcreteClass : public AbstractClass {
public:
void pureVirtualFunc() override {
std::cout << "派生类实现了纯虚函数" << std::endl;
}
};
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
虚函数表 是C++中实现多态性的一种机制。它是一张用于存储虚函数地址的表格,每个包含虚函数的类都会有一个对应的虚函数表。
虚函数表是一个静态的数据结构,在程序运行时被创建,并且与类的对象无关。
虚函数表中保存了虚函数的地址,它是一个由指针组成的数组,每个指针都指向对应虚函数的实际代码。子类继承了父类的虚函数表,并可以通过修改虚函数表中的指针来实现对虚函数的覆盖或扩展。这样,在通过基类指针或引用调用虚函数时,实际执行的是根据对象类型确定的子类中的虚函数。
我们看下面的代码,试问sizeof(Base)是多少
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
在上面的例子中:Base 是一个基类,它包含一个虚函数 Func1() 和一个私有成员变量 _b。而虚函数和非虚函数对于类的大小没有影响。
C++中,sizeof(Base) 的结果是编译器在编译时计算出来的类型的大小(以字节为单位),故为4。
为什么虚函数和非虚函数对于类的大小没有影响?
因为 C++ 实现了一种叫做虚表(vtable)的机制,用来支持虚函数的动态派发。

/// 在之前代码的基础上,加上虚函数Func2(),非虚函数Func3()
/// 只有虚函数Func1()被重写
// 基类
class Base
{
public:
virtual void Func1() // 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;
}
运行代码,观察下面的监视窗口:

得出以下结论:
Func1完成了重写,所以d的虚表中存的是重写的 Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。Func2继承下来后是虚函数,则放进虚表,Func3由于不是虚函数,被继承下来但不会放进虚表。关于虚表生成:
派生类的虚表生成:
虚函数,虚表的存放位置:
C++中,当一个类声明了虚函数时,编译器会在该类的对象中添加一个指向虚函数表的指针,通常被称为虚函数指针(vptr)。虚函数表是一个静态的数据结构,包含了所有虚函数的地址。子类继承了父类的虚函数表,并可以通过修改虚函数表中的指针来实现对虚函数的覆盖或扩展。这样,在通过基类指针或引用调用虚函数时,实际执行的是根据对象类型确定的子类中的虚函数。
由于C++中的引用和指针都支持动态绑定,因此可以通过基类引用或指针来实现多态。当程序通过基类指针或引用调用虚函数时,实际执行的是根据动态类型确定的子类中的虚函数。这意味着同样的代码对不同类型的对象的行为也是不同的。
上文提到了 绑定的概念:C++中,绑定(Binding)指的是将函数调用与函数实现关联起来的过程。
静态绑定(Static Binding)和动态绑定(Dynamic Binding)是两种不同的绑定方式。
静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载。
// 基类
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; } // 被派生类 重写
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a;
};
// 派生类
class Derive :public Base {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
virtual void func4() { cout << "Derive::func4" << endl; }
private:
int b;
};
对于上面的代码,当我们进行调试,会发现监视窗口中看不到,因为监视窗口隐藏了这两个函数,我们利用下面的代码打印出虚表函数来查看信息。

typedef void(*VFPTR) (); // 声明函数指针
void PrintVTable(VFPTR vTable[])
{
// 依次取虚表里的虚函数指针(vfptr)地址,并打印
// 通过调用可以看出存的函数
cout << "虚表地址-> " << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf("第%d个虚函数地址: 0x%x,-> ", i, vTable[i]);
VFPTR fun = vTable[i];
fun(); // 执行虚函数
}
cout << endl;
}
int main()
{
Base b;
Derive d;
// 使用指针类型转换和取地址运算符& 获取 b、d 对象的虚函数表指针
VFPTR* vTableb = (VFPTR*)(*(int*)&b);
PrintVTable(vTableb); // 打印虚函数表存储的虚函数指针地址
VFPTR* vTabled = (VFPTR*)(*(int*)&d);
PrintVTable(vTabled);
return 0;
}
执行上面代码后,有以下结果:

下面的代码,演示了多继承的虚函数表结构:
#include
using namespace std;
// 基类A
class A {
public:
virtual void func1() { cout << "A::func1" << endl; }
virtual void func2() { cout << "A::func2" << endl; }
};
// 基类B
class B {
public:
virtual void func3() { cout << "B::func3" << endl; }
virtual void func4() { cout << "B::func4" << endl; }
};
// 派生类C,多继承自A和B
class C : public A, public B {
public:
virtual void func5() { cout << "C::func5" << endl; }
virtual void func6() { cout << "C::func6" << endl; }
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
cout << "虚表地址-> " << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
cout << "第" << i << "个虚函数地址: " << vTable[i] << ",-> ";
VFPTR fun = vTable[i];
fun(); // 执行虚函数
}
cout << endl;
}
int main()
{
C c;
// 使用指针类型转换和取地址运算符& 获取 c 对象的虚函数表指针
VFPTR* vTableC = (VFPTR*)(*(int*)&c);
PrintVTable(vTableC);
return 0;
}