《C++进阶之继承多态》【多态:概念 + 实现 + 拓展 + 原理】

发布于:2025-08-17 ⋅ 阅读:(16) ⋅ 点赞:(0)

在这里插入图片描述

往期《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::BuyTicket

    Func(&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; 
    } 
};

关键场景

  • 实现功能类似但 参数类型/数量 不同的函数(如:不同类型的加法)
  • 提高代码可读性,避免为相似功能定义不同函数名(如:addIntaddDouble

隐藏(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() 调用链的执行流程拆解:

  1. testA 的虚函数,B 未重写 test,因此调用 A::test
  2. A::test() 内部调用 func(),由于 func() 是虚函数,实际执行 B::func()
  3. 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::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;
}

在这里插入图片描述

内存布局:

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,值为 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),表中存储该类所有虚函数的地址。
  • 如果派生类重写了基类的虚函数,则在派生类的虚函数表中,该函数的地址会被替换为派生类的实现。

运行时

  • 当创建一个包含虚函数的类的对象时,编译器会在对象的内存布局中隐式添加一个虚表指针,指向该类的虚函数表。
  • 当通过基类指针或引用调用虚函数时,程序会先通过对象的虚表指针找到虚函数表,再根据函数在表中的偏移量调用实际的函数实现。

虚表指针的内存布局:

假设有以下类继承关系:

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;
}

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到