【多态:概念 + 实现 + 拓展 + 原理】目录
- 前言:
- ------------多态的概念------------
- ------------多态的实现------------
- ------------多态的拓展------------
-
- [1. override的意义与使用?](#1. override的意义与使用?)
- [2. final关键字怎么使用?](#2. final关键字怎么使用?)
- [3. 析构函数是怎么进行重写的?](#3. 析构函数是怎么进行重写的?)
- [4. 重载 + 隐藏 + 重写的区别是什么?](#4. 重载 + 隐藏 + 重写的区别是什么?)
- [5. 一道多态的面试题,淘汰95%的面试者?](#5. 一道多态的面试题,淘汰95%的面试者?)
- [6. 什么是"纯虚函数 + 抽象类"?](#6. 什么是“纯虚函数 + 抽象类”?)
- ------------多态的原理------------
-
- [1. 什么是虚函数表?](#1. 什么是虚函数表?)
- [2. 什么是虚表指针?](#2. 什么是虚表指针?)
- [3. 一道关于虚表指针的例题,快来尝试一下吧!!!](#3. 一道关于虚表指针的例题,快来尝试一下吧!!!)
- [4. 动态多态的底层原理是什么?](#4. 动态多态的底层原理是什么?)

往期《C++初阶》回顾:
往期《C++进阶》回顾:
/------------ 继承多态 ------------/
【普通类/模板类的继承 + 父类&子类的转换 + 继承的作用域 + 子类的默认成员函数】
【final + 继承与友元 + 继承与静态成员 + 继承模型 + 继承和组合】
前言:
hi~小伙伴们大家好呀!(ノ≧∀≦)ノ♪👋
猜猜今天是什么日子?嗷~是超适合放松的周六呀!先跟大家道一声周末愉快~☀️(๑˃̵ᴗ˂̵)و
不过话说回来,正在享受假期的小伙伴们,是不是每天都像在过周末一样自在呀?哈哈,想想都觉得惬意~(≧∇≦)/☕️
今天要给大家带来的,就是面向对象三大特性里的 "最后一块拼图"------"多态" 的内容啦!(゚▽゚*)📚这次我把知识点梳理成了 【多态:概念 + 实现 + 拓展 + 原理】 的清晰结构,从基础认知到实际操作,再到额外补充的细节和底层逻辑,都帮大家安排得明明白白~(ง •̀_•́)ง✨
希望大家看完这篇博客,能对多态有更透彻的理解,收获满满干货呀!💪(。・ω・。)ノ♥
------------多态的概念------------
多态(Polymorphism)
:是面向对象编程(OOP)的三大核心特性之一,它允许同一操作作用于不同的对象时,可以产生不同的行为。
- 简单说,就是 "一个行为,多种形态",通过统一的接口处理不同类型的对象,从而提高代码的灵活性和可扩展性。
多态的本质 :"一种接口,多种实现"
以下从 C++ 的角度,分
编译时多态
和运行时多态
详细解析:
多态的核心价值 :是 解耦
"接口使用"
和"具体实现"
:
- 调用者只需关注 "做什么"(接口),无需关心 "怎么做"(具体实现)
- 不同对象通过同一接口调用时,会自动执行自身的实现逻辑
C++ 中的多态分类 :编译时多态(静态多态) 和 运行时多态(动态多态)
核心区别在于:确定调用哪个实现的时机(编译阶段 vs 运行阶段)
------------多态的实现------------
① 静态多态的实现
静态多态
:通过函数重载
或模板
,编译期根据调用参数确定具体执行的函数。
- 特点:行为确定于编译阶段,效率高,属于 "静态绑定"。
示例 1 :函数重载(同一作用域的同名函数,参数不同)
cpp
#include <iostream>
using namespace std;
// 重载:根据参数类型/数量,编译期确定调用哪个函数
int Add(int a, int b)
{
return a + b;
}
double Add(double a, double b)
{
return a + b;
}
int main()
{
//1.编译期确定调用 Add(int, int)
cout << Add(1, 2) << endl;
//2.编译期确定调用 Add(double, double)
cout << Add(1.5, 2.5) << endl;
return 0;
}

示例 2 :
模板(泛型编程,编译期生成具体代码)
cpp
#include <iostream>
using namespace std;
// 模板:编译期根据 T 的类型,生成对应函数
template <typename T>
T Add(T a, T b)
{
return a + b;
}
int main()
{
//1.编译期生成 Add<int>(int, int)
cout << Add(1, 2) << endl;
//2.编译期生成 Add<double>(double, double)
cout << Add(1.5, 2.5) << endl;
return 0;
}

② 动态多态的实现
动态多态
:通过虚函数
和继承
,运行期根据对象的实际类型确定调用的函数。
- 特点:行为确定于运行阶段,支持动态扩展,属于 "动态绑定"。
核心条件(三要素):
- 继承:派生类继承基类。
- 虚函数 :基类声明 virtual 函数,派生类重写(override) 该虚函数。
- 基类指针/引用:通过基类指针或引用调用虚函数,指向派生类对象。
虚函数
虚函数(Virtual Function)
:是在基类中声明的、使用virtual
关键字修饰的成员函数。
- 虚函数是 C++ 实现动态多态的核心机制。
- 虚函数的目的是为派生类提供一个可重写的接口。
- 虚函数会让程序在运行时根据对象的实际类型调用对应的函数实现。
任务1 :基类声明虚函数
cpp/*---------------------定义:"基类:Shape类"---------------------*/ class Shape { public: virtual void Draw() //基类声明 virtual,为派生类提供重写接口 { cout << "画一个形状" << endl; } };
重写
重写(Override)
:是派生类重新实现基类中已声明的虚函数,让同一接口在不同派生类中有不同行为。
- 重写是 C++ 面向对象编程中实现动态多态的关键机制。
重写的严格规则(三同原则 + 协变返回)
派生类重写基类虚函数时,需满足以下条件,否则会变成 "隐藏" 而非 "重写"
1. 三同原则(基本规则)
- 函数名相同:派生类函数名必须与基类虚函数完全一致
- 参数列表相同:参数的类型、数量、顺序必须完全一致
- 返回值类型相同 :C++ 要求返回值类型严格一致,除非是协变返回类型
任务2 :派生类重写虚函数
cpp/*---------------------定义:"派生类:Circle类"---------------------*/ class Circle : public Shape { public: //1.派生类重写基类虚函数Draw() void Draw() { cout << "画一个圆" << endl; } }; /*---------------------定义:"派生类:Rectangle类"---------------------*/ class Rectangle : public Shape { public: //1.派生类重写基类虚函数Draw() void Draw() { cout << "画一个矩形" << endl; } };
协变
2. 协变返回(特殊情况)
协变返回类型
:是指若基类虚函数返回基类指针/引用 ,派生类重写的函数可返回派生类指针/引用,仍视为重写。
cpp
class Base
{
public:
virtual Base* Clone() // 基类虚函数返回 Base*
{
return new Base();
}
};
class Derived : public Base
{
public:
Derived* Clone() // 派生类重写,返回 Derived*(协变返回)
{
return new Derived();
}
};
代码示例1:运行时多态的实现
cpp
#include <iostream>
using namespace std;
/*---------------------定义:"基类:Shape类"---------------------*/
class Shape
{
public:
//1.基类定义虚函数Draw()
virtual void Draw() //基类声明 virtual,为派生类提供重写接口
{
cout << "画一个形状" << endl;
}
};
/*---------------------定义:"派生类:Circle类"---------------------*/
class Circle : public Shape
{
public:
//1.派生类重写基类虚函数Draw()
void Draw()
{
cout << "画一个圆" << endl;
}
};
/*---------------------定义:"派生类:Rectangle类"---------------------*/
class Rectangle : public Shape
{
public:
//1.派生类重写基类虚函数Draw()
void Draw()
{
cout << "画一个矩形" << endl;
}
};
int main()
{
//1.基类指针指向派生类对象
Shape* shape1 = new Circle();
Shape* shape2 = new Rectangle();
//2.运行时根据对象实际类型,调用对应 Draw 函数
shape1->Draw(); // 输出:画一个圆(Circle 的 Draw)
shape2->Draw(); // 输出:画一个矩形(Rectangle 的 Draw)
delete shape1;
delete shape2;
return 0;
}

代码示例2:运行时多态的实现
cpp
#include <iostream>
using namespace std;
/*---------------------定义:"基类:Person类"---------------------*/
class Person
{
public:
virtual void BuyTicket() //基类声明 virtual,为派生类提供重写接口
{
cout << "买票-全价" << endl;
}
};
/*---------------------定义:"派生类:Student类"---------------------*/
class Student : public Person
{
public:
virtual void BuyTicket() //重写基类的虚函数 BuyTicket
{
cout << "买票-打折" << endl;
}
};
void Func(Person* ptr) //注意:通过基类指针调用虚函数,实际执行的是指针指向对象的重写版本
{
ptr->BuyTicket();
/* 多态的核心逻辑:
* 1.虽然调用的是 Person 指针的 BuyTicket,
* 2.但实际执行的函数由 ptr 指向的对象的真实类型决定(Person 或 Student)
* 3.这就是运行时多态(动态绑定)的体现
*/
}
int main()
{
//1.创建:"基类 + 派生类"的对象
Person ps;
Student st;
//2.调用 Func 函数,分别传入"Person + Student"对象的地址
Func(&ps); //ptr 是 Person* 类型,指向 Person 对象,调用 Person::BuyTicket
Func(&st); //ptr 是 Person* 类型,但指向 Student 对象,调用 Student::BuyTicket(重写版本)
return 0;
}


------------多态的拓展------------
1. override的意义与使用?
在 C++ 中,派生类重写基类虚函数时,即使派生类的函数不加
virtual
关键字,也能构成重写。这是因为:基类的虚函数被继承到派生类后,会自动保持 "虚函数" 的属性,无需重复声明 virtual
但这种写法不规范,原因有二:
可读性差:其他开发者阅读代码时,无法直观识别这是虚函数重写。
维护风险:若后续修改基类(如:删除虚函数关键字 ),派生类的重写逻辑会被破坏,且难以排查。
因此 :C++11 及以上建议用 override 关键字显式标记重写,既规范又能让编译器帮你检查重写是否正确(如:函数名、参数不匹配时会报错 )
cpp
#include <iostream>
using namespace std;
/*---------------------定义:"基类:Animal类"---------------------*/
class Animal
{
public:
virtual void makeSound()
{
cout << "动物发出声音" << endl;
}
};
/*---------------------定义:"派生类:Dog类"---------------------*/
class Dog : public Animal
{
public:
//写法1:隐式重写(不推荐)
void makeSound()
{
cout << "汪汪汪!" << endl;
}
};
/*---------------------定义:"派生类:Cat类"---------------------*/
class Cat : public Animal
{
public:
//写法2:显式用override标记(推荐)
void makeSound() override
{
cout << "喵喵喵~" << endl;
}
};
/*---------------------测试函数:通过基类指针调用虚函数---------------------*/
void playSound(Animal* animal)
{
animal->makeSound(); //多态调用
}
int main()
{
//1.创建:"基类 + 派生类"的对象
Animal generic;
Dog dog;
Cat cat;
//2.调用playSound函数进行多态调用
playSound(&generic); // 输出:动物发出声音
playSound(&dog); // 输出:汪汪汪!
playSound(&cat); // 输出:喵喵喵~
return 0;
}

2. final关键字怎么使用?
final 关键字
:用于限制
类的继承
或虚函数的重写
,增强代码的安全性和可维护性。
final
关键字有两种用法:
修饰类:禁止该类被继承(即该类不能作为基类)
修饰虚函数:禁止派生类重写该虚函数
1. 修饰类(禁止继承)
cpp
#include <iostream>
using namespace std;
class Base
{
// 基类成员
};
class Derived final : public Base //注意:使用 final 修饰,禁止被继承
{
// 最终派生类成员
};
class IllegalDerived : public Derived //编译错误:无法从 final 类 Derived 派生
{
// ...
};
int main()
{
//1.创建"最终派生类"的对象
Derived d; //正确:可以正常创建 final 类的对象
cout << "成功创建 Derived 对象(final 类)" << endl;
//2.创建"继承最终派生类"的对象
IllegalDerived i; //错误:无法实例化从 final 类派生的类
return 0;
}

2. 修饰虚函数(禁止重写)
cpp
#include <iostream>
using namespace std;
class Base
{
public:
//1.声明虚函数,允许派生类重写
virtual void func()
{
cout << "Base::func()" << endl;
}
//2.声明虚函数并用 final 修饰,禁止派生类重写
virtual void finalFunc() final
{
cout << "Base::finalFunc()" << endl;
}
};
class Derived : public Base
{
public:
//1.正常重写非 final 的虚函数
void func() override
{
cout << "Derived::func()" << endl;
}
//2.编译错误:void Derived::finalFunc() 重写 final 函数
void finalFunc() override
{
cout << "Derived::finalFunc()" << endl;
}
};
int main()
{
//1.测试虚函数重写
Base* ptr = new Derived();
ptr->func(); // 调用 Derived::func()(多态)
//2.测试 final 函数
ptr->finalFunc(); // 错误:Derived::finalFunc() 无法重写 Base::finalFunc()
delete ptr;
return 0;
}

3. 析构函数是怎么进行重写的?
在 C++ 中,当基类的析构函数被声明为虚函数时,只要派生类定义了析构函数,无论是否添加virtual关键字,该派生类析构函数都会与基类析构函数构成重写。
尽管基类析构函数(如:
~Base()
)和派生类析构函数(如:~Derived()
)名字不同,看似不符合重写 "函数名相同" 的规则。但实际上编译器会对析构函数名称做特殊处理,将编译后的名称统一处理为
destructor
,从而实现重写机制。
通过下面的代码示例可以看到:
如果基类析构函数
~A()
没有加virtual
修饰,那么在执行delete p2
时,只会调用基类A
的析构函数,而不会调用派生类B
的析构函数。若派生类
B
的析构函数~B()
中包含资源释放的逻辑,这种情况就会导致资源无法正常释放,进而引发内存泄漏问题 。
cpp
#include <iostream>
using namespace std;
/*---------------------定义:"基类:A类"---------------------*/
class A
{
public:
//1.声明虚析构函数
virtual ~A()
{
cout << "~A()" << endl;
}
//虚析构函数的作用:当通过基类指针删除派生类对象时,确保调用正确的析构函数(派生类析构函数)
};
/*---------------------定义:"派生类:B类"---------------------*/
class B : public A
{
public:
~B() //这里虽然没写 override,但因为基类是虚函数,所以构成是重写不是覆盖
{
cout << "~B()->delete:" << _p << endl;
delete[] _p; //释放动态分配的数组,防止内存泄漏
}
protected:
int* _p = new int[10];
};
int main()
{
//1.创建基类 A 的对象,用基类指针 p1 指向它
A* p1 = new A;
//2.创建派生类 B 的对象,用基类指针 p2 指向它(多态的体现,基类指针指向派生类对象)
A* p2 = new B;
//3.删除 p1 指向的对象,调用基类 A 的析构函数
cout << "---------删除基类的对象---------" << endl;
delete p1;
//4.删除 p2 指向的对象,由于基类 A 的析构函数是虚函数,且派生类 B 重写了析构函数
cout << "---------删除派生类的对象---------" << endl;
delete p2; //注:这里会调用派生类 B 的析构函数,正确释放派生类对象中的资源(如:_p 指向的数组)
return 0;
}

4. 重载 + 隐藏 + 重写的区别是什么?
在 C++ 编程中,
重载(Overload)
、隐藏(Hide
)和重写(Override)
是三个容易混淆但概念完全不同的机制。它们的区别主要体现在
作用范围
、实现方式
和应用场景
上。
重载(Overload)
:同一作用域内(如:同一个类中),允许存在多个同名函数,但参数列表(类型、数量、顺序)不同,与返回值类型无关。核心特点:
- 作用范围:同一类或同一命名空间内。
- 编译期绑定:编译器根据参数类型在编译时确定调用哪个函数。
- 不涉及继承:无需基类与派生类关系。
cpp
class Calculator
{
public:
int add(int a, int b) // 参数类型:int+int
{
return a + b;
}
double add(double a, double b) // 参数类型:double+double
{
return a + b;
}
int add(int a, int b, int c) // 参数数量不同
{
return a + b + c;
}
};
关键场景
- 实现功能类似但 参数类型/数量 不同的函数(如:不同类型的加法)
- 提高代码可读性,避免为相似功能定义不同函数名(如:
addInt
、addDouble
)
隐藏(Hide)
:派生类中定义的成员(函数或变量)与基类同名,导致基类成员在派生类作用域内被隐藏,无法直接访问。
核心特点:
- 作用范围:发生在基类与派生类的继承关系中
- 名称覆盖:只要名称相同即会隐藏,与参数列表无关(即使派生类函数参数不同,基类同名函数也会被隐藏)
- 访问限制 :若需访问基类被隐藏的成员,需用基类名::成员名显式指定
cpp
#include <iostream>
using namespace std;
class Base
{
public:
void func(int x)
{
cout << "Base::func(int)" << endl;
}
void func(double x)
{
cout << "Base::func(double)" << endl;
}
};
class Derived : public Base
{
public:
void func(int x) // 隐藏Base的func(int)和func(double)
{
cout << "Derived::func(int)" << endl;
}
};
int main()
{
Derived d;
d.func(10); // 调用Derived::func(int)
d.Base::func(10); // 显式调用Base::func(int)
d.Base::func(3.14); // 显式调用Base::func(double)
return 0;
}

关键场景
- 派生类需要定义与基类同名的成员,但不想覆盖基类的所有同名函数时,需注意隐藏问题。
重写(Override)
:派生类中重新实现基类的虚函数 ,函数签名(名称、参数列表、返回值类型)必须与基类完全一致(C++11 后可用override
关键字显式声明)
核心特点:
- 作用范围:仅发生在基类与派生类的继承关系中,且基类函数必须是虚函数
- 运行时多态:通过 基类指针/引用 调用时,实际执行的是派生类重写的函数(动态绑定)
- 严格匹配:函数签名必须与基类完全一致,否则会被视为隐藏(除非使用override强制检查)
cpp
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void speak()
{
cout << "Animal speaks" << endl;
}
};
class Dog : public Animal
{
public:
void speak() override // 显式重写
{
cout << "汪汪汪!" << endl;
}
};
class Cat : public Animal
{
public:
void speak() // 隐式重写(等价于override)
{
cout << "喵喵喵~" << endl;
}
};
int main()
{
Animal* ptr1 = new Dog();
Animal* ptr2 = new Cat();
ptr1->speak(); // 输出 "汪汪汪!"(调用Dog::speak)
ptr2->speak(); // 输出 "喵喵喵~"(调用Cat::speak)
delete ptr1;
delete ptr2;
return 0;
}

关键场景
- 实现面向对象的多态性,让不同派生类对象通过统一接口(基类指针)执行不同行为(如:不同动物的叫声)
重载、隐藏、重写的综合对比:
特性 | 重载(Overload) |
隐藏(Hide) |
重写(Override) |
---|---|---|---|
作用范围 | 同一作用域 | 基类和派生类之间 | 基类和派生类之间 |
函数关系 | 函数名相同 参数列表不同 | 函数名相同 (参数列表可同可不同) | 函数名、参数列表、返回类型必须相同 |
是否需虚函数 | 不需要 | 不需要 | 基类函数必须声明为 virtual |
绑定时机 | 编译期静态绑定 | 编译期名称覆盖 | 运行期动态绑定(多态) |
参数要求 | 参数类型 / 数量 / 顺序不同 | 名称相同即可(参数无关) | 必须与基类函数签名完全一致 |
典型场景 | 实现同功能不同参数的函数 | 派生类定义与基类同名的成员 | 实现多态行为 (如:"动物" 派生类的不同叫声) |
5. 一道多态的面试题,淘汰95%的面试者?
以下程序输出结果是什么()
A .A->0
B .B->1
C .
A->1
D .B->0
E .
编译出错
F .以上都不正确
温馨提示:这道题的坑很多,但是这道题也绝对称得上是一道好题!!!
cpp
#include <iostream>
using namespace std;
/*---------------------定义:"基类:A类"---------------------*/
class A
{
public:
virtual void func(int val = 1)
{
cout << "A->" << val << endl;
}
virtual void test()
{
func();
}
};
/*---------------------定义:"派生类:B类"---------------------*/
class B : public A
{
public:
void func(int val = 0)
{
cout << "B->" << val << endl;
}
};
int main()
{
B* p = new B;
p->test();
delete p;
return 0;
}
解析
关键规则:虚函数的动态绑定 + 默认参数的静态绑定
虚函数
func
:
func()
是虚函数,B
重写了A::func()
- 当通过基类指针或引用调用时,实际调用的是派生类的实现
B::func()
默认参数
val
:
- 默认参数在 编译期 根据调用者的静态类型决定,而非运行时动态类型
test()
在A
中定义,调用func()
时使用的默认参数是A::func(int val=1)
的val=1
,即使实际调用的是B::func()
p->test()
调用链的执行流程拆解:
test
是A
的虚函数,B
未重写test
,因此调用A::test
A::test()
内部调用func()
,由于func()
是虚函数,实际执行B::func()
test
定义在A
中,调用func()
时,静态类型是A
,因此使用A::func
的默认参数val = 1
注意:B::func 的默认参数 :func 的默认参数由调用点的静态类型决定

答案 【B】

6. 什么是"纯虚函数 + 抽象类"?
纯虚函数(Pure Virtual Function)
:是一种特殊的虚函数,在基类中声明但没有实现(只有函数原型),必须由派生类重写才能使用。
- 在 C++ 中,纯虚函数通过在虚函数声明末尾添加
= 0
来标识- 纯虚函数在基类中仅需声明而无需定义具体实现(语法上虽允许定义,但因必须由派生类重写,其基类实现通常并无实际意义)
语法示例:
cppclass Shape { public: // 纯虚函数:没有函数体,必须由派生类实现 virtual double area() = 0; };
纯虚函数的特性 :
- 没有默认实现:纯虚函数在基类中只有声明,没有函数体。
- 强制派生类实现:任何继承抽象类的派生类必须实现所有纯虚函数,否则该派生类也会被视为抽象类。
- 支持多态:通过纯虚函数,可以定义基类的接口规范,让派生类提供具体实现,实现运行时多态。
抽象类(Abstract Class)
:是包含至少一个纯虚函数的类,它不能被实例化(即不能创建对象),只能作为基类被继承。
- 抽象类的主要作用是定义接口规范,为派生类提供统一的框架。
cpp
#include <iostream>
using namespace std;
/*------------------定义:"抽象类:Shape类"------------------*/
class Shape
{
public:
//1.定义"纯虚函数"
virtual double area() = 0;
//2.定义"普通虚函数(非纯虚)"
virtual void draw()
{
cout << "Drawing a shape..." << endl;
}
};
/*------------------定义:"派生类:Circle类"------------------*/
class Circle : public Shape
{
private:
double radius;
public:
Circle(double r)
: radius(r)
{
}
//1.必须实现基类的纯虚函数
double area() override
{
return 3.14 * radius * radius;
}
//2.可选重写draw()虚函数
void draw() override
{
cout << "所画圆的半径为 " << radius << endl;
}
};
int main()
{
/*--------------第一阶段:"创建对象"--------------*/
//1.创建虚基类的对象
// Shape shape; // 错误!无法创建抽象类的对象
//2.创建派生类的对象
Circle circle(5.0);
/*--------------第二阶段:"调用函数"--------------*/
//1.多态调用
Shape* ptr = &circle;
cout << "圆的面积为: " << ptr->area() << endl;
ptr->draw();
//2.直接调用
cout << "圆的面积为: " << circle.area() << endl;
circle.draw();
return 0;
}

抽象类的特性:
- 不能实例化:无法创建抽象类的对象
- 必须被继承:抽象类的价值在于被派生类继承并实现其纯虚函数。
- 部分实现可选 :抽象类可以包含普通
成员函数和数据成员
,也可以有虚函数
的默认实现。
------------多态的原理------------
1. 什么是虚函数表?
虚函数表(Virtual Table,简称 vtable)
:是一个存储类的虚函数地址的静态数组,由编译器自动生成和维护。
- 虚函数表是 C++ 等面向对象语言实现运行时多态的核心机制。
- 每个包含虚函数的类 都会有一个独立的虚函数表(vtable),存储该类所有虚函数的地址。
- 类的对象中会隐含一个虚表指针(vptr),指向该类的虚函数表,用于在运行时动态查找并调用正确的虚函数。
下面从虚函数表的
工作原理
、内存布局
和基本特性
三个方面进行详细解释:
虚函数表的工作原理:
编译时
- 编译器为每个包含虚函数的类生成虚函数表,表中按声明顺序存储虚函数的地址
- 如果派生类重写了基类的虚函数,则在派生类的虚函数表中,该函数的地址会被替换为派生类的实现
运行时
- 当通过基类指针或引用调用虚函数时,程序先通过对象的虚表指针找到对应的虚函数表
- 然后根据虚函数在表中的偏移量,找到并调用实际的函数实现(可能是基类或派生类的版本)
虚函数表的内存布局:
假设存在以下继承关系:
cpp
#include <iostream>
using namespace std;
/* 注意事项:
* 1.基类:包含虚函数的类会生成虚函数表(vtable)
* 2.每个对象:包含一个隐藏的虚表指针(vptr)指向该表
*/
/*------------------定义:"抽象类:Base类"------------------*/
class Base
{
public:
//1.定义:"虚函数:func1"
virtual void func1()
{
cout << "Base::func1" << endl;
}
//2.定义:"虚函数:func2"
virtual void func2()
{
cout << "Base::func2" << endl;
}
};
/*------------------定义:"派生类:Derived类"------------------*/
class Derived : public Base
{
public:
//1.重写:"基类的虚函数func1()"
void func1() override
{
cout << "Derived::func1" << endl;
}
//注意:func2()未重写,继承Base::func2()的实现
};
int main()
{
//1.创建基类对象并调用函数
cout << "=== 直接调用Base对象 ===" << endl;
Base base;
base.func1(); // 输出: Base::func1
base.func2(); // 输出: Base::func2
//2.创建派生类对象并直接调用函数
cout << "\n=== 直接调用Derived对象 ===" << endl;
Derived derived;
derived.func1(); // 输出: Derived::func1
derived.func2(); // 输出: Base::func2(继承自基类)
//3.通过基类的指针调用(多态调用)
cout << "\n=== 通过基类指针调用派生类对象 ===" << endl;
Base* ptr = &derived;
ptr->func1(); // 输出: Derived::func1(运行时动态绑定)
ptr->func2(); // 输出: Base::func2(继承自基类)
//4.通过基类的引用调用(同样触发多态)
cout << "\n=== 通过基类引用调用派生类对象 ===" << endl;
Base& ref = derived;
ref.func1(); // 输出: Derived::func1
ref.func2(); // 输出: Base::func2
return 0;
}

内存布局:
cppBase类的虚函数表: ┌───────────────────────┐ │ Base::func1()地址 │ <-- Base对象的vptr指向此处 ├───────────────────────┤ │ Base::func2()地址 │ └───────────────────────┘ Derived类的虚函数表: ┌───────────────────────┐ │ Derived::func1()地址 │ <-- Derived对象的vptr指向此处 ├───────────────────────┤ │ Base::func2()地址 │ <-- 未重写,继承Base的实现 └───────────────────────┘
注意事项:
派生类的内存布局由两部分构成:
继承自基类的成员
,以及自身新增成员
:
一般情况下,派生类继承基类后,会复用基类里的虚函数表指针,不会额外生成新的指针。
但要注意,派生类中 "继承自基类部分" 的虚函数表指针,和直接创建的基类对象的虚函数表指针并非同一实体------ 就像派生类里继承的基类成员,与独立基类对象的成员,是相互独立的内存区域,虚表指针的归属逻辑也遵循类似的 "继承但独立存储" 规则 。
虚函数表的基本特性:
空间开销
:每个对象增加一个虚表指针(通常为 8 字节),每个类增加一个虚函数表。性能开销
:调用虚函数时需要通过虚表间接寻址,比普通函数调用略慢。
虚函数和虚函数表分别存放在哪里?
在 C++ 中,虚函数本身的存储特性和普通函数是 "同根同源" 的:
- 编译完成后,虚函数会被编译成一段机器指令,最终存放在 常量区(也常被称为代码段 ) 里
- 本质就是可执行的程序逻辑,和普通函数的存储区域类型一致。
但虚函数特殊之处在于,编译器会为包含虚函数的类生成 虚函数表(vtable ),虚函数的地址会被登记到对应类的虚函数表中 。
而关于 虚函数表的存储位置,C++ 标准并没有强制、统一的规定,不同编译器实现可能有差异 。
那下面我们以常见的 VS(Visual Studio )IDE为例,进行程序验证,看看:虚函数和虚函数表分别的存在哪里?
cpp
#include <iostream>
#include <cstdio>
#include <cstdlib>
using namespace std;
/*-----------------定义:"基类:Base类"-----------------*/
class Base
{
public:
//1.定义:"虚函数func1"
virtual void func1()
{
cout << "Base::func1" << endl;
}
//2.定义:"虚函数func2"
virtual void func2()
{
cout << "Base::func2" << endl;
}
//3.定义:"普通函数func3"
void func3()
{
cout << "Base::func3" << endl;
}
protected:
int a = 1;
};
/*-----------------定义:"继承类:Derive类"-----------------*/
class Derive : public Base
{
public:
//1.重写:"基类的虚函数 func1"---> 实现派生类自己的逻辑
void func1() override
{
cout << "Derive::func1" << endl;
}
//2.定义:"虚函数 func4"---> 派生类自己的,可继续被它的子类重写
virtual void func4()
{
cout << "Derive::func4" << endl;
}
//3.定义:"普通函数 func5"
void func5()
{
cout << "Derive::func5" << endl;
}
protected:
int b = 2;
};
int main()
{
/*-----------------第一阶段:创建存储在内存四区上的变量-----------------*/
//1.定义"栈区"的普通整型变量 i,值为 0
int i = 0;
//2.定义"静态区"的整型变量 j,值为 1,程序运行期间一直存在
static int j = 1;
//3.在"堆区"上动态分配一个整型内存,p1 指向该内存地址
int* p1 = new int;
//4.定义指向常量字符串的指针 p2,字符串存放在"常量区"
const char* p2 = "xxxxxxxx";
/*-----------------第二阶段:打印内存四区的地址-----------------*/
cout << "---------------内存四区的大致地址---------------" << endl;
printf("栈区:%p\n", &i);
printf("堆区:%p\n", p1); //打印堆区动态分配内存的地址
printf("静态区:%p\n", &j);
printf("常量区:%p\n", p2); //打印常量区字符串的地址
/*-----------------第三阶段:创建基类和派生类的对象-----------------*/
//1.创建"基类 + 派生类"的对象
Base b;
Derive d;
//2.定义基类指针 p1,指向基类对象 b
Base* pb = &b;
//3.定义派生类指针 p2,指向派生类对象 d
Derive* pd = &d;
/*-----------------第三阶段:打印基类和派生类中的"虚函数表 + 虚函数"的地址-----------------*/
cout << "---------------基类和派生类中"虚函数表"地址---------------" << endl;
//1.打印基类对象 b 的虚表地址
printf("Person虚表地址:%p\n", (void*)pb); //注意:通过强转指针取出虚表指针值
//2.打印派生类对象 d 的虚表地址
printf("Student虚表地址:%p\n", (void*)pd); //注意:派生类也有自己的虚函数表
cout << "---------------基类和派生类中"函数"的地址---------------" << endl;
cout << "---------基类中的函数---------" << endl;
printf("虚函数func1:%p\n", &Base::func1);
printf("虚函数func2:%p\n", &Base::func2);
printf("普通函数func3:%p\n", &Base::func3);
cout << "---------派生类中的函数---------" << endl;
printf("重写的虚函数func1:%p\n", &Derive::func1);
printf("虚函数func4:%p\n", &Derive::func4);
printf("普通函数func5:%p\n", &Derive::func5);
delete p1;
return 0;
}

2. 什么是虚表指针?
虚表指针(Virtual Table Pointer,简称 vptr)
:是一个隐式的指针成员,存在于每个包含虚函数的类的对象中。
- 虚表指针是 C++ 实现运行时多态的底层机制之一。
- 它指向该类对应的虚函数表(vtable),用于在运行时动态查找并调用正确的虚函数。
下面从虚函数表的
工作原理
和应用场景
两个方面进行详细解释:
虚表指针的工作原理:
编译时
- 编译器为每个包含虚函数的类生成一个虚函数表(vtable),表中存储该类所有虚函数的地址。
- 如果派生类重写了基类的虚函数,则在派生类的虚函数表中,该函数的地址会被替换为派生类的实现。
运行时
- 当创建一个包含虚函数的类的对象时,编译器会在对象的内存布局中隐式添加一个虚表指针,指向该类的虚函数表。
- 当通过基类指针或引用调用虚函数时,程序会先通过对象的虚表指针找到虚函数表,再根据函数在表中的偏移量调用实际的函数实现。
虚表指针的内存布局:
假设有以下类继承关系:
cpp
class Base
{
public:
//1.定义:"虚函数:func1"
virtual void func1()
{
cout << "Base::func1" << endl;
}
//2.定义:"虚函数:func2"
virtual void func2()
{
cout << "Base::func2" << endl;
}
};
class Derived : public Base
{
public:
//1.重写:"基类的虚函数func1()"
void func1() override
{
cout << "Derived::func1" << endl; // 重写
}
//注意:func2() 未重写,继承Base的实现
};
内存布局:
cppBase对象的内存布局: ┌───────────────────────┐ │ vptr (指向Base的vtable)│ <-- 隐式添加的虚表指针 ├───────────────────────┤ │ 其他数据成员 │ └───────────────────────┘ Base的虚函数表 (vtable): ┌───────────────────────┐ │ Base::func1()地址 │ ├───────────────────────┤ │ Base::func2()地址 │ └───────────────────────┘ Derived对象的内存布局: ┌───────────────────────┐ │vptr(指向Derived的vtable)│ <-- 隐式添加的虚表指针 ├───────────────────────┤ │ 其他数据成员 │ └───────────────────────┘ Derived的虚函数表 (vtable): ┌───────────────────────┐ │ Derived::func1()地址 │ <-- 重写后的函数地址 ├───────────────────────┤ │ Base::func2()地址 │ <-- 继承的函数地址 └───────────────────────┘
虚表指针的基本特性:
隐式存在
:虚表指针由编译器自动添加,用户无法直接访问空间开销
:每个包含虚函数的对象增加一个指针大小的内存开销(通常为 8 字节,取决于系统架构)性能开销
:虚函数调用需要通过虚表指针间接寻址,比普通函数调用略慢。多重继承
:如果一个类继承多个包含虚函数的基类,可能有多个虚表指针,每个指向一个基类的虚函数表。
3. 一道关于虚表指针的例题,快来尝试一下吧!!!
下面的程序在 32 位平台上的运行结果是什么()
A .编译报错
B .运行报错
C .8
D .12
cpp
#include <iostream>
using namespace std;
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
protected:
int _b = 1;
char _ch = 'x';
};
int main()
{
//1.创建的基类的对象
Base b;
//2.输出基类对象 b 的大小
cout << sizeof(b) << endl;
return 0;
}
解析
要解决这个问题,我们需要分析 C++ 类对象的内存布局,关键在于理解虚表指针 和内存对齐 对
sizeof
计算的影响。在 C++ 中,类对象的内存大小由以下部分决定:
- 虚表指针 :若类包含虚函数,编译器会为类生成虚函数表(
vtable
),并在对象中隐含一个指向该表的指针(vptr
)- 内存对齐 :需考虑数据成员的类型大小,以及内存对齐(为提升访问效率,数据成员按一定规则排列,通常对齐到自身类型大小的倍数,最终整体大小对齐到最大成员类型的倍数 )
分析 Base 类的内存组成:
(1)虚函数表指针(vptr)
Base
类定义了虚函数virtual void Func1()
,因此对象会包含一个vptr
,32 位平台占 4 字节(2)非静态数据成员
类中包含:
int _b = 1;
:int
类型占 4 字节char _ch = 'x';
:char
类型占 1 字节(3)内存对齐的影响
为满足内存对齐规则(整体大小需对齐到最大基本成员类型
int
的 4 字节倍数 ):
char _ch
本身占 1 字节,但会填充 3 个空白字节 ,使其占用 4 字节(与int
对齐 )
计算 sizeof(Base):
对象总大小 = vptr 大小 + 数据成员大小(含对齐填充):
4 ( vptr ) + 4 ( int ) + 4 ( char 及填充 ) = 12 字节 4 \, (\text{vptr}) + 4 \, (\text{int}) + 4 \, (\text{char 及填充}) = 12 \, \text{字节} 4(vptr)+4(int)+4(char 及填充)=12字节

答案 【D】
4. 动态多态的底层原理是什么?
动态多态的底层原理的总结:
C++ 中,运行时多态的实现依赖虚函数表(vtable) 和虚表指针(vptr):
虚函数表(vtable):每个包含虚函数的类,编译器会生成一个虚函数表,存储该类所有虚函数的地址。
虚表指针(vptr):每个对象的首地址会包含一个虚表指针,指向所属类的虚函数表。
调用过程 :通过基类指针调用虚函数时,编译器会根据对象的
vptr
找到其实际类型的vtable
,再调用对应虚函数的地址。
cpp
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
private:
string _name; // 姓名成员变量
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "买票-打折" << endl;
}
private:
string _id; // 学号成员变量
};
void Func(Person* ptr)
{
//这里可以看到虽然都是Person指针Ptr在调用BuyTicket
//但是跟ptr没关系,而是由ptr指向的对象决定的
ptr->BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(&ps);
Func(&st);
return 0;
}
