多态(Polymorphism)是面向对象编程(OOP)的三大核心特性之一(另外两个是封装和继承),指的是同一接口(函数、方法)在不同对象上表现出不同行为的能力。简单来说,就是 “一个接口,多种实现”。

什么是虚(virtual)函数?

看下面的例子,注意在Base中加了virtual和没有加virtual的区别

#include <iostream>
using std::cout;
using std::endl;
class Base{
public:
    //成员函数上面标注了一个virtual那么便是虚函数
    //virtual
    void print(){
        cout << "Base::print" << endl;
    }

    //virtual
    void display(){
            cout << "Base::display()" << endl;
    }

private:
    long _base = 10;
};
class Derived : public Base{
public:
    void print(){
        cout << "Derived::print" << endl;
    }

private:
    long _derived = 20;
};

void test(){
    Derived d;  
    Base * pbase = &d;//使用基类指针去指向派生类对象
    pbase->print();//pbase里面存储的便是派生类对象的地址
    pbase->display();//Base *指针决定了该指针只可以sizeof(Base)个字节
    cout << sizeof(Base) << endl;   
    cout << sizeof(Derived) << endl;	
}

int main()
{
    test();
    return 0;
}

output : 

Base::print
Base::display()
8
16

没加virtual时, Base * pbase = & derived,此时发生向上转型。通过pbase->display()方法调用时,

转换成display(pdbase),通过此时的类名Base以及参数,去代码区查找对应的函数,最终找到Base::display然后调用, print()同理

加virtual之后

class Base{
public:
    //成员函数上面标注了一个virtual那么便是虚函数
    virtual
    void print(){
        cout << "Base::print" << endl;
    }

    virtual
    void display(){
            cout << "Base::display()" << endl;
    }

private:
    long _base = 10;
};

 output : 

Derived::print
Derived::print
16
24

当Base的display函数加上了virtual关键字,变成了一个虚函数,Base对象的存储布局就改变了。在存储的开始位置会多加一个虚函数指针,该虚函数指针指向一张虚函数表(简称虚表),其中存放的是虚函数的入口地址

Derived继承了Base类,那么创建一个Derived对象,依然会创建出一个Base类的基类子对象

在Derived类中又定义了print函数,发生了覆盖的机制(override),覆盖的是虚函数表中虚函数的入口地址

总结:

  • 不加virtual关键字时,当Base * pbase = & derived时,此时发生向上转型。通过pbase->display()方法调用时,因为没有virtual关键字,属于静态绑定。编译器根据指针类型推断出应该调用Base类的display()方法。

  • 加virtual关键字时,当Base * pbase = & derived时,此时同样发生向上转型。通过pbase->display()方法调用时,由于有virtual关键字,属于动态绑定。编译器会通过vfptr查找到虚表,根据虚表确定正确的函数地址。virtual就相当于告诉编译器,不要去代码区遍历找成员函数,必须通过遍历虚表的方式找成员函数

在虚函数机制中virtual关键字的含义

1、虚函数是存在的;(存在)

2、通过间接的方式去访问;(间接)

3、通过基类的指针访问到派生类的函数,基类的指针共享了派生类的方法(共享)

虚函数的覆盖(override)

如果一个基类的成员函数定义为虚函数,那么它在所有派生类中也保持为虚函数,即使在派生类中省略了virtual关键字,也仍然是虚函数。虚函数一般用于灵活拓展,所以需要派生类中对此虚函数进行覆盖。覆盖的格式有一定的要求:

  • 与基类的虚函数有相同的函数名;

  • 与基类的虚函数有相同的参数个数;

  • 与基类的虚函数有相同的参数类型;

  • 与基类的虚函数有相同的返回类型。

我们在派生类中对虚函数进行覆盖时,很有可能写错函数的形式(函数名、返回类型、参数个数),等到要使用时才发现没有完成覆盖。这种错误很难发现,所以C++提供了关键字override来解决这一问题。

关键字override的作用:

在虚函数的函数参数列表之后,函数体的大括号之前,加上override关键字,告诉编译器此处定义的函数是要对基类的虚函数进行覆盖。

class Base{
public:
    virtual void display() const{
        cout << "Base::display()" << endl;
    }
private:
    long _base;
};


class Derived
: public Base
{
public:
    //想要在派生类中定义虚函数覆盖基类的虚函数
    //很容易打错函数名字,同时又不会报错
    //没有完成有效的覆盖
    /* void dispaly() const{   //不会报错     */
    /* void dispaly() const override   //编译器会报错   */
    void display() const override
    {
        cout << "Derived::display()" << endl;
    }
private:
    long _derived;

};

覆盖 总结:

(1)覆盖是在虚函数之间的概念,需要派生类中定义的虚函数与基类中定义的虚函数的形式完全相同

(2)当基类中定义了虚函数时,派生类去进行覆盖,即使在派生类的同名的成员函数前不加virtual,依然是虚函数;

(3)发生在基类派生类之间,基类与派生类中同时定义形式相同的虚函数。覆盖的是虚函数表中的入口地址,并不是覆盖函数本身。

 多态的核心思想

通过基类定义统一的接口,派生类根据自身特性重写(override)接口的实现。当使用基类指针或引用指向派生类对象时,调用接口会自动执行派生类的实现,而无需关心对象的具体类型。

多态的实现条件

  1. 继承关系:存在基类和派生类的继承结构。
  2. 虚函数:基类中声明虚函数(用virtual关键字)。
  3. 重写:派生类中重写基类的虚函数(函数名、参数列表、返回值完全一致,推荐用override关键字显式标识)。
  4. 动态绑定:通过基类的指针或引用调用虚函数,程序在运行时根据对象实际类型选择对应函数版本。
#include <iostream>
using std::cout;
using std::endl;
//多态、虚函数、虚函数指针、虚函数覆盖四者有什么关系呢???
//虚函数:一个成员函数标注了virtual那么便是虚函数
//虚函数指针:成员函数标注了virtual之后的对象会存在一个虚函数指针
//虚函数覆盖:覆盖的是虚函数表里面的虚函数的入口地址
//多态:使用基类指针去指向派生类对象,并且使用基类指针调用虚函数
//便会触发多态
class Base{
public:
    //成员函数上面标注了一个virtual那么便是虚函数
    virtual
    void print(){
        cout << "Base::print" << endl;
    }

private:
    long _base = 10;
};
class Derived : public Base{
public:
    void print(){
        cout << "Derived::print" << endl;
    }
    //virtual

private:
    long _derived = 20;
};
class Derived2 : public Base{
public:
    void print(){
        cout << "Derived2::print" << endl;
    }
    //virtual

private:
    long _derived = 20;
};

void test2(){
    //第一次:pbase指向Derived1类对象
    //第二次:pbase指向Derived2类对象
    Derived d;
    Derived2 d2;
    Base * pbase;
    //只需要将指针去指向不同的派生类对象
    //这个就是多态
    pbase = &d2;
    pbase->print();
}


int main()
{
    test2();
    return 0;
}

多态的实现原理

多态的底层依赖虚函数表(vtable) 和虚函数指针(vptr)

  1. 虚函数表(vtable):每个包含虚函数的类会有一个全局的 vtable,存储该类所有虚函数的地址。派生类会继承基类的 vtable,并替换重写的虚函数地址。
  2. 虚函数指针(vptr):每个对象会包含一个 vptr,指向所属类的 vtable。
  3. 动态绑定:当通过基类指针 / 引用调用虚函数时,程序通过 vptr 找到对应 vtable,再根据函数索引找到实际要执行的函数地址(运行时确定)。

多态的优势

  1. 代码复用与扩展:新增派生类时,无需修改使用基类接口的代码(如printInfo函数),只需重写虚函数即可扩展功能(符合 “开闭原则”)。
  2. 简化逻辑:调用者只需关注基类接口,无需关心具体实现,降低代码复杂度。
  3. 灵活性:同一操作能适配不同对象,使程序更灵活、易维护。

总结

多态是面向对象编程的核心机制,它通过虚函数实现了 “接口与实现分离”,让代码具备更好的扩展性和复用性。在框架设计、插件系统、通用算法等场景中,多态是不可或缺的技术。

面试常考题

1、虚表存放在哪里?

在 C++ 中,虚函数表(vtable)的存储位置属于编译器实现细节,C++ 标准并未严格规定,但通常存放在进程的只读数据段(.rodata section) 或全局数据段(.data section) 中,具体取决于编译器和操作系统。

2、一个类中虚函数表有几张?

虚函数表(虚表)可以理解为是一个数组,存放的是一个个虚函数的地址

如果没有虚函数,那么此时不会有虚函数指针,也不会有虚表

单继承: 派生类继承了一个基类,该基类中定义了多个虚函数,那么此时会有一张虚函数表(基类有一张,派生类有一张)

多继承: 派生类继承了多个基类,多个基类中各自定义了各自的虚函数,那么此时便会有多张虚函数表

#include <iostream>
using namespace std;

// 基类1:含虚函数
class Base1 {
public:
    virtual void func1() { cout << "Base1::func1()" << endl; }
    virtual void funcA() { cout << "Base1::funcA()" << endl; }
};

// 基类2:含虚函数
class Base2 {
public:
    virtual void func2() { cout << "Base2::func2()" << endl; }
    virtual void funcB() { cout << "Base2::funcB()" << endl; }
};

// 派生类:多继承Base1和Base2,并重写所有虚函数
class Derived : public Base1, public Base2 {
public:
    // 重写Base1的虚函数
    void func1() override { cout << "Derived::func1()" << endl; }
    
    // 重写Base2的虚函数
    void func2() override { cout << "Derived::func2()" << endl; }
    
    // 新增的虚函数
    virtual void func3() { cout << "Derived::func3()" << endl; }
};

 虚表数量分析

  • Base1 有 1 份虚表,包含 func1()funcA() 的地址。
  • Base2 有 1 份虚表,包含 func2()funcB() 的地址。
  • Derived 会生成 2 份虚表
    • 第 1 份虚表(对应Base1):
      包含重写的 func1()、继承的 funcA()新增的 func3() 的地址。若派生类新增了虚函数,其地址会被追加到虚表末尾;
    • 第 2 份虚表(对应Base2):
      包含重写的 func2()、继承的 funcB() 的地址。

       

3、虚函数机制的底层实现是怎样的?

4.三个概念的区分:重载(overload) ,隐藏 (oversee),覆盖(override)

#include <iostream>
using namespace std;

class Base {
public:
    virtual void func() {  // 基类虚函数(可被重写)
        cout << "Base::func()" << endl;
    }
    void func(int x) {  // 基类非虚函数(可被隐藏)
        cout << "Base::func(int) x=" << x << endl;
    }
};

class Derived : public Base {
public:
    // 重写:与基类func()同名、同参数,覆盖基类虚函数
    void func() override {
        cout << "Derived::func()" << endl;
    }
    // 隐藏:与基类func(int)同名但参数不同(或参数相同但未重写)
    void func(double x) {
        cout << "Derived::func(double) x=" << x << endl;
    }
};

int main() {
    Derived d;
    Base* bptr = &d;

    // 重写生效:多态调用派生类版本
    bptr->func();  // 输出 "Derived::func()"

    // 派生类中直接调用:优先使用派生类的func()和func(double)
    d.func();       // 输出 "Derived::func()"(重写的版本)
    d.func(3.14);   // 输出 "Derived::func(double) x=3.14"(隐藏基类func(int))

    // 基类的func(int)被隐藏,需显式调用
    d.Base::func(10);  // 输出 "Base::func(int) x=10"

    return 0;
}

在 C++ 中,隐藏(name hiding)和重写(override)是两种不同的机制,它们作用于不同的函数,因此可以在同一个派生类中同时存在,但不能对同一个基类虚函数既隐藏又重写。

5.代码题

  • 可以共存:派生类中可以同时存在 “重写基类虚函数” 和 “隐藏基类其他同名函数” 的情况(如示例中func()被重写,func(int)func(double)隐藏)。
  • 不可对同一函数既重写又隐藏:对于基类的某个特定虚函数(如func()),要么被派生类重写(参数列表相同),要么被派生类的同名不同参数函数隐藏,二者互斥。
#include <iostream>

class Base {
public:
    virtual void show(int x = 10) {
        std::cout << "Base::show(), x = " << x << std::endl;
    }
    virtual ~Base() {}
};

class Derived : public Base {
public:
    void show(int x = 20) override {
        std::cout << "Derived::show(), x = " << x << std::endl;
    }
};

int main() {
    Base* ptr = new Derived();
    ptr->show(); // 输出什么?
    delete ptr;
    return 0;
}

许多开发者会凭直觉认为,既然ptr指向Derived对象,那么调用的应该是Derived::show(),使用的默认参数也应该是Derived版本中定义的20

然而,程序的实际输出是:

Derived::show(), x = 10

发生了什么?

  • 函数体(Derived::show())是动态绑定的ptr虽然是Base类型的指针,但它实际指向一个Derived对象。由于show()是虚函数,运行时通过虚函数表机制,正确地调用了Derived类中的show函数实现。这符合我们对多态的预期。

  • 默认参数(x = 10)是静态绑定的:编译器在编译ptr->show()这行代码时,只知道ptr的静态类型是Base*。因此,它会查找Base::show()的声明,并将它的默认参数10嵌入到调用指令中。

最终结果就是,我们调用了派生类的函数,却传入了基类的默认参数。这种行为上的不一致性,极易导致难以发现的逻辑错误,是C++中一个典型的“合法但危险”的陷阱。

动态多态(虚函数机制)被激活的条件(重点*)

虚函数机制是如何被激活的呢,或者说动态多态是怎么表现出来的呢?其实激活条件还是比较严格的,需要满足以下全部要求:

  1. 基类定义虚函数

  2. 派生类中要覆盖虚函数 (覆盖的是虚函数表中的地址信息)

  3. 创建派生类对象

  4. 基类的指针指向派生类对象(或基类引用绑定派生类对象)

  5. 通过基类指针(引用)调用虚函数

最终的效果:基类指针调用到了派生类实现的虚函数。(如果没有虚函数机制,基类指针只能调用到基类的成员函数)

虚函数的限制

虚函数机制给C++提供了灵活的用法,但仍然受到了一些约束,以下几种函数不能设为虚函数:

1.构造函数不能设为虚函数

构造函数的作用是创建对象时完成数据的初始化,而虚函数机制被激活的条件之一就是要先创建对象,有了对象才能表现出动态多态。如果将构造函数设为虚函数,那此时构造未执行完,对象还没完成初始化,存在矛盾。

如果构造函数是虚函数,那么调用它就需要通过vptr来查找vtable中的函数地址。但此时,vptr还尚未被初始化!这就产生了一个“先有鸡还是先有蛋”的悖论:为了调用虚构造函数,需要vptr;而为了初始化vptr,需要先进入构造函数。这在逻辑上是行不通的。

2.静态成员函数不能设为虚函数

虚函数的实际调用:  this -> vfptr -> vtable -> virtual function,但是静态成员函数没有this指针,所以无法访问到vfptr

vfptr是属于一个特定对象的部分,虚函数机制起作用必然需要通过vfptr去间接调用虚函数。静态成员函数找不到这样特定的对象。

3.Inline函数不能设为虚函数

因为inline函数在编译期间完成替换,而在编译期间无法展现动态多态机制,所以起作用的时机是冲突的。如果同时存在,inline失效。

4.普通函数不能设为虚函数

虚函数要解决的是对象多态的问题,与普通函数无关,普通函数式非成员函数,不能设置为虚函数。

不同访问方式对虚函数的影响

虚函数的调用结果取决于 “访问方式”(对象直接调用 / 基类指针 / 引用调用),核心区分 “静态绑定” 和 “动态绑定”:

1. 通过派生类对象直接调用

  • 非虚函数:静态绑定,调用派生类版本(若存在,隐藏基类版本)。
  • 虚函数:直接调用派生类重写版本(无需指针 / 引用,因对象类型明确)。
class Base {
public:
    virtual void vfunc() { cout << "Base::vfunc" << endl; }
    void func() { cout << "Base::func" << endl; }
};

class Derived : public Base {
public:
    void vfunc() override { cout << "Derived::vfunc" << endl; }
    void func() { cout << "Derived::func" << endl; }
};

int main() {
    Derived d;
    d.vfunc(); // 调用Derived::vfunc(虚函数,对象类型明确)
    d.func();  // 调用Derived::func(非虚函数,隐藏基类版本)
    return 0;
}

2. 通过基类指针 / 引用指向派生类对象调用

  • 虚函数:动态绑定,调用派生类重写版本(多态激活)。
  • 非虚函数:静态绑定,调用基类版本(按指针类型决定)。
int main() {
    Derived d;
    Base* p = &d;
    
    p->vfunc(); // 动态绑定:调用Derived::vfunc(多态)
    p->func();  // 静态绑定:调用Base::func(按指针类型)
    return 0;
}

3. 通过派生类指针调用基类虚函数

需显式指定基类作用域,强制调用基类版本(绕过多态):

int main() {
    Derived d;
    Derived* p = &d;
    
    p->vfunc(); // 调用Derived::vfunc(默认)
    p->Base::vfunc(); // 显式调用Base::vfunc(绕过多态)
    return 0;
}

在构造函数和析构函数中访问虚函数

在 C++ 中,在构造函数和析构函数中调用虚函数时,不会触发多态行为,而是直接调用当前类(正在构造 / 析构的类)的虚函数版本。这是由对象的生命周期和虚函数表(vtable)的初始化 / 销毁顺序决定的,需要特别注意避免误用。

一、构造函数中调用虚函数

1. 行为特点

  • 不会触发多态:构造派生类对象时,会先调用基类构造函数,再调用派生类构造函数。
  • 调用当前正在构造的类的版本:在基类构造函数中调用虚函数,只会调用基类自己的虚函数版本(即使派生类重写了该函数)。

2. 原因分析

  • 对象构造时,vptr(虚函数指针)的初始化顺序与构造函数一致:
    • 先初始化基类部分,vptr 指向基类的 vtable;
    • 再初始化派生类部分,vptr 才会切换到派生类的 vtable。
  • 因此,基类构造函数执行期间,对象还未完全成为派生类对象,vptr 尚未指向派生类的 vtable,无法调用派生类的重写版本。
#include <iostream>
using namespace std;

class Base {
public:
    Base() {
        cout << "Base::Base()" << endl;
        func(); // 构造函数中调用虚函数
    }

    virtual void func() {
        cout << "Base::func()" << endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        cout << "Derived::Derived()" << endl;
        func(); // 构造函数中调用虚函数
    }

    void func() override {
        cout << "Derived::func()" << endl;
    }
};

int main() {
    Derived d; 
    // 输出:
    // Base::Base()
    // Base::func()  // 基类构造中调用基类版本
    // Derived::Derived()
    // Derived::func()  // 派生类构造中调用派生类版本
    return 0;
}

析构函数中调用虚函数同理

析构函数设为虚函数(重点)

class Base
{
public:
    Base()
    : _base(new int(10))
    { cout << "Base()" << endl; }

    virtual void display() const{
        cout << "*_base:" << *_base << endl;
    }

    ~Base(){
        if(_base){
            delete _base;
            _base = nullptr;
        }
        cout << "~Base()" << endl;
    }

private:
    int * _base;
};

class Derived
: public Base
{
public:
    Derived()
    : Base()
    , _derived(new int(20))
    {
        cout << "Derived()" << endl;
    }

    virtual void display() const override{
        cout << "*_derived:" << *_derived << endl;
    }

    ~Derived(){
        if(_derived){
            delete _derived;
            _derived = nullptr;
        }
        cout << "~Derived()" << endl;
    }

private:
    int * _derived;
};

void test0(){
    Base * pbase = new Derived();
    pbase->display();

    delete pbase;
    //编译器会进行类型检查,pbase指向的空间是一个Derived对象
  	//所以会调用Derived的析构函数 —— 需要让析构函数设为虚函数,Derived析构函数会在虚表中覆盖Base析构函数的地址
    //这样通过pbase才能调用到Derived析构函数
    //Derived析构函数执行完,会自动调用Base的析构函数(没有走虚表这个途径) —— 析构函数本身的机制
}

output

Base()
Derived()
*_derived:20
~Base()  // 只调用了基类析构函数,派生类的_derived内存泄漏

问题原因

  • 非虚析构函数的调用是静态绑定(编译期根据指针类型Base*决定调用Base的析构函数)。
  • 派生类Derived_derived指针未被delete,导致内存泄漏。

解决方案:将基类析构函数声明为虚函数

只需修改Base的析构函数,添加virtual关键字:

class Base {
public:
    // ... 其他成员 ...

    // 基类析构函数声明为虚函数
    virtual ~Base() {  // 关键修改:添加virtual
        if(_base){
            delete _base;
            _base = nullptr;
        }
        cout << "~Base()" << endl;
    }
};

执行结果(正确情况)

Base()
Derived()
*_derived:20
~Derived()  // 先调用派生类析构函数,释放_derived
~Base()    // 再调用基类析构函数,释放_base

修复原理

  1. 基类析构函数为虚函数时,派生类析构函数会自动成为虚函数(即使不写virtual),并覆盖基类虚表中的析构函数地址。
  2. delete pbase时,通过基类指针的vptr找到派生类的虚表,调用Derived的析构函数(动态绑定)。
  3. 派生类析构函数执行完毕后,会自动调用基类析构函数(析构函数的链式调用机制),确保所有资源释放。

当编译器处理派生类的析构函数时,会自动在其函数体末尾插入基类析构函数的调用代码

例如,对于:

~Derived() { /* 派生类析构逻辑 */ }

编译器会将其隐式扩展为:

~Derived() {
    /* 派生类析构逻辑 */
    Base::~Base(); // 编译器自动添加基类析构调用
}

这种自动插入机制保证了:只要调用了派生类析构函数,就一定会触发基类析构函数的调用,形成链式关系。

为什么析构函数需要设为虚函数?

  • 防止资源泄漏:当派生类有动态分配的资源(如_derived指针)时,必须通过派生类析构函数释放。
  • 多态场景的必然要求:若基类可能被继承,且存在 “基类指针指向派生类对象” 的场景(如多态),析构函数必须设为虚函数,否则delete时无法正确调用派生类析构函数。

总结

  • 规则只要类可能被继承,且可能通过基类指针删除派生类对象,就必须将基类析构函数声明为虚函数
  • 效果:确保delete基类指针时,先调用派生类析构函数,再调用基类析构函数,避免资源泄漏。
  • 注意:派生类析构函数无需显式写virtual(但建议写override明确意图),会自动继承虚属性。虚析构函数在继承时,一定会被重写。

= default

= default 是 C++11 引入的语法,用于显式要求编译器为类的特殊成员函数(如构造函数、析构函数、拷贝构造、移动构造等)生成默认实现。当它与 virtual 结合时,表示:

  • 该函数是一个虚函数(支持多态,派生类可重写);
  • 编译器会为该函数生成默认实现(而非手动编写)。

虚析构函数。为了确保派生类对象通过基类指针销毁时能正确调用派生类的析构函数,基类析构函数通常需要声明为 virtual。此时若无需自定义析构逻辑,可使用 = default 让编译器生成默认实现:

class Base {
public:
    // 虚析构函数,由编译器生成默认实现
    virtual ~Base() = default; 
};

class Derived : public Base {
private:
    int* data;
public:
    Derived() : data(new int) {}
    // 派生类自定义析构函数(释放资源)
    ~Derived() override { 
        delete data; 
    }
};

这里 Base::~Base() = default; 等价于编译器自动生成的默认析构函数(空实现),但显式声明为 virtual 以确保多态销毁的正确性。

纯虚函数

纯虚函数(Pure Virtual function)是 C++ 中一种特殊的虚函数,它没有具体实现,仅作为接口声明,强制派生类必须重写(override)该函数才能实例化对象。纯虚函数是实现 “接口类” 和 “抽象类” 的核心机制,用于定义统一接口但不提供默认实现。

一、纯虚函数的定义形式

在虚函数声明的末尾加上 = 0,即可将其定义为纯虚函数:

class 类名 {
public:
    virtual 返回值类型 函数名(参数列表) = 0;  // 纯虚函数
};
  • = 0 表示该函数没有默认实现,是 “纯虚” 的。
  • 纯虚函数可以有声明,但不能在类内提供实现(若要提供实现,必须在类外)。

二、包含纯虚函数的类:抽象类

  • 抽象类(Abstract Class):包含纯虚函数的类称为抽象类。
  • 特性
    1. 抽象类不能实例化对象(无法创建 类名 对象名 这样的实例)。
    2. 抽象类的派生类必须重写所有纯虚函数,否则派生类仍为抽象类(也不能实例化)。
    3. 抽象类可以包含普通成员变量和非纯虚函数(有实现的虚函数或普通函数)。

三、纯虚函数的示例

#include <iostream>
using namespace std;

// 抽象类:包含纯虚函数
class Shape {  // 形状(抽象概念,无法实例化)
public:
    // 纯虚函数:声明接口,无实现
    virtual double area() const = 0;  // 计算面积
    virtual void draw() const = 0;    // 绘制图形

    // 普通成员函数(可以有实现)
    void printInfo() const {
        cout << "面积:" << area() << endl;  // 调用纯虚函数(依赖派生类实现)
    }
};

// 派生类1:重写所有纯虚函数(可实例化)
class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}

    // 重写纯虚函数
    double area() const override {
        return 3.14 * radius * radius;
    }

    void draw() const override {
        cout << "绘制圆形(半径:" << radius << ")" << endl;
    }
};

// 派生类2:重写所有纯虚函数(可实例化)
class Rectangle : public Shape {
private:
    double width, height;
public:
    Rectangle(double w, double h) : width(w), height(h) {}

    double area() const override {
        return width * height;
    }

    void draw() const override {
        cout << "绘制矩形(宽:" << width << ",高:" << height << ")" << endl;
    }
};

int main() {
    // Shape s;  // 错误:抽象类不能实例化对象

    // 通过抽象类指针实现多态
    Shape* shape1 = new Circle(5.0);
    Shape* shape2 = new Rectangle(3.0, 4.0);

    shape1->draw();    // 多态调用:绘制圆形
    shape1->printInfo();  // 调用普通函数,内部依赖纯虚函数area()

    shape2->draw();    // 多态调用:绘制矩形
    shape2->printInfo();

    delete shape1;
    delete shape2;
    return 0;
}

输出结果

绘制圆形(半径:5)
面积:78.5
绘制矩形(宽:3,高:4)
面积:12

四、纯虚函数的特殊用法

  1. 纯虚函数的类外实现
    纯虚函数可以在类外提供实现(但仍需在派生类中重写才能实例化),通常用于提供 “默认实现”,但强制派生类显式重写(如需使用默认实现,派生类可在重写时调用基类版本)。

    class Base {
    public:
        virtual void func() = 0;  // 纯虚函数声明
    };
    
    // 类外提供纯虚函数的实现
    void Base::func() {
        cout << "Base::func() 默认实现" << endl;
    }
    
    class Derived : public Base {
    public:
        void func() override {
            Base::func();  // 调用基类的默认实现
            cout << "Derived::func() 扩展实现" << endl;
        }
    };
    
  2. 抽象类作为接口
    若抽象类中所有成员函数都是纯虚函数,且没有成员变量,则该类可视为 “接口(Interface)”,仅定义行为规范(类似 Java 中的interface)。

    // 接口类:仅含纯虚函数
    class Printable {
    public:
        virtual void print() const = 0;  // 打印接口
        virtual ~Printable() = default;  // 接口类建议声明虚析构函数
    };
    

    五、纯虚函数的核心作用

  3. 定义接口规范:强制派生类实现特定功能(如Shapearea()draw()),确保所有派生类遵循统一接口。
  4. 实现抽象概念:对于无法实例化的抽象概念(如 “形状”“动物”),用抽象类表示,仅通过派生类(如 “圆形”“狗”)实例化具体对象。
  5. 支持多态:抽象类指针 / 引用可指向任何派生类对象,调用纯虚函数时触发多态,实现 “同一接口,不同实现”。

六、注意事项

  1. 抽象类不能实例化:试图创建抽象类的对象会导致编译错误。
  2. 派生类必须重写所有纯虚函数:否则派生类仍是抽象类,无法实例化。
  3. 抽象类可以作为基类:通过抽象类指针 / 引用实现多态是其主要用途。
  4. 接口类建议声明虚析构函数:若通过基类指针删除派生类对象,虚析构函数确保派生类析构函数被调用(避免资源泄漏)。

总结

纯虚函数是 C++ 中定义接口和抽象类的关键工具,它通过 “强制派生类实现” 和 “支持多态” 两大特性,确保代码的规范性和灵活性。在框架设计、插件系统等场景中,纯虚函数是实现 “开闭原则”(对扩展开放,对修改关闭)的核心机制。

验证虚表的存在(重点)

从前面的知识讲解,我们已经知道虚表的存在,但之前都是理论的说法,我们是否可以通过程序来验证呢?——当然可以

#include <iostream>
using std::cout;
using std::endl;
//验证虚表的存在
//虚表里面存储的是虚函数的入口地址
class Base{
public:
    virtual 
    void print(){
        cout << "Base::print" << endl;
    }
    virtual 
    void show(){
        cout << "Base::show()" << endl;
    }
    virtual 
    void display(){
        cout << "Base::display" << endl;
    }

private:
    long _base = 10;
};
class Derived : public Base{
public:
    virtual 
    void print() override{
        cout << "Derived::print" << endl;
    }
    virtual 
    void show() override{
        cout << "Derived::show()" << endl;
    }
    virtual 
    void display() override{
        cout << "Derived::display" << endl;
    }

private:
    long _derived = 20;
};


void test(){
    //将使用代码来验证虚表是真实存在的
    Base base;
    Derived derived;
    cout << sizeof(base) << endl;//16
    cout << sizeof(derived) << endl;//24

    //下面的操作过程可以以base为素材,也可以以derived为素材
    //案例中使用derived
    Derived * pd = &derived;
    //指针类型的强转
    long * pl = (long *)pd;
    cout << *pl << endl;//pl里面存储的就是虚表的地址
    cout << *(pl + 1) << endl;
    cout << *(pl + 2) << endl;

    //虚函数表里面存储的是虚函数的入口地址
    //其实也就是函数指针
    //arr[0] = *(arr + 0)
    //arr[1] = *(arr + 1);
    //vtable是首地址 vtable + 1  vtable +2便是指定偏移量的地址
    long * vtable = (long *)*pl;

    //下面这三个其实对应的就是Derived里面的三个虚函数的地址
    vtable;
    vtable + 1;
    vtable + 2;

    typedef void (*pFunc)();
    pFunc pf;
    //可以认为*vtable是一个函数,使用一个函数指针去指向该函数
    pf = (pFunc)*vtable;//对应的便是Derived::print()函数
    pf();//通过函数指针便可以去调用该函数
    pFunc pf2;
    pf2 = (pFunc)*(vtable + 1);
    pf2();//对应的便是Derived::show()
    pFunc pf3;
    pf3 = (pFunc)*(vtable + 2);
    pf3();//对应的便是Derived::display()

}

int main()
{
    test();
    return 0;
}

创建一个Derived类对象d,这个对象的内存结构是由三个内容构成的,开始位置是虚函数指针,第二个位置是long型数据_base,

第三个位置是long型数据_derived.

第一次强转将这个Derived类对象视为了存放三个long型元素的数组,打印这个数组中的三个元素,后两个本身就是long型数据,输出其值,第一个本身是指针(地址),打印出来的结果是编译器以long型数据来看待这个地址的值。

这个虚函数指针指向虚表,虚表中存放三个虚函数的入口地址(3 * 8字节),那么再将虚表视为存放三个long型元素的数组,第二次强转,直接输出数组的三个元素,得到的结果是编译器以long型数据来看待这三个函数地址的值。

虚表中的三个元素本身是函数指针,那么再将这个三个元素强转成相应类型的函数指针,就可以通过函数指针进行调用了。

——验证了虚表中存放虚函数的顺序,是按照基类中虚函数的声明顺序去存放的。

虚拟继承

虚拟继承是 C++ 中用于解决菱形继承(钻石问题) 的一种技术。当一个派生类间接多次继承自同一个基类时,会导致基类成员在派生类中出现多份副本,虚拟继承可以确保基类在派生类中只保留一份实例。

下面是虚拟继承的内存图示

class B {
public:
    virtual void f() {
        cout << "B::f()" << endl;
    }

    virtual void Bf() {
        cout << "B::Bf()" << endl;
    }

private:
    int _ib;
    char _cb;
};

class B1 : virtual public B {
public:
    virtual void f() { cout << "B1::f()" << endl; }
    virtual void f1() { cout << "B1::f1()" << endl; }
    virtual void Bf1() { cout << "B1::Bf1()" << endl; }
private:
    int _ib1;
    char _cb1;
};

class B2 : virtual public B {
public:
    virtual void f() { cout << "B2::f()" << endl; }
    virtual void f2() { cout << "B2::f2()" << endl; }
    virtual void Bf2() { cout << "B1::Bf2()" << endl; }
private:
    int _ib2;
    char _cb2;
};

class D
    : public B1
    , public B2
{
public:
    virtual void f() { cout << "D::f()" << endl; }
    virtual void f1() { cout << "D::f1()" << endl; }
    virtual void f2() { cout << "D::f2()" << endl; }
    virtual void Df() { cout << "D::Df()" << endl; }
private:
    int _id;
    char _cd;
};

如果派生类中又定义了新的虚函数,会在内存中多出一个属于派生类的虚函数指针,指向一张新的虚表(VS的实现)

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class B {
public:
    virtual void f() {
        cout << "B::f()" << endl;
    }

    virtual void Bf() {
        cout << "B::Bf()" << endl;
    }

private:
    int _ib;
    char _cb;
};

class B1 : virtual public B {
public:
    virtual void f() { cout << "B1::f()" << endl; }
    virtual void f1() { cout << "B1::f1()" << endl; }
    virtual void Bf1() { cout << "B1::Bf1()" << endl; }
private:
    int _ib1;
    char _cb1;
};

class B2 : virtual public B {
public:
    virtual void f() { cout << "B2::f()" << endl; }
    virtual void f2() { cout << "B2::f2()" << endl; }
    virtual void Bf2() { cout << "B1::Bf2()" << endl; }
private:
    int _ib2;
    char _cb2;
};

class D
    : virtual public B1
    , virtual public B2
{
public:
    virtual void f() { cout << "D::f()" << endl; }
    virtual void f1() { cout << "D::f1()" << endl; }
    virtual void f2() { cout << "D::f2()" << endl; }
    virtual void Df() { cout << "D::Df()" << endl; }
private:
    int _id;
    char _cd;
};

int main(){

return 0;
}

菱形继承

虚拟继承是 C++ 中用于解决菱形继承(钻石问题) 的一种技术。当一个派生类间接多次继承自同一个基类时,会导致基类成员在派生类中出现多份副本,虚拟继承可以确保基类在派生类中只保留一份实例。

菱形继承问题示例

class A {  // 公共基类
public:
    int x;
};

class B : public A {};  // B继承A
class C : public A {};  // C继承A
class D : public B, public C {};  // D同时继承B和C

此时D会间接包含A的两份副本(分别来自BC),访问D.x时会产生歧义(不知道访问的是B::A::x还是C::A::x)。

虚拟继承的解决方式

在中间层(BC)继承基类A时,使用virtual关键字声明虚拟继承:

class A {
public:
    int x;
};

// 虚拟继承:B和C共享A的同一个实例
class B : virtual public A {};  
class C : virtual public A {};  

class D : public B, public C {};  // D中只包含一份A的实例

虚拟继承的特点

  1. 消除数据冗余:基类A在最终派生类D中只存在一份实例,避免了多份副本导致的内存浪费。

  2. 解决访问歧义:通过D对象访问A的成员(如d.x)时,不会再产生二义性。

  3. 间接基类初始化:在虚拟继承中,最终派生类(D)需要直接初始化虚拟基类(A),而中间类(BC)对虚拟基类的初始化会被忽略。

    class D : public B, public C {
    public:
        D() : A(), B(), C() {}  // 必须直接初始化虚拟基类A
    };
    

核心用途

虚拟继承主要用于设计复杂的类层次结构,尤其是存在多路径继承同一基类的场景,它保证了基类成员的唯一性,避免了菱形继承带来的二义性和数据冗余问题。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐