《C++进阶之继承多态》【多态:概念 + 实现 + 拓展 + 原理】
【多态:概念 + 实现 + 拓展 + 原理】目录
- 前言:
- ------------多态的概念------------
- ------------多态的实现------------
- ① 静态多态的实现
- ② 动态多态的实现
- 虚函数
- 重写
- 协变
- ------------多态的拓展------------
- 1. override的意义与使用?
- 2. final关键字怎么使用?
- 3. 析构函数是怎么进行重写的?
- 4. 重载 + 隐藏 + 重写的区别是什么?
- 5. 一道多态的面试题,淘汰95%的面试者?
- 6. 什么是“纯虚函数 + 抽象类”?
- ------------多态的原理------------
- 1. 什么是虚函数表?
- 虚函数和虚函数表分别存放在哪里?
- 2. 什么是虚表指针?
- 3. 一道关于虚表指针的例题,快来尝试一下吧!!!
- 4. 动态多态的底层原理是什么?
往期《C++初阶》回顾:
《C++初阶》目录导航
往期《C++进阶》回顾:
/------------ 继承多态 ------------/
【普通类/模板类的继承 + 父类&子类的转换 + 继承的作用域 + 子类的默认成员函数】
【final + 继承与友元 + 继承与静态成员 + 继承模型 + 继承和组合】
前言:
hi~小伙伴们大家好呀!(ノ≧∀≦)ノ♪👋
猜猜今天是什么日子?嗷~是超适合放松的周六呀!先跟大家道一声周末愉快~☀️(๑˃̵ᴗ˂̵)و
不过话说回来,正在享受假期的小伙伴们,是不是每天都像在过周末一样自在呀?哈哈,想想都觉得惬意~(≧∇≦)/☕️
今天要给大家带来的,就是面向对象三大特性里的 “最后一块拼图”——“多态” 的内容啦!(゚▽゚*)📚
这次我把知识点梳理成了 【多态:概念 + 实现 + 拓展 + 原理】 的清晰结构,从基础认知到实际操作,再到额外补充的细节和底层逻辑,都帮大家安排得明明白白~(ง •̀_•́)ง✨
希望大家看完这篇博客,能对多态有更透彻的理解,收获满满干货呀!💪(。・ω・。)ノ♥
------------多态的概念------------
多态(Polymorphism)
:是面向对象编程(OOP)的三大核心特性之一,它允许同一操作作用于不同的对象时,可以产生不同的行为。
- 简单说,就是 “一个行为,多种形态”,通过统一的接口处理不同类型的对象,从而提高代码的灵活性和可扩展性。
多态的本质:“一种接口,多种实现”
以下从 C++ 的角度,分
编译时多态
和运行时多态
详细解析:
多态的核心价值:是 解耦
“接口使用”
和“具体实现”
:
- 调用者只需关注 “做什么”(接口),无需关心 “怎么做”(具体实现)
- 不同对象通过同一接口调用时,会自动执行自身的实现逻辑
C++ 中的多态分类:编译时多态(静态多态) 和 运行时多态(动态多态)
核心区别在于:确定调用哪个实现的时机(编译阶段 vs 运行阶段)
------------多态的实现------------
① 静态多态的实现
静态多态
:通过函数重载
或模板
,编译期根据调用参数确定具体执行的函数。
- 特点:行为确定于编译阶段,效率高,属于 “静态绑定”。
示例 1:
函数重载(同一作用域的同名函数,参数不同)
#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:
模板(泛型编程,编译期生成具体代码)
#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:
基类声明虚函数
/*---------------------定义:“基类:Shape类”---------------------*/ class Shape { public:virtual void Draw() //基类声明 virtual,为派生类提供重写接口{ cout << "画一个形状" << endl; } };
重写
重写(Override)
:是派生类重新实现基类中已声明的虚函数,让同一接口在不同派生类中有不同行为。
- 重写是 C++ 面向对象编程中实现动态多态的关键机制。
重写的严格规则(三同原则 + 协变返回)
派生类重写基类虚函数时,需满足以下条件,否则会变成 “隐藏” 而非 “重写”
1. 三同原则(基本规则)
- 函数名相同:派生类函数名必须与基类虚函数完全一致
- 参数列表相同:参数的类型、数量、顺序必须完全一致
- 返回值类型相同:C++ 要求返回值类型严格一致,除非是协变返回类型
任务2:
派生类重写虚函数
/*---------------------定义:“派生类: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. 协变返回(特殊情况)
协变返回类型
:是指若基类虚函数返回基类指针/引用,派生类重写的函数可返回派生类指针/引用,仍视为重写。
class Base
{
public:virtual Base* Clone() // 基类虚函数返回 Base*{ return new Base(); }
};class Derived : public Base
{
public:Derived* Clone() // 派生类重写,返回 Derived*(协变返回){ return new Derived(); }
};
代码示例1:运行时多态的实现
#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:运行时多态的实现
#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::BuyTicketFunc(&st); //ptr 是 Person* 类型,但指向 Student 对象,调用 Student::BuyTicket(重写版本)return 0;
}
------------多态的拓展------------
1. override的意义与使用?
在 C++ 中,派生类重写基类虚函数时,即使派生类的函数不加
virtual
关键字,也能构成重写。这是因为:基类的虚函数被继承到派生类后,会自动保持 “虚函数” 的属性,无需重复声明 virtual
但这种写法不规范,原因有二:
可读性差:其他开发者阅读代码时,无法直观识别这是虚函数重写。
维护风险:若后续修改基类(如:删除虚函数关键字 ),派生类的重写逻辑会被破坏,且难以排查。
因此:C++11 及以上建议用 override 关键字显式标记重写,既规范又能让编译器帮你检查重写是否正确(如:函数名、参数不匹配时会报错 )
#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. 修饰类(禁止继承)
#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. 修饰虚函数(禁止重写)
#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()
中包含资源释放的逻辑,这种情况就会导致资源无法正常释放,进而引发内存泄漏问题 。
#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)
:同一作用域内(如:同一个类中),允许存在多个同名函数,但参数列表(类型、数量、顺序)不同,与返回值类型无关。核心特点:
- 作用范围:同一类或同一命名空间内。
- 编译期绑定:编译器根据参数类型在编译时确定调用哪个函数。
- 不涉及继承:无需基类与派生类关系。
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)
:派生类中定义的成员(函数或变量)与基类同名,导致基类成员在派生类作用域内被隐藏,无法直接访问。
核心特点:
- 作用范围:发生在基类与派生类的继承关系中
- 名称覆盖:只要名称相同即会隐藏,与参数列表无关(即使派生类函数参数不同,基类同名函数也会被隐藏)
- 访问限制:若需访问基类被隐藏的成员,需用基类名::成员名显式指定
#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强制检查)
#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.以上都不正确
温馨提示:这道题的坑很多,但是这道题也绝对称得上是一道好题!!!
#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
来标识- 纯虚函数在基类中仅需声明而无需定义具体实现(语法上虽允许定义,但因必须由派生类重写,其基类实现通常并无实际意义)
语法示例:
class Shape { public:// 纯虚函数:没有函数体,必须由派生类实现virtual double area() = 0; };
纯虚函数的特性 :
- 没有默认实现:纯虚函数在基类中只有声明,没有函数体。
- 强制派生类实现:任何继承抽象类的派生类必须实现所有纯虚函数,否则该派生类也会被视为抽象类。
- 支持多态:通过纯虚函数,可以定义基类的接口规范,让派生类提供具体实现,实现运行时多态。
抽象类(Abstract Class)
:是包含至少一个纯虚函数的类,它不能被实例化(即不能创建对象),只能作为基类被继承。
- 抽象类的主要作用是定义接口规范,为派生类提供统一的框架。
#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),指向该类的虚函数表,用于在运行时动态查找并调用正确的虚函数。
下面从虚函数表的
工作原理
、内存布局
和基本特性
三个方面进行详细解释:
虚函数表的工作原理:
编译时
- 编译器为每个包含虚函数的类生成虚函数表,表中按声明顺序存储虚函数的地址
- 如果派生类重写了基类的虚函数,则在派生类的虚函数表中,该函数的地址会被替换为派生类的实现
运行时
- 当通过基类指针或引用调用虚函数时,程序先通过对象的虚表指针找到对应的虚函数表
- 然后根据虚函数在表中的偏移量,找到并调用实际的函数实现(可能是基类或派生类的版本)
虚函数表的内存布局:
假设存在以下继承关系:
#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::func1base.func2(); // 输出: Base::func2//2.创建派生类对象并直接调用函数cout << "\n=== 直接调用Derived对象 ===" << endl;Derived derived;derived.func1(); // 输出: Derived::func1derived.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::func1ref.func2(); // 输出: Base::func2return 0;
}
内存布局:
Base类的虚函数表: ┌───────────────────────┐ │ Base::func1()地址 │ <-- Base对象的vptr指向此处 ├───────────────────────┤ │ Base::func2()地址 │ └───────────────────────┘Derived类的虚函数表: ┌───────────────────────┐ │ Derived::func1()地址 │ <-- Derived对象的vptr指向此处 ├───────────────────────┤ │ Base::func2()地址 │ <-- 未重写,继承Base的实现 └───────────────────────┘
注意事项:
派生类的内存布局由两部分构成:
继承自基类的成员
,以及自身新增成员
:
一般情况下,派生类继承基类后,会复用基类里的虚函数表指针,不会额外生成新的指针。
但要注意,派生类中 “继承自基类部分” 的虚函数表指针,和直接创建的基类对象的虚函数表指针并非同一实体—— 就像派生类里继承的基类成员,与独立基类对象的成员,是相互独立的内存区域,虚表指针的归属逻辑也遵循类似的 “继承但独立存储” 规则 。
虚函数表的基本特性:
空间开销
:每个对象增加一个虚表指针(通常为 8 字节),每个类增加一个虚函数表。性能开销
:调用虚函数时需要通过虚表间接寻址,比普通函数调用略慢。
虚函数和虚函数表分别存放在哪里?
在 C++ 中,虚函数本身的存储特性和普通函数是 “同根同源” 的:
- 编译完成后,虚函数会被编译成一段机器指令,最终存放在 常量区(也常被称为代码段 ) 里
- 本质就是可执行的程序逻辑,和普通函数的存储区域类型一致。
但虚函数特殊之处在于,编译器会为包含虚函数的类生成 虚函数表(vtable ),虚函数的地址会被登记到对应类的虚函数表中 。
而关于 虚函数表的存储位置,C++ 标准并没有强制、统一的规定,不同编译器实现可能有差异 。
那下面我们以常见的 VS(Visual Studio )IDE为例,进行程序验证,看看:虚函数和虚函数表分别的存在哪里?
#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,值为 0int 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,指向基类对象 bBase* pb = &b;//3.定义派生类指针 p2,指向派生类对象 dDerive* 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),表中存储该类所有虚函数的地址。
- 如果派生类重写了基类的虚函数,则在派生类的虚函数表中,该函数的地址会被替换为派生类的实现。
运行时
- 当创建一个包含虚函数的类的对象时,编译器会在对象的内存布局中隐式添加一个虚表指针,指向该类的虚函数表。
- 当通过基类指针或引用调用虚函数时,程序会先通过对象的虚表指针找到虚函数表,再根据函数在表中的偏移量调用实际的函数实现。
虚表指针的内存布局:
假设有以下类继承关系:
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的实现
};
内存布局:
Base对象的内存布局: ┌───────────────────────┐ │ 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
#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
,再调用对应虚函数的地址。
#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;
}