跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

C++ 多态详解

C++ 多态分为编译时多态和运行时多态。运行时多态需满足继承关系、基类指针或引用调用虚函数、派生类重写虚函数三个条件。核心机制依赖虚函数表(vtable)和虚函数表指针(_vfptr),实现动态绑定。纯虚函数定义抽象类,强制子类实现接口。析构函数建议设为虚函数以防止内存泄漏。override 和 final 关键字用于辅助重写检查和禁止重写。

微码行者发布于 2026/3/30更新于 2026/5/2327 浏览
C++ 多态详解

一、多态的概念与分类

多态(polymorphism)即'多种形态',在 C++ 中分为两类:

  • 编译时多态(静态多态):
    • 典型代表:函数重载、函数模板
    • 特点:在编译阶段,根据实参类型匹配到对应的函数地址,属于静态绑定
  • 运行时多态(动态多态):
    • 典型代表:通过虚函数实现的多态
    • 特点:在运行阶段,根据指针/引用指向的实际对象类型,调用对应的虚函数,属于动态绑定

例子:

  • 买票:普通人全价、学生半价/75 折、军人优先
  • 动物叫:猫'喵'、狗'汪汪'

二、多态的定义及实现

2.1 多态的构成条件

多态发生在继承关系下,用基类的指针或引用调用虚函数,产生不同行为。

必须同时满足两个条件:

  1. 必须是基类的指针或引用调用虚函数
  2. 被调用的函数必须是虚函数,并且在派生类中完成了虚函数重写/覆盖

说明:

  • 只有基类指针/引用才能既指向基类对象,又指向派生类对象
  • 派生类必须对基类虚函数完成重写,才能在运行时表现出不同行为

代码示例:

#include <iostream>
using namespace std;

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

class Student : public Person {
public:
    // 重写基类的虚函数
    void BuyTicket() override {
        cout << "买票 - 打折" << endl;
    }
};

void Func(Person& ptr) {
    // 这里虽然是 Person 引用 ptr 在调用 BuyTicket
    // 但实际调用的函数由 ptr 引用的对象类型决定,这就是多态
    ptr.BuyTicket();
}

int main() {
    Person p;
    Student s;
    Func(p); // 输出:买票 - 全价
    Func(s); // 输出:买票 - 打折
    return 0;
}

核心要点解析

  1. 虚函数重写:
    • Person 中的 BuyTicket 是 virtual 虚函数。
    • Student 中的 BuyTicket 虽然没有写 virtual,但因为继承了基类的虚函数属性,所以依然构成重写(Override)。加上 override 关键字是更好的编程习惯,可以让编译器帮你检查是否真的重写了基类函数。
  2. 多态的触发条件:
    • 这里使用了基类的引用 Person& ptr 作为函数参数。
    • 当 ptr 引用 Person 对象时,调用 Person::BuyTicket()。
    • 当 ptr 引用 Student 对象时,调用 Student::BuyTicket()。
    • 这种在运行时根据对象实际类型来决定调用哪个函数的机制,就是动态绑定,也是多态的核心。
  3. 底层原理:
    • 当类中存在虚函数时,编译器会为该类生成一张虚函数表(vtable),表中存放着所有虚函数的地址。
    • 每个对象都会包含一个隐藏的虚函数表指针(_vfptr),指向所属类的虚函数表。
    • 当通过基类引用调用虚函数时,程序会通过对象的 _vfptr 找到对应的虚函数表,再从表中查找并调用正确的函数地址。
2.1.1 虚函数
  • 定义:在类的成员函数前加 virtual 修饰,该函数即为虚函数
  • 注意:非成员函数不能加 virtual
class Person {
public:
    // 虚函数示例
    virtual void BuyTicket() {
        cout << "买票 - 全价" << endl;
    }
};
2.1.2 虚函数的重写/覆盖
  • 定义:派生类中有一个跟基类完全相同的虚函数(返回值类型、函数名字、参数列表完全相同),称派生类的虚函数重写了基类的虚函数。
  • 注意:派生类虚函数不加 virtual 时,也可构成重写(因为继承后基类虚函数属性被保留),但写法不规范,不推荐。考试选择题常故意设置此'坑',用于判断是否构成多态。

代码示例 1

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

class Student : public Person {
public:
    // 重写基类虚函数
    virtual void BuyTicket() {
        cout << "买票 - 打折" << endl;
    }
};

// 使用示例:基类指针调用
void Func(Person* ptr) {
    ptr->BuyTicket(); // 动态绑定,由 ptr 指向的对象决定调用哪个版本
}

int main() {
    Person ps;
    Student st;
    Func(&ps); // 输出:买票 - 全价
    Func(&st); // 输出:买票 - 打折
    return 0;
}

代码示例 2

// 另一个示例:动物叫声
#include <iostream>
using namespace std;

class Animal {
public:
    // 基类虚函数,定义接口
    virtual void talk() const {}
};

class Dog : public Animal {
public:
    // 重写基类虚函数
    virtual void talk() const override {
        std::cout << "汪汪" << std::endl;
    }
};

class Cat : public Animal {
public:
    // 重写基类虚函数
    virtual void talk() const override {
        std::cout << "(>^ω^<) 喵" << std::endl;
    }
};

void letsHear(const Animal& animal) {
    // 多态调用:根据 animal 实际引用的对象类型,调用对应的 talk()
    animal.talk();
}

int main() {
    Cat cat;
    Dog dog;
    letsHear(cat); // 输出:(>^ω^<) 喵
    letsHear(dog); // 输出:汪汪
    return 0;
}

核心解析

  1. 多态的体现:
    • letsHear 函数接收一个 const Animal& 类型的引用。
    • 当传入 Cat 对象时,调用 Cat::talk();传入 Dog 对象时,调用 Dog::talk()。
    • 同一个函数 letsHear,在运行时根据传入对象的实际类型,表现出不同的行为,这就是多态。
  2. 关键机制:
    • 虚函数:Animal 中的 talk() 被声明为 virtual,这是实现多态的基础。
    • 重写(Override):Dog 和 Cat 类中重新定义了 talk(),与基类的虚函数签名完全一致,构成重写。
    • 动态绑定:通过基类的引用(或指针)调用虚函数时,会在运行时查找对象的虚函数表,找到并调用正确的函数版本。
  3. 代码规范:在派生类的重写函数后加上 override 关键字是一个好习惯,它能让编译器检查你是否真的重写了一个存在的虚函数,避免因拼写错误等导致的'隐藏'而非'重写'的问题。

理解「通过基类的指针 / 引用调用虚函数」

基类指针/引用 = 可以指向/引用 父类对象 或 子类对象

void letsHear(const Animal& animal) {
    animal.talk();
}

这里:animal 是 基类 Animal 的引用;但它既可以引用猫,也可以引用狗 letsHear(cat); // animal 引用了猫 letsHear(dog); // animal 引用了狗 然后你调用:animal.talk(); 这就叫:✅ 通过基类的引用,调用虚函数

再看指针版本(一样道理)

void func(Animal* ptr) {
    ptr->talk(); // 通过基类指针调用虚函数
}
Animal* p1 = new Cat;
Animal* p2 = new Dog;
p1->talk();
p2->talk();

这也叫:✅ 通过基类的指针,调用虚函数

重点:为什么一定要「基类指针/引用」?因为只有基类指针/引用,才能同时接收父类和子类。 Animal& animal = cat; // ✅ 可以 Animal& animal = dog; // ✅ 可以 如果不用基类指针/引用,就不是多态: Cat c; c.talk(); // 这是普通调用,不是多态

总结:多态 = 基类指针/引用 + 调用虚函数;指针/引用是谁不重要;它指向/引用的对象是谁,就调用谁的函数

2.1.3 多态场景选择题解析
class A {
public:
    // 虚函数,默认参数 val=1
    virtual void func(int val = 1) {
        cout << "A->" << val << endl;
    }
    virtual void test() {
        func(); // 这里调用 func
    }
};

class B : public A {
public:
    // 重写虚函数,默认参数 val=0
    void func(int val = 0) {
        cout << "B->" << val << endl;
    }
};

int main() {
    B* p = new B;
    p->test();
    p->func();
    return 0;
}

关键流程

① p->test(); p 是 B* ;B 自己没有写 test() ;所以去调用 父类 A::test()

② 进入 A::test()

virtual void test() {
    func(); // 等价于 this->func();
}

这里的 this 是谁?this 是 A* 类型,但指向的是 B 对象 func() 是虚函数,满足多态条件 → 调用 B::func()

③ 调用 B::func(int val = 0)

这里是最坑的地方: 规则 -> 函数体:运行时动态决议(多态) 默认参数:编译期静态决议(看指针/引用类型)

在 A::test() 里:this 是 A*,编译阶段就把默认参数定为 A 里的 val = 1,不会用 B 里的 0

所以 -> 调用的是:B::func(1),输出:B->1

p->func(); 执行过程:p 是 B*,直接调用 B::func(),没有多态,就是普通调用,默认参数用 B 自己的:val = 0

所以第二句输出:B->0

总结

  1. test 找不到 → 调用 A::test
  2. test 里的 func 是虚函数 → 多态调用 B::func
  3. 默认参数看当前指针类型(A)→ 用 A 的默认值 1
  4. 在 test() 里调用 func:函数 → B,默认参数 → A 的 1
  5. 直接 p->func():函数 → B,默认参数 → B 的 0
  6. 函数体看对象,默认参数看类型。
2.1.4 虚函数重写的其他问题

协变(了解)

  • 定义:派生类重写基类虚函数时,返回值类型不同,但基类返回基类对象指针/引用,派生类返回派生类对象指针/引用,这种情况称为协变
  • 特点:实际意义不大,仅作了解
#include <iostream>
using namespace std;

// 先定义两个有继承关系的类
class A {
public:
    virtual void show() {
        cout << "I am A" << endl;
    }
};

class B : public A {
public:
    void show() override {
        cout << "I am B" << endl;
    }
};

// 父类
class Person {
public:
    virtual A* BuyTicket() {
        cout << "买票 - 全价" << endl;
        return new A; // 返回 A*
    }
};

// 子类
class Student : public Person {
public:
    // 这里返回 B*,B 是 A 的子类 → 满足【协变】
    B* BuyTicket() override {
        cout << "买票 - 打折" << endl;
        return new B; // 返回 B*
    }
};

void Func(Person* ptr) {
    // 多态调用
    A* p = ptr->BuyTicket();
    p->show();
    delete p;
}

int main() {
    Person ps;
    Student st;
    Func(&ps);
    cout << "--------" << endl;
    Func(&st);
    return 0;
}

「协变」;子类重写虚函数时,返回值可以是父类返回值的「派生类指针/引用」。

  1. 类 A 和 类 B(用来演示协变):A 是父类;B 继承 A,是 A 的子类;它们有一个同名虚函数 show(),构成重写;这一对 A 和 B 就是为了满足:返回值类型是父子关系。
  2. Person 类(父类):有一个虚函数 BuyTicket(),返回值类型:A*
  3. Student 类(子类,重点!协变):Student 继承 Person,重写了虚函数 BuyTicket(),返回值是 B*,而 B 是 A 的子类 👉 这就是 C++ 协变:子类重写虚函数时,返回值可以是父类返回值的派生类指针/引用。
  4. 多态调用函数 Func:ptr 是 Person*,可以指向 Person 或 Student;ptr->BuyTicket() 是多态调用:指向 Person → 调用 Person::BuyTicket;指向 Student → 调用 Student::BuyTicket
  5. main 函数执行流程:
    • 第一次调用:Func(&ps)
      • 调用 Person::BuyTicket() ;输出:买票 - 全价 ;返回 new A ;调用 A::show() → 输出 I am A
    • 第二次调用:Func(&st)
      • 调用 Student::BuyTicket() ;输出:买票 - 打折 ;返回 new B ;调用 B::show() → 输出 I am B
  6. 协变的规则(必须记住):必须是 虚函数 重写;返回值必须是 指针 或 引用;子类返回值类型 必须是 父类返回值类型的 派生类。满足这三条,编译器就认为是正确的重写。
  7. 总结(超精简):协变 = 虚函数重写 + 返回值是父子类指针/引用
    • 目的:让子类可以返回更具体的类型,同时保持多态

析构函数的重写

规则:如果基类的析构函数为虚函数,派生类析构函数只要定义,无论是否加 virtual,都与基类析构函数构成重写

原因:编译器对析构函数名称做了特殊处理,统一处理成 destructor

重要性:若基类析构函数不是虚函数,delete 基类指针指向派生类对象时,只会调用基类析构函数,导致派生类资源泄漏

面试高频考点:基类析构函数建议设计为虚函数

#include <iostream>
using namespace std;

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

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

int main() {
    A* p1 = new A;
    A* p2 = new B;
    delete p1;
    delete p2;
    return 0;
}

运行结果(控制台打印)

~A() 
~B()->delete:0000021E45AFB420 
~A()

逐行解释打印过程

  1. 第一行输出:~A() 对应代码:A* p1 = new A; delete p1; p1 指向 new A,是纯 A 对象;delete p1 调用 A 的析构函数;打印:~A()
  2. 第二行输出:~B()->delete:0x... 对应代码:A* p2 = new B; delete p2; p2 是父类指针 A*,但指向子类 B 对象;因为 ~A() 是 虚析构,所以 delete p2 会多态调用 ~B();因为父析构 是虚析构,delete p2 会 多态调用,先调用 ~B(),再自动调用 ~A(),子类、父类都被正确销毁,没有内存泄漏。先执行 B 的析构:打印:~B()->delete:地址;执行 delete _p; 释放 B 内部的数组。
  3. 第三行输出:~A() 子类析构执行完后,会自动调用父类析构,所以再打印:~A()

总结

  • 析构函数是虚函数 → 构成多态
  • 虚析构 = 保证「父类指针删子类对象」时,能删干净。
  • 只要满足:有继承,父类指针指向子类对象,子类有动态内存 / 需要清理,父类析构函数,一律写成虚析构!只要子类里有动态申请的资源,父类析构必须是虚函数。
  • 父类指针指向子类对象,delete 时,先调用子类析构,再自动调用父类析构,这样才不会内存泄漏。
2.1.5 override 和 final 关键字

C++11 引入,用于辅助虚函数重写的检查和控制:

  • override:
    • 作用:显式标记派生类函数是重写基类虚函数,编译器会检查是否真的重写了基类方法
    • 若未重写,编译报错,避免拼写错误等导致的'假重写'
  • final:
    • 作用:修饰虚函数,表示该函数不能被派生类重写;修饰类,表示该类不能被继承
// override 示例
class Car {
public:
    virtual void Drive() {} // 注意:原示例中拼写为 Dirve,会导致 Benz 中 Drive() 不构成重写,编译报错
};

class Benz : public Car {
public:
    virtual void Drive() override { // 检查是否重写了基类的 Drive()
        cout << "Benz-舒适" << endl;
    }
};

// final 示例
class Car {
public:
    virtual void Drive() final {} // 该虚函数不能被重写
};

class Benz : public Car {
public:
    // 编译报错:无法重写 final 函数
    virtual void Drive() {
        cout << "Benz-舒适" << endl;
    }
};
  1. override 是干嘛的?
    • 作用:检查你有没有写对重写。
    • virtual void Drive() override; 告诉编译器:我这个函数,是要重写父类的虚函数!
    • 如果写错了(比如名字拼错、参数不对),编译器直接报错。
    • 不加 override,写错了编译器不报错,只会当成新函数,你还不知道错在哪。
    • 举例:
      • 父类:virtual void Drive() {}
      • 子类写成:virtual void Dirve() override; // 拼错了加了 override → 直接报错,马上就知道写错了。
  2. final 是干嘛的?
    • 作用:禁止子类再重写我。
    • virtual void Drive() final {} 意思就是:到此为止,不许子类再改我这个函数!谁再重写,就编译报错
  3. 一句话总结
    • override:帮你检查重写是否正确,防止写错。
    • final:禁止子类重写,断了继承的路。
  4. final 放在类后面:禁止继承
    • 意思就是:这个类,不能当爸爸!不能被别人继承!
    class Car final // 这里 final
    { };
    // 报错!Car 被 final 了,不能被继承
    class Benz : public Car { };
    
    • class 类名 final;谁都不能继承它;子类都写不出来,直接编译报错

总结:final 写在类后面:禁止被继承;final 写在虚函数后面:禁止被重写

2.1.6 重载/重写/隐藏的对比
特性重载 (Overload)重写/覆盖 (Override)隐藏 (Hide)
作用域同一作用域继承体系的父类和子类(不同作用域)继承体系的父类和子类(不同作用域)
函数名相同相同相同
参数列表不同(类型、个数、顺序)完全相同可相同或不同
返回值可相同或不同必须相同(协变例外)可相同或不同
virtual 关键字无关必须是虚函数无关
本质编译器多态运行期多态名字隐藏,编译期确定

三、纯虚函数和抽象类

  • 纯虚函数:在虚函数声明后加 = 0,如 virtual void Drive() = 0;
    • 特点:不需要实现(语法上可实现,但无意义,因为要被派生类重写),仅用于声明接口
  • 抽象类:包含纯虚函数的类
    • 特点:不能实例化出对象;若派生类继承后不重写纯虚函数,派生类也是抽象类
    • 作用:强制派生类重写虚函数,实现统一接口规范
class Car {
public:
    // 纯虚函数,定义接口
    virtual void Drive() = 0;
};

class Benz : public Car {
public:
    // 重写纯虚函数,才能实例化
    virtual void Drive() {
        cout << "Benz-舒适" << endl;
    }
};

class BMW : public Car {
public:
    virtual void Drive() {
        cout << "BMW-操控" << endl;
    }
};

int main() {
    // Car car; // 编译报错:无法实例化抽象类
    Car* pBenz = new Benz;
    pBenz->Drive(); // 输出:Benz-舒适
    Car* pBMW = new BMW;
    pBMW->Drive(); // 输出:BMW-操控
    return 0;
}

什么是 纯虚函数? 写法:virtual 函数 = 0; 只有声明,没有实现;作用:规定子类必须重写这个函数

什么是 抽象类? 包含 至少一个纯虚函数 的类;不能创建对象!Car car; // 报错!抽象类不能实例化

子类必须做什么? 子类必须重写纯虚函数;不重写 → 子类也变成抽象类,也不能创建对象

main 函数里做了什么? 父类指针 指向子类对象;调用 Drive();发生 多态,调用对应子类的函数

总结:

  1. 纯虚函数:virtual void Drive() = 0;
  2. 抽象类:包含纯虚函数,不能创建对象
  3. 作用:制定接口/规则,强制子类实现
  4. 子类必须重写纯虚函数,否则也不能实例化
  5. 依然支持 多态调用

四、多态的原理

4.1 虚函数表指针(_vfptr)

当类中包含虚函数时,编译器会在对象布局中插入一个虚函数表指针(_vfptr)

该指针指向一个虚函数表(vtable),表中存放该类所有虚函数的地址

32 位程序中,_vfptr 占 4 字节;64 位程序中占 8 字节

class Base {
public:
    virtual void Func1() {
        cout << "Func1()" << endl;
    }
    virtual void Func2() {
        cout << "Func2()" << endl;
    }
    void Func3() // 普通成员函数,不占对象空间
    {
        cout << "Func3()" << endl;
    }
protected:
    int _b = 1;
    char _ch = 'x';
};

int main() {
    Base b;
    cout << sizeof(b) << endl; // 输出 12
    return 0;
}
  1. 关键点:只要有 虚函数,对象里就多一个 虚表指针
    • 只要类里有 至少一个 virtual 函数,对象就会多一个:vfptr 虚函数表指针
    • 32 位平台:指针大小 4 字节,64 位平台:指针大小 8 字节
  2. 成员变量
    int _b; // 4 字节
    char _ch; // 1 字节
    
  3. 内存对齐(重点)
    • 规则:整体对齐到 最大成员类型的大小;这里最大是 int → 对齐到 4 字节
    • 计算 -> vfptr:4 _b:4 _ch:1 总和 = 4 + 4 + 1 = 9 对齐到 4 的倍数 → 12 字节

总结:有虚函数 → 多 4 字节 虚表指针;成员变量:int(4) + char(1);内存对齐 → 最终大小 12

普通成员函数(Func3)不占对象大小!只有:成员变量、虚表指针(有虚函数才存在),才算进 sizeof(对象);虚函数:也不存到对象里!只多一个 8 字节(64 位)或 4 字节(32 位)的指针 指向虚表 👉 函数本身,永远不算进对象大小!

4.2 多态的实现原理

4.2.1 动态绑定过程

满足多态条件时,函数调用在运行时动态绑定:

  1. 通过对象的 _vfptr 找到对应的虚函数表
  2. 在虚函数表中查找要调用的虚函数地址
  3. 根据实际对象类型,调用对应版本的虚函数

代码示例:加 virtual → 有多态(各自调用自己)

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

class Student : public Person {
public:
    virtual void BuyTicket() {
        cout << "买票 - 打折" << endl;
    }
private:
    string _id;
};

class Soldier : public Person {
public:
    virtual void BuyTicket() {
        cout << "买票 - 优先" << endl;
    }
private:
    string _codename;
};

void Func(Person* ptr) {
    ptr->BuyTicket(); // 动态绑定
}

int main() {
    Person ps;
    Student st;
    Soldier sr;
    Func(&ps); // 调用 Person::BuyTicket
    Func(&st); // 调用 Student::BuyTicket
    Func(&sr); // 调用 Soldier::BuyTicket
    return 0;
}

输出:

买票 - 全价
买票 - 打折
买票 - 优先

解释

  1. 父类加了 virtual → 变成虚函数
  2. 子类函数构成重写
  3. 用 Person* 调用时:看指向的对象,不看指针类型;指向谁,就调用谁的函数

对比代码:不加 virtual → 没有多态(全调用父类)

#include <iostream>
#include <string>
using namespace std;

class Person {
public:
    // 没加 virtual
    void BuyTicket() {
        cout << "买票 - 全价" << endl;
    }
protected:
    string _name;
};

class Student : public Person {
public:
    void BuyTicket() {
        cout << "买票 - 打折" << endl;
    }
protected:
    int _id;
};

class Soldier : public Person {
public:
    void BuyTicket() {
        cout << "买票 - 优先" << endl;
    }
protected:
    string _codename;
};

void Func(Person* ptr) {
    ptr->BuyTicket();
}

int main() {
    Person ps;
    Student st;
    Soldier sr;
    Func(&ps);
    Func(&st);
    Func(&sr);
    return 0;
}

输出:

买票 - 全价
买票 - 全价
买票 - 全价

解释:

  1. 父类 BuyTicket 不是虚函数,子类加不加 virtual 都没用,子类的 BuyTicket 不叫重写,叫隐藏
  2. 什么叫「同名隐藏」?子类有个函数,名字和父类一样;但不是重写;用父类指针调用时,只看父类,不看子类
    Func(&ps); // 调 Person::BuyTicket
    Func(&st); // 也调 Person::BuyTicket
    Func(&sr); // 也调 Person::BuyTicket
    
  3. 用 Person* 指针调用时:编译器只看指针类型,不看指向对象
    • 指针是 Person* → 一律调用 Person::BuyTicket

总结

  • 不加 virtual:看指针类型
  • 加 virtual:看指向对象
  • 这就是多态的本质。
  • 父类不加 virtual:指针是谁,就调用谁 → 全调用父类
  • 父类加 virtual:指针指向谁,就调用谁 → 多态生效
4.2.2 动态绑定 vs 静态绑定
  • 静态绑定:不满足多态条件的函数调用,编译期确定函数地址
    • 例子:普通函数调用、非虚函数调用、对象直接调用函数
  • 动态绑定:满足多态条件的函数调用,运行期通过虚函数表确定函数地址
    • 例子:基类指针/引用调用虚函数
// 动态绑定(满足多态条件)
ptr->BuyTicket(); // 运行时在虚函数表中查找地址

// 静态绑定(不满足多态条件)
// BuyTicket 不是虚函数,编译期直接确定调用地址
ptr->BuyTicket();
4.2.3 虚函数表(vtable)

虚函数表是一个数组,存放类中所有虚函数的地址,最后通常以 0x00000000 作为结束标记(不同编译器实现略有差异)

基类和派生类有各自独立的虚函数表:

  • 基类虚函数表:存放基类虚函数地址
  • 派生类虚函数表:
    1. 先拷贝基类虚函数表的内容
    2. 若派生类重写了基类虚函数,用派生类虚函数地址覆盖对应位置
    3. 新增的派生类虚函数地址追加到表中

虚函数表存放在代码段/常量区,虚函数本身也存放在代码段

代码 1

#include <iostream>
using namespace std;

class Base {
public:
    virtual void func1() {
        cout << "Base::func1" << endl;
    }
    virtual void func2() {
        cout << "Base::func2" << endl;
    }
    void func5() {
        cout << "Base::func5" << endl;
    }
protected:
    int a = 1;
};

class Derive : public Base {
public:
    // 重写基类的 func1
    virtual void func1() {
        cout << "Derive::func1" << endl;
    }
    virtual void func3() {
        cout << "Derive::func1" << endl;
    }
    void func4() {
        cout << "Derive::func4" << endl;
    }
protected:
    int b = 2;
};

int main() {
    Base b1;
    Base b2;
    Derive d;
    return 0;
}
  1. 先看类结构
    class Base {
    public:
        virtual void func1() { cout << "Base::func1" << endl; }
        virtual void func2() { cout << "Base::func2" << endl; }
        void func5() { cout << "Base::func5" << endl; } // 普通函数
    protected:
        int a = 1;
    };
    class Derive : public Base {
    public:
        virtual void func1() { cout << "Derive::func1" << endl; } // 重写
        virtual void func3() { cout << "Derive::func3" << endl; }
        void func4() { cout << "Derive::func4" << endl; } // 普通函数
    protected:
        int b = 2;
    };
    
  2. 先讲最重要:虚函数表(虚表)
    • (1)Base 类的虚表
      • Base 有 2 个虚函数:func1、func2
      • 所以 Base 对象里会有:vfptr 虚表指针(4/8 字节)、成员变量 int a
      • 64 位下 sizeof(Base) = 12(8 指针 + 4 int)
    • (2)Derive 类的虚表
      • Derive 继承 Base,会继承虚表。
      • 发生两件事:
        1. func1 被重写 → 虚表中 func1 被替换成 Derive::func1
        2. 新的虚函数 func3 → 加到 Derive 自己的虚表里
      • 最终 Derive 虚表内容:func1 → Derive::func1、func2 → Base::func2、func3 → Derive::func3
      • Derive 对象内容:vfptr(8 字节)、Base::a(4)、Derive::b(4);sizeof(Derive) = 16
  3. 哪些是重写?哪些不是?
    • ✅ 构成重写:Base::func1() virtual Derive::func1() virtual
    • ❌ 不构成重写:func2:子类没重写;func3:子类新虚函数,父类没有;func4、func5:普通成员函数,和多态无关
  4. 普通函数 vs 虚函数
    • 虚函数(virtual):进虚表,对象存指针,支持多态
    • 普通函数(func4、func5):不进虚表,不占对象大小,不支持多态
  5. main 里的对象
    int main() {
        Base b1;   // 有 vfptr + a
        Base b2;   // 有 vfptr + a
        Derive d;  // 有 vfptr + a + b
        return 0;
    }
    
    • b1 和 b2 是两个不同对象,各有一套成员变量;但它们共用同一张虚表 (所有 Base 对象共享一张虚表)
    • d 是子类对象,有自己的虚表
  6. 总结:有虚函数 → 对象多一个 vfptr 虚表指针;子类重写虚函数 → 虚表中对应函数地址被替换;普通函数 不算进对象大小,不进虚表;同类对象 共享同一张虚表;子类会继承并改写虚表

代码 2

class Base {
public:
    virtual void func1() {
        cout << "Base::func1" << endl;
    }
    virtual void func2() {
        cout << "Base::func2" << endl;
    }
    void func5() {
        cout << "Base::func5" << endl;
    } // 普通函数
protected:
    int a = 1;
};

class Derive : public Base {
public:
    // 重写基类 func1
    virtual void func1() {
        cout << "Derive::func1" << endl;
    }
    // 新增虚函数
    virtual void func3() {
        cout << "Derive::func3" << endl;
    }
    void func4() {
        cout << "Derive::func4" << endl;
    } // 普通函数
protected:
    int b = 2;
};

int main() {
    Base b;
    Derive d;
    // 虚函数表地址、虚函数地址示例(vs 下)
    printf("Person 虚表地址:%p\n", *(int*)&b);
    printf("Student 虚表地址:%p\n", *(int*)&d);
    printf("虚函数地址:%p\n", &Base::func1);
    printf("普通函数地址:%p\n", &Base::func5);
    return 0;
}

解释:&b:取对象 b 的地址 (int*)&b:把对象地址强转成 int 指针 (int)&b:解引用,取出对象最前面 4 字节的值 ;对象最前面 4 字节 → 就是虚表指针 vfptr → 也就是虚表地址!

子类对象 d 最前面也是 虚表指针,但它指向的是 Derive 自己的虚表,所以打印出来的地址 和 Base 不一样

&Base::func1:取虚函数的地址;这个地址 存在虚表里面

普通函数地址 直接存在代码段,不进虚表,不占对象空间

运行后你会看到什么(重点)

Base 虚表地址:0xXXXXXXXX
Derive 虚表地址:0xYYYYYYYY
虚函数地址:0xZZZZZZZZ
普通函数地址:0xWWWWWWWW

你会发现 3 个关键事实:

  1. Base 和 Derive 虚表地址不一样,各自有自己的虚表
  2. 虚函数、普通函数地址完全不同 -> 虚函数:走虚表;普通函数:直接调用

总结:有虚函数 → 对象里有虚表指针;每个类一张虚表;子类重写虚函数 → 改写自己虚表;普通函数不进虚表,不占对象空间 (int)&对象 就是在 取虚表地址

3. 子类不重写任何虚函数 → 虚表地址依然不一样!但 虚函数的地址会一样。

  1. 虚表地址((int)&b 和 (int)&d) 永远不一样!
    • Base 有自己的虚表,Derive 有自己的虚表,只要是两个类,虚表就是两个不同的数组,所以它们的地址一定不同。
    Base 虚表地址:0x123
    Derive 虚表地址:0x456 ← 一定不同
    
  2. 虚函数地址(比如 func1、func2) 如果子类没有重写 → 地址完全一样!
    • Base::func1
    • Derive::func1(继承过来,没重写)它们是同一个函数,所以:虚表里面存的函数地址是一样的
  3. 普通函数地址:本来就和虚表无关,永远一样

总结:

  1. 虚表地址:Base 虚表地址 ≠ Derive 虚表地址;不管有没有重写,都不一样
  2. 虚函数地址:子类不重写 → 父子虚函数地址 相同;子类重写 → 父子虚函数地址 不同

虚表:每个类一张,地址永远不同;虚函数:重写才变,不重写就共用父类的

代码 3

#include <cstdio>
using namespace std;

int main() {
    int i = 0;
    static int j = 1;
    int* p1 = new int;
    const char* p2 = "xxxxxxxx";
    printf("栈:%p\n", &i);
    printf("静态区:%p\n", &j);
    printf("堆:%p\n", p1);
    printf("常量区:%p\n", p2);
    return 0;
}

逐行解释

  1. int i = 0; 存放在:栈(stack);局部变量,函数一结束就自动销毁;取地址 &i 就是栈上地址
  2. static int j = 1;
    • 存放在:静态区(全局/静态区);程序整个运行期间都存在;只初始化一次;取地址 &j 是静态区地址
  3. int* p1 = new int; p1 本身在栈,new int 申请的空间在堆(heap),p1 存放的就是堆地址
  4. const char* p2 = "xxxxxxxx";
    • 字符串常量 "xxxxxxxx" 存放在常量区,p2 本身在栈,p2 的值就是常量区地址

这 4 个地址的特点(重点):你运行后会看到类似这样(地址只是示例)

栈:0x7ffee3b5c8ac
静态区:0x4040a0
堆:0x800003240
常量区:0x4020a4

规律一眼看懂:1. 栈地址最高 (接近 0x7fff…) 2. 堆 在中间 3. 静态区、常量区 很低 (靠近程序代码区)

总结:局部变量 → 栈;static / 全局 → 静态区;new / malloc → 堆;字符串常量 → 常量区

五、总结

  1. 多态是什么:父类指针/引用指向子类对象;调用同一个函数,不同对象表现不同行为
  2. 多态成立的 3 个条件:有 继承;子类 重写 父类 虚函数;父类指针/引用调用虚函数
  3. 虚函数 virtual void func() {}:允许子类重写,支持多态
  4. 重写(覆盖):函数名、参数、返回值完全相同;父类必须带 virtual
  5. 协变(特殊重写):返回值是父子类指针/引用,父虚函数返回父类指针,子虚函数返回子类指针
    virtual A* f() {}
    virtual B* f() {} // B 继承 A
    
  6. override:检查重写 void Drive() override; 作用:必须重写成功,否则报错;防止拼写错、参数错
  7. final:禁止重写/继承
    virtual void f() final {} // 不能重写
    class A final {};         // 不能继承
    
  8. 虚析构函数 virtual ~A() {}
    • 父类指针指向子类对象 delete 时;必须用 虚析构;否则子类析构不调用 → 内存泄漏
  9. 纯虚函数 & 抽象类 virtual void Drive() = 0;
    • 包含纯虚函数 → 抽象类;抽象类 不能实例化;子类必须重写,否则还是抽象类
  10. 继承虚函数,重写加指针,多态就成立。父指子类象,析构必须虚,否则会泄漏。纯虚是接口,子类必须写,抽象不实例。override 检查,final 禁止改。
  11. 多态的核心是运行时根据对象类型调用对应函数,依赖虚函数和虚函数表实现
    • 实现多态的关键:基类指针/引用 + 虚函数重写
  12. 虚函数表是实现多态的底层机制,每个含虚函数的类都有一张表,对象通过 _vfptr 访问
  13. 抽象类(含纯虚函数)用于定义接口,强制派生类实现具体功能
  14. 基类析构函数应设为虚函数,避免内存泄漏

目录

  1. 一、多态的概念与分类
  2. 二、多态的定义及实现
  3. 2.1 多态的构成条件
  4. 2.1.1 虚函数
  5. 2.1.2 虚函数的重写/覆盖
  6. 2.1.3 多态场景选择题解析
  7. 2.1.4 虚函数重写的其他问题
  8. 2.1.5 override 和 final 关键字
  9. 2.1.6 重载/重写/隐藏的对比
  10. 三、纯虚函数和抽象类
  11. 四、多态的原理
  12. 4.1 虚函数表指针(_vfptr)
  13. 4.2 多态的实现原理
  14. 4.2.1 动态绑定过程
  15. 4.2.2 动态绑定 vs 静态绑定
  16. 4.2.3 虚函数表(vtable)
  17. 五、总结
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • AI 编程助手横向评测:GitHub Copilot vs CodeWhisperer vs Cursor
  • WSL2 + Ubuntu 24.04 + Docker Desktop 本地开发环境搭建
  • 整车质量估计算法:从原理到实车应用
  • Docker Desktop 启动报错 WSL 版本过旧解决方案
  • OpenClaw 个人 AI 助手安装部署指南
  • C++ 设计模式详解:分类、实现与核心应用
  • Gemma-3-12B-IT WebUI 部署与使用指南
  • 二分查找算法原理与常见场景应用
  • Python 自动化脚本:8 个实用场景与代码实现
  • C++ 基础核心概念:命名空间、引用与内联函数
  • OpenCode 开源 AI 编程助手实战指南
  • GitHub Copilot
  • ThinkPHP 与 Laravel 实现游戏玩家视频交流平台的人脸识别功能
  • OpenCode Superpowers 插件安装与使用指南
  • 在 Kali Linux 上使用 PHPStudy 部署 DVWA 靶场及配置详解
  • 10 款论文 AIGC 降重工具实测对比
  • Python venv 虚拟环境工具使用指南及 uv 升级教程
  • YOLOv11 数据集训练、推理及网络结构详解
  • Rust Trait 定义与实现:从抽象到实践
  • 基于 Isaac Lab 的 Robot Lab 机器人强化学习实战指南

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online