C++面向对象:多态
多态,字面意思就是“多种形态”,也就是对同一个行为具有多个不同表现形式。
C++中,多态主要指的是通过一个基类的指针或引用来调用派生类的虚函数时,程序能够在运行时根据实际指向的对象类型来决定调用哪个函数版本。
class Base {
public:
    virtual void foo() {
       std::cout << "Base Called." << std::endl;
   }
};
class Derived : public Base
{
public:
    virtual void foo() { 
       std::cout << "Derived Called." << std::endl;
   }
};
void bar(Base *p)
{
    p->foo();
}
int main()
{
    Base b;
    Derived d;
    bar(&b); // Base Called.
    bar(&d); // Derived Called.
}
从上面可以看出:bar()接口根据传入的不同对象,调用不同的bar版本。
C++实现多态的关键:
- 必须有继承关系。
- 基类重写了派生类的虚函数。
- 必须通过基类的指针或引用调用派生类的虚函数。
动态绑定和静态绑定
静态绑定发生在编译时,编译器在编译阶段就确定了要调用的具体函数及地址,运行时无需额外的查找过程,直接跳转到函数地址执行。
动态绑定发生在运行时,程序在运行期间,根据对象的实际类型来决定调用哪个函数。动态绑定只适用于虚函数,依赖于虚函数表和虚函数表指针机制。
虚函数工作原理
当一个类包含虚函数时,编译器会为该类生成一个虚函数表。
- 虚函数表:一个指针数组,其中每个指针都指向该类的虚函数的实现。基类和派生类都有自己的vtable。
- 虚函数表指针:当创建一个包含虚函数的类的对象时,该对象会在其内存中多出一个vptr,它指向该类的vtable。
当通过基类指针或引用调用一个虚函数时,C++的运行时系统会执行以下步骤:
- 查找vptr:通过指针或引用找到对象的vptr。
- 访问vtable:通过vptr找到正确的vtable。
- 调用函数:在vtable中找到对应虚函数的地址,并跳转到该地址执行代码。
含有虚函数的类的内存分布
当一个类中包含虚函数时,编译器会:
- 为该类创建一个虚函数表。这个表是编译器在编译期为每个含有虚函数的类创建的,它实际上是一个静态数组,通常存储在只读数据段(.rodata)或代码段(.text)中。
- 在类的实例中添加一个虚函数表指针。这个指针是对象中的第一个成员,它指向该类对应的虚函数表。
虚函数表的内存布局

上图中:
- 紫色线框中的内容仅限于虚拟继承和多重继承场景,单继承场景不存在这两个变量。
- virtual call offsets:当派生类继承了多个基类,派生类对象可能会有多个虚函数表。- virtual call offsets记录了为了正确调用某个虚函数,需要对this指针进行调整的偏移量。
- virtual base offsets专门用于解决虚继承中的问题,在虚继承中,虚基类的子对象位置是不固定的。- virtual call offsets记录了从当前虚函数表的起始地址到虚基类子对象的偏移量。
- offset to top记录了从当前虚函数表的地址到整个对象内存块的起始地址的偏移量,单继承场景为0。
- RTTI information是一个对象指针,指向- std::type_info对象,包含了类的名称、大小、对齐方式等元数据。
- virtual function pointers是虚函数指针数组,其中存放着类的所有虚函数指针。
- 类中的vptr指向的不是虚函数表首地址,而是指向虚函数表中的第一个虚函数起始地址。
纯虚函数
纯虚函数是一种特殊虚函数,它没有函数体。
纯虚函数强制派生类实现该函数。
一个含有纯虚函数的类叫做抽象类,抽象类不能被实例化。
如果派生类不实现纯虚函数,那么这个派生类自身也会成为一个抽象类。
class Foo {
public:
    virtual void bar() = 0;
};
构造函数不能是virtual
在构造一个派生类对象时,vptr会经历一个“先指向基类虚表,后指向派生类虚表”的过程。
当创建一个Derived类的对象时:
- 调用基类构造函数,编译器会把对象的vptr设置为指向Base类的虚函数表, 如果Base构造函数中调用一个虚函数,它会通过Base类的虚表来查找并调用Base版本的函数。
- 基类构造函数完成后,编译器会把对象的vptr重新设置为指向Derived类的虚函数表。如果Derived构造函数中调用虚函数,它就会通过Derived类的虚表来调用Derived版本的函数。
构造函数的职责是创建对象。当一个对象被创建时,它需要知道自己的确切类型和内存大小,以便正确地分配内存并初始化成员。
虚函数的运行是建立在对象的基础上,在构造函数执行时,对象尚未形成,所以不能将构造函数定义为虚函数。
简单来说,构造函数是用来建立多态的,而不是使用多态的。
析构函数最好是virtual
如果基类的析构函数不是虚函数,那么在通过基类指针删除派生类对象时,可能会发生内存泄漏或其他未定义的行为。
class Base {
public:
    ~Base() {}  // 只会清理Base的资源
};
class Derived : public Base {
private:
    int* data;
public:
    Derived() {
        data = new int[10];
    }
    ~Derived() {
        delete[] data; // 清理 Derived 的资源
    }
};
int main() {
    Base* ptr = new Derived();
    delete ptr;
}
- 如果Base的析构函数不是虚函数。当delete ptr被调用时,程序会静态绑定到Base的析构函数。Derived的析构函数将永远不会被调用。
- 如果将~Base()声明为virtual,delete ptr就会触发动态绑定,首先调用Derived的析构函数,然后调用Base的析构函数。