1. 什么是多态?

多态 是面向对象编程的三大特性之一(另外两个是封装继承)。通俗来讲,多态就是“一个接口,多种实现”。即同一个行为或函数,在不同对象上表现出不同的形态。

1.1 两类

  • 编译时多态(静态多态:包括函数重载和模板。在编译阶段就确定了调用哪个函数。

  • 运行时多态(动态多态:通过虚函数机制实现,在程序运行时根据对象的实际类型来决定调用哪个函数。

本文将重点讲解运行时多态

1.2 多态例子

  • 买票行为:普通人全价,学生半价,军人优先。

  • 动物叫声:猫发出“喵”,狗发出“汪汪”。


2. 实现条件

要实现运行时多态,必须满足以下两个条件

  1. 必须使用基类的指针或引用来调用虚函数;

  2. 被调用的函数必须是虚函数,并且在派生类中完成了重写。

说明:要实现多态效果,第一必须是基类的指针或引用,因为只有基类的指针或引用才能既指向基类对象又指向派生类对象;第二派生类必须对基类的虚函数完成重写/覆盖,重写或者覆盖了,基类和派生类之间才能有不同的函数,多态的不同形态效果才能达到。

2.1 虚函数与重写

2.1.1 虚函数

在成员函数前加上 virtual 关键字,该函数就成为虚函数。注意非成员函数不能加virtual修饰。

class Person {
public:
    virtual void BuyTicket() {
        cout << "买票-全价" << endl;
    }
};

2.1.2 重写

派生类中定义一个与基类虚函数完全相同(函数名、参数列表、返回值相同)的虚函数,称为重写或覆盖。

class Student : public Person {
public:
    virtual void BuyTicket() {
        cout << "买票-半价" << endl;
    }
};

2.2 示例

多态是面向对象编程的重要特性之一,它允许我们通过基类指针或引用来调用派生类的函数。下面是一个完整的多态调用示例:

class Shape {
public:
    virtual double area() const = 0; // 纯虚函数
};

class Circle : public Shape {
    double radius;
public:
    Circle(double r) : radius(r) {}
    double area() const override {
        return 3.14159 * radius * radius;
    }
};

class Rectangle : public Shape {
    double width, height;
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    double area() const override {
        return width * height;
    }
};

void printArea(const Shape& shape) {
    cout << "面积: " << shape.area() << endl;
}

int main() {
    Circle c(5.0);
    Rectangle r(4.0, 6.0);
    
    printArea(c);  // 输出圆的面积
    printArea(r);  // 输出矩形的面积
    
    return 0;
}

这个示例展示了:

  • 纯虚函数和抽象类

  • 通过引用实现多态

  • 带参数的多态方法调用


3. 实现原理

3.1 虚函数表指针

每个含有虚函数的类(或有虚函数的基类)都会有一个虚函数表指针,指向该类的虚函数表。虚表中存放的是该类所有虚函数的地址。

class Base {
public:
    virtual void func1() {}
    virtual void func2() {}
    int a = 1;
};

int main() {
    Base b;
    cout << sizeof(b) << endl;  // 输出可能是12(32位系统)
    return 0;
}

上面代码运行结果12bytes,除了_b和_ch成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关)。对象中的这个指针我们叫做虚函数表指针(。

4.2虚函数表

  • 基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共用同一张虚表,不同类型的对象各自有独立的虚表,所以基类和派生类有各自独立的虚表。

  • 派生类由两部分构成,继承下来的基类和自己的成员,一般情况下,继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针。

  • 派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。

  • 派生类的虚函数表中包含:(1)基类的虚函数地址,(2)派生类重写的虚函数地址完成覆盖,派生类自己的虚函数地址三个部分。

  • 虚函数表本质是一个存虚函数指针的指针数组,一般情况下这个数组最后面放了一个0x00000000标记。(这个C++并没有进行规定,各个编译器自行定义的,vs系列编译器会再后面放个0x00000000标记,g++系列编译不会放)

  • 虚函数表存在哪的?这个问题严格说并没有标准答案,C++标准并没有规定,我们写下面的代码可以对比验证一下。vs下是存在代码段

这里Derive中没有看到func3函数,这个vs监视窗口看不到,可以通过内存窗口查看

class Derive : public Base {
public:
    virtual void func1() override { cout << "Derive::func1"; }
    virtual void func3() { cout << "Derive::func3"; }
};

4. 动态绑定与静态绑定

  • 静态绑定:在编译时确定函数地址,如普通函数调用、重载函数。

  • 动态绑定:在运行时通过虚函数表查找函数地址,实现多态。

     // ptr是指针+BuyTicket是虚函数满⾜多态条件。
     // 这⾥就是动态绑定,编译在运⾏时到ptr指向对象的虚函数表中确定调⽤函数地址
ptr->BuyTicket();
00EF2001 mov      eax,dword ptr [ptr]
00EF2004 mov      edx,dword ptr [eax]
00EF2006 mov      esi,esp
00EF2008 mov      ecx,dword ptr [ptr]
00EF200B mov      eax,dword ptr [edx]
00EF200D call eax
    // BuyTicket不是虚函数,不满⾜多态条件。
    // 这⾥就是静态绑定,编译器直接确定调⽤函数地址
    ptr->BuyTicket();
00EA2C91 mov      ecx,dword ptr [ptr]
00EA2C94 call     Student::Student (0EA153Ch)

5. 特殊情况

5.1 协变

允许派生类虚函数的返回值类型为基类虚函数返回类型的派生类指针或引用。

class A {};
class B : public A {};

class Person {
public:
    virtual A* BuyTicket() { return nullptr; }
};

class Student : public Person {
public:
    virtual B* BuyTicket() override { return nullptr; }
};

5.2 析构函数的重写

建议将基类的析构函数声明为虚函数,否则通过基类指针删除派生类对象时,只会调用基类析构函数,导致内存泄漏。

class A {
public:
    virtual ~A() { cout << "~A()"; }
};

class B : public A {
public:
    ~B() { cout << "~B()"; delete[] _p; }
private:
    int* _p = new int[10];
};

// 使用:
A* p = new B;
delete p;  // 正确调用 ~B() 和 ~A()

5.3 override 和 final 关键字

从上面可以看出,C++对虚函数重写的要求比较严格,但是有些情况下由于疏忽,比如函数名写错、参数写错等,导致无法构成重写,而这种错误在编译期间是不会报出的,只有在程序运⾏时没有得到预期结果才来debug会得不偿失,因此C++11提供了override,可以帮助用户检测是否重写。如果我们不想让派生类重写这个虚函数,那么可以用final去修饰。

  • override 显式声明重写,编译器会检查是否成功重写。

  • final 禁止后续派生类重写该虚函数。

class Car {
public:
    virtual void Drive() final {}  // 禁止重写
};

class Benz : public Car {
public:
    virtual void Drive() override {} // 错误:无法重写final函数
};

6. 重载、重写、隐藏的对比

类型作用域函数要求关键字
重载同一类函数名相同,参数不同
重写基类与派生类函数名、参数、返回值相同virtual
隐藏基类与派生类函数名相同,不构成重写



7. 纯虚函数与抽象类

在虚函数的后面写上 =0,则这个函数为纯虚函数,纯虚函数不需要定义实现。只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派生类继承若不重写纯虚函数,那么派生类也是抽象类。

class Car {
public:
    virtual void Drive() = 0;  // 纯虚函数
};

class Benz : public Car {
public:
    virtual void Drive() override {
        cout << "Benz-舒适" << endl;
    }
};

// Car car;  // 错误:抽象类不能实例化
Car* p = new Benz;  // 正确:多态使用

8. 总结

特性说明
多态类型编译时多态(重载、模板) vs 运行时多态(虚函数)
实现条件基类指针/引用 + 虚函数重写
底层机制虚函数表(vtable) + 虚函数表指针(vptr)
关键字virtualoverridefinal
应用场景接口抽象、动态行为选择、资源安全释放

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐