【C++】动态多态

多态概念

多态(Polymorphism) 是 C++ 面向对象编程(OOP)的三大特性之一(封装、继承、多态)。多态指的是 相同的接口(函数名)在不同的类中表现出不同的行为。简单来说就是一个事物作用于不同的对象,实现的功能状态不一样。

多态的核心思想

父类指针(或引用)指向子类对象,调用子类的重写方法

C++ 多态的两种类型

C++ 支持 两种多态

  1. 编译时多态(静态多态 )
    • 函数重载 (Function Overloading)
    • 运算符重载 (Operator Overloading)
    • 模板 (Templates)
    • 绑定时机:编译期
  2. 运行时多态(动态多态 )
    • 虚函数 (Virtual Functions)
    • 抽象类 (Abstract Class)
    • 绑定时机:运行期(通过 vtable 机制)

实现动态多态的特点和要求💥💥

1:必须要有继承,没有继承,就没有多态(父类的指针/引用可以指向不同的子类对象)
2:子类必须要重写父类的同名方法
3:父类的同名方法必须定义成虚函数
注意❗❗:父类的同名方法定义成了虚函数,所有子类中同名的方法全部都默认是虚函数
          (子类加不加virtual都行)
-------------------------------------------------------------------------------
语法
virtual 返回值 函数名(参数)
{
    
}

动态多态应用场景举例

在这里插入图片描述

#include <iostream>
using namespace std;
// 基类,武器是什么武器根本不知道! 所以该武器的攻击是一种虚拟的行为
class wuqi
{
public:
    // 武器攻击的虚方法 (虚构出来的)
    virtual void pk()
    {
        cout << "不知道啥武器战斗" << endl;
    }
};
 
// 实例化武器  -> 弓箭
class BowAndArrow : public wuqi
{
public:
    virtual void pk() // 覆盖基类的攻击方法
    {
        cout << "弓箭发动会心一击!造成伤害 99999" << endl;
    }
};
 
// 实例化武器  -> 魔法书
class MagicBook : public wuqi
{
public:
    virtual void pk() // 覆盖基类的攻击方法
    {
        cout << "魔法书发动大火球,造成灼伤效果,每秒扣除 1000" << endl;
    }
};
 
// 实例化武器  -> 魔法书
class LightSword : public wuqi
{
public:
    virtual void pk() // 覆盖基类的攻击方法
    {
        cout << "光剑发动,旋风斩,群体伤害 88888" << endl;
    }
};
 
// 创建一个角色
class role
{
public:
    void attck(wuqi *p) // 角色的攻击接口,实现攻击的多态性
    {
        p->pk(); // 用武器战斗
    }
};
 
int main()
{
    // 创建一个角色
    role r;
    // 一个攻击函数,作用于不同的武器对象,攻击效果不一样!
    r.attck(new BowAndArrow);
    r.attck(new MagicBook);
    r.attck(new LightSword);
}

在这里插入图片描述

多态(虚函数)底层原理

C++ 的 运行时多态(动态多态) 是通过 虚函数表(vtable)虚指针(vptr) 来实现的。虚函数表是一种用于 动态绑定 的内部数据结构,它可以让 父类指针调用子类的方法

什么是 vtable 虚函数表?

当一个类包含虚函数virtual 关键字),编译器会为它创建一个虚函数表(vtable)。这个表包含了指向该类虚函数的指针

  • 每个包含虚函数的类都有一个 vtable
  • 每个对象都有一个 vptr(虚指针)指向 vtable
  • 调用虚函数时,程序会通过 vptr 查找 vtable,然后调用正确的函数

在这里插入图片描述

证明虚指针存在

#include <iostream>
using namespace std;

class Base {
public:
    virtual void show() 
    {
        cout << "Base::show()" << endl;
    }
private:
    int a;
};

class Derived : public Base 
{
public:
    void show() override
    {
        cout << "Derived::show()" << endl;
    }
private:
    double b;
};

int main()
{
    cout << sizeof(Base) << endl; // 16
    cout << sizeof(Derived) << endl; // 24

    return 0;
}

C++隐藏-重载-重写区别👍👍

重写(覆盖,复写)

重写必须满足 3 个核心条件:

条件说明示例
1. 基类函数必须是 virtual只有基类虚函数才能被重写,否则是隐藏virtual void func();
2. 子类函数的签名必须完全相同函数名、返回类型、参数列表必须一致void func(int)
(参数不同,变成重载)
3. 访问权限必须 兼容子类方法的访问权限不能低于基类方法public → publicpublic → private
#include <iostream>
using namespace std;

class Animal {
public:
    virtual void eat() {  // ✅ 虚函数(必须有 virtual 关键字)
        cout << "Animal is eating" << endl;
    }
};

class Cat : public Animal {
public:
    void eat() override {  // ✅ 重写(override)
        cout << "Cat is eating fish" << endl;
    }
};

int main() {
    Animal* a = new Cat(); // ✅ 基类指针指向子类对象
    a->eat(); // 🔥 调用 Cat::eat(),而不是 Animal::eat()
    a->Animal::eat(); // ✅ 调用基类函数
    delete a;
    return 0;
}

隐藏

子类和父类的函数名相同

  • 如果派生类函数与基类函数同名,但参数不同,无论基类函数前是否有virtual修饰,基类函数被隐藏.
  • 如果派生类函数与基类函数同名,参数也相同(不关心返回值类型),但是基类函数前无virtual修饰,基类函数被隐藏。
#include <iostream>
using namespace std;

class Base {
public:
    void show() {  // 基类方法
        cout << "Base::show()" << endl;
    }
};

class Derived : public Base {
public:
    void show(int x) {  // ❌ 这是 "隐藏",不是 "重写"
        cout << "Derived::show(" << x << ")" << endl;
    }
};

int main() {
    Derived d;
    d.show(10);  // ✅ 调用 Derived::show(int)
    // d.show();  // ❌ 编译错误!Base::show() 被隐藏,无法访问

    d.Base::show();  // ✅ 仍然可以手动访问基类的方法
    Base* p = &d;	// ✅利用基类指针,显示隐藏后的接口
    p->show();
    return 0;
}

注意:❗❗

  • 子类void show(int x)前面加上 virtual修饰也还是隐藏,因为基类和派生类的参数不同,重写要求参数一致
  • 子类void show()也是隐藏,因为父类前面必须加上 virtual修饰

重载

当一个函数具有多个相同的函数名,不同的参数列表,不关心返回值,且要在同一个类内,这就是函数重载

#include <iostream>
using namespace std;

class OverloadExample {
public:
    void show() {  // ❶ 第一个 show()
        cout << "show() without parameters" << endl;
    }

    void show(int x) {  // ❷ 第二个 show()
        cout << "show(int x): " << x << endl;
    }

    void show(double y) {  // ❸ 第三个 show()
        cout << "show(double y): " << y << endl;
    }
};

int main() {
    OverloadExample obj;
    obj.show();      // ✅ 调用 show()
    obj.show(10);    // ✅ 调用 show(int)
    obj.show(3.14);  // ✅ 调用 show(double)
    return 0;
}

父类的同名函数是虚函数(virtual)和普通函数(非虚函数)的区别

在 C++ 中,函数的调用方式 取决于它是 普通函数 还是 虚函数(virtual

  • 普通函数(非虚函数) → 静态联编(Static Binding)
  • 虚函数(virtual) → 动态联编(Dynamic Binding)

静态联编 vs 动态联编

静态联编(Static Binding)

  • 发生在 编译阶段,编译器在编译时就决定调用哪个函数
  • 函数的调用取决于“左侧变量的类型”(即“指针/引用的静态类型”),而不是右侧对象的实际类型
  • 普通(非 virtual)成员函数采用静态联编
#include <iostream>
using namespace std;

class Animal {
public:
    void eat()  // ❌ 不是虚函数,采用静态联编
    {
        cout << "动物吃食物" << endl;
    }
};

class Cat : public Animal {
public:
    void eat()  // ❌ 这不是重写,而是隐藏(Hiding)
    {
        cout << "猫吃鱼" << endl;
    }
};

int main()
{
    Animal a;
    Animal *animalPtr;

    Cat c;
    Cat *catPtr;

    // 1️⃣ 父类指针指向父类对象
    animalPtr = &a;
    animalPtr->eat();  // ✅ 调用 Animal::eat()
    //Animal &animalRef1 = a;  // 换成引用也是一样的
    //animalRef1.eat(); 

    // 2️⃣ 父类指针指向子类对象(不需要强制转换)
    animalPtr = &c;
    animalPtr->eat();  // ❌ 调用 Animal::eat(),不会调用 Cat::eat()

    // 3️⃣ 子类指针指向父类对象(⚠️ 需要强制转换)
    catPtr = (Cat *)&a;  
    catPtr->eat();  // ❌ 可能导致未定义行为!
    //Cat &catRef1 = (Cat &)a;  // 换成引用也是一样的
    //catRef1.eat();  

    // 4️⃣ 子类指针指向子类对象
    catPtr = &c;
    catPtr->eat();  // ✅ 调用 Cat::eat()

    return 0;
}

动态联编(Dynamic Binding)

  • 发生在 运行时,编译器会在运行时确定调用哪个函数
  • 函数的调用取决于“右侧对象的实际类型”(即“赋值给指针/引用的对象类型”)
  • virtual 成员函数采用动态联编,它依赖于虚表(vtable) 机制。
#include <iostream>
using namespace std;

class Animal {
public:
    virtual void eat()  // ✅ 变成虚函数,采用动态联编
    {
        cout << "动物吃食物" << endl;
    }
};

class Cat : public Animal {
public:
    void eat() override  // ✅ 正确重写(Override)
    {
        cout << "猫吃鱼" << endl;
    }
};

int main()
{
    Animal a;
    Animal *animalPtr;

    Cat c;
    Cat *catPtr;

    // 1️⃣ 父类指针指向父类对象
    animalPtr = &a;
    animalPtr->eat();  // ✅ 调用 Animal::eat()

    // 2️⃣ 父类指针指向子类对象(不需要强制转换)
    animalPtr = &c;
    animalPtr->eat();  // ✅ 调用 Cat::eat()(动态绑定)

    // 3️⃣ 错误情况(父类对象尝试转换为子类指针)
    catPtr = dynamic_cast<Cat*>(&a);  
    if (catPtr) {
        catPtr->eat();  // ❌ 这个不会执行,因为 catPtr 是 nullptr
    } else {
        cout << "转换失败,catPtr 为空" << endl;
    }

    // 4️⃣ 子类指针指向子类对象
    catPtr = &c;
    catPtr->eat();  // ✅ 调用 Cat::eat()

    return 0;
}

虚析构

问题: 父类的指针指向子类对象的时候,如果delete释放父类的指针,那么正常情况下只会调用父类的析构函数,不会调用子类的析构函数(释放不彻底)

虚析构作用: 把基类与派生类的所有析构函数都放入虚表,这样基类和派生类的析构都会执行。

虚析构原理:

不加virtual,此时采用静态联编(只调用赋值运算左边的类(父类)析构函数) 
        Animal *p=c1;
        delete p;virtual,此时采用动态联编(依据赋值运算右边的类型(子类),先调用子类析构,再调用父类析构)
        Animal *p=c1;
        delete p;

语法:

virtual ~析构函数   //写了虚析构就不能同时再去写普通析构
{

}

示例:

#include <iostream>
using namespace std;

class Animal {
public:
    virtual void eat() {  // ✅ 虚函数
        cout << "动物吃食物" << endl;
    }

    virtual ~Animal() {  // ✅ 变成虚析构
        cout << "Animal 虚析构函数" << endl;
    }
};

class Cat : public Animal {
public:
    void eat() override {
        cout << "猫吃鱼" << endl;
    }

    ~Cat() {
        cout << "Cat 析构函数" << endl;
    }
};

int main() {
    Animal* animalPtr = new Cat();
    animalPtr->eat();  // ✅ 调用 Cat::eat()
    delete animalPtr;  // ✅ 现在会先调用 Cat::~Cat(),再调用 Animal::~Animal()
    return 0;
}

纯虚函数和抽象类

纯虚函数

概念: 纯虚函数是基类中不能直接实现的函数,它的目的是让子类必须重写这个函数,否则子类就会变成抽象类,无法实例化。

语法:

virtual 返回类型 函数名(参数) = 0; //空架子,没有函数体,也不能有函数体

其中,= 0 表示这是一个纯虚函数,在基类中不会提供实现,必须由子类重写

抽象类(abstract base class 抽象基类)

👉 定义
    1.抽象类是至少包含一个纯虚函数的类。
    2.不能直接创建对象(实例化),只能作为基类被继承。
    3.子类继承了抽象类,子类必须把抽象类中所有的纯虚函数都实现,
      如果有任何一个纯虚函数没有实现,那么子类依然是抽象类。
    4.子类实现抽象类的纯虚函数,必须同名同参,同返回值。
    5.作用:提供接口,让子类实现具体行为。

示例:

#include <iostream>
using namespace std;

// 🐾 1. 抽象类(含有纯虚函数)
class Animal {
public:
    virtual void makeSound() = 0;  // ✅ 纯虚函数,子类必须实现
    virtual ~Animal() {}           // ✅ 虚析构,防止内存泄漏
};

// 🐱 2. 具体子类(继承抽象类)
class Cat : public Animal {
public:
    void makeSound() override {  // ✅ 必须实现纯虚函数
        cout << "喵喵喵!" << endl;
    }
};

// 🐶 3. 另一个子类
class Dog : public Animal {
public:
    void makeSound() override {  // ✅ 必须实现纯虚函数
        cout << "汪汪汪!" << endl;
    }
};

int main() {
    // ❌ 不能创建抽象类对象
    // Animal a;  // 错误:抽象类不能实例化

    // ✅ 但可以用基类指针指向子类对象(多态)
    Animal* cat = new Cat();
    Animal* dog = new Dog();

    cat->makeSound();  // 输出:喵喵喵!
    dog->makeSound();  // 输出:汪汪汪!

    delete cat;
    delete dog;
    return 0;
}

总结

<think>好的,我现在要详细分用户的问题,了解C++动态多态的底层实现机制,特别是vtable的原理。首先,我需要回忆自己对C++多态的理解,并查阅提供的引用资料来确保准确性。 用户的问题集中在动态多态的底层机制,特别是虚函数表(vtable)和虚函数表指针(vptr)。根据引用[2]和[3],动态多态是通过虚函数表实现的。当类中存在函数时,编译器会为该类生成一个虚函数表,其中存放函数的地址。每个对象在实例化时会有一个指向该表的vptr。 接下来需要考虑vptr的存储位置。根据引用[3]中的例子,Base类的对象b包含函数,因此对象的内存布局中会有一个vptr,指向Base的虚函数表。这个vptr通常位于对象内存布局的最前面,之后是类的成员变量。 继承情况下的vtable如何变化?例如,如果有一个派生类继承自基类,并重写函数,那么派生类的vtable会包含基类的函数,但被重写的函数地址会被替换为派生类的实现。引用[5]提到,造过程中vptr会从基类到派生类逐步更新。这解释了派生类对象在造时如何正确初始化vptr,确保多态调用时使用正确的函数。 动态多态的具体调用过程是怎样的?当通过基类指针或引用调用函数时,程序会通过对象的vptr找到对应的虚函数表,再根据函数在表中的位置调用正确的函数。例如,引用[2]中的例子,基类指针调用performTask和recharge时,实际执行的是派生类中的实现,这就是通过vtable查找到的。 还需要考虑函数的重要性。如果基类的函数不是函数,当通过基类指针删除派生类对象时,可能只会调用基类的函数,导致资源泄漏。因此,基类函数通常应声明为函数。 此外,虚函数表的结和内存布局可能受编译器实现的影响,但大多数编译器遵循相似的模式。每个类有自己的vtable,所有该类的对象共享同一个vtable。而每个对象有自己的vptr,指向对应的vtable。 可能的疑问点:多重继承下的vtable如何处理?这种情况下,每个基类可能有自己的vtable,派生类对象可能包含多个vptr,指向不同的vtable。这可能导致更复杂的内存布局,但基本机制仍然是使用vptr和vtable来实现动态绑定。 总结来说,动态多态的底层机制依赖于虚函数表虚函数表指针。编译器在编译时生成虚函数表,运行时通过对象的vptr找到正确的函数地址,实现多态调用。</think>### C++动态多态底层实现机制详解 C++动态多态通过**虚函数表(vtable)**和**虚函数表指针(vptr)**实现,以下是其核心原理的分步解: --- #### 1. **虚函数表(vtable)的生成** - **触发条件**:当类中声明至少一个函数时,编译器会为该类生成一个虚函数表(vtable)。 - **内容**:vtable是一个函数指针数组,存储该类所有函数的实际地址。例如: ```cpp class Base { public: virtual void func1() { /*...*/ } // 函数 virtual void func2() { /*...*/ } }; ``` 此时,`Base`类的vtable包含两个条目:`&Base::func1`和`&Base::func2`[^3]。 --- #### 2. **虚函数表指针(vptr)的存储** - **对象内存布局**:每个包含函数的类的实例化对象中,编译器会隐式插入一个指针`vptr`,指向对应的vtable。 - 例如,`Base b;`对象内存首部存储`vptr`,其后是成员变量(如`int _b`和`char _a`)[^3]。 - **继承关系下的vptr更新**: - 派生类造时,会从基类到派生类逐步更新`vptr`。例如,`Derived`类对象造时,先调用基类造函数(此时`vptr`指向基类vtable),再调用派生类造函数(`vptr`更新为指向派生类vtable)[^5]。 --- #### 3. **动态绑定的实现过程** 当通过**基类指针或引用**调用函数时: 1. 通过对象的`vptr`找到对应的vtable。 2. 从vtable中取出目标函数的地址。 3. 调用该地址对应的函数。 **示例**: ```cpp class CleaningRobot : public Robot { public: void performTask() override { /* 清洁逻辑 */ } // 重写基类函数 }; Robot* robot = new CleaningRobot(); robot->performTask(); // 实际调用CleaningRobot::performTask() ``` 此时,`robot->performTask()`会根据`CleaningRobot`对象的`vptr`找到派生类的vtable,并调用其函数实现[^2][^5]。 --- #### 4. **虚函数表的结(以单继承为例)** | 虚函数表条目 | 函数地址 | |--------------|------------------------| | 第0项 | `&CleaningRobot::performTask` | | 第1项 | `&CleaningRobot::recharge` | - **多态调用本质**:函数调用被编译为类似`(*(vptr[n]))(this)`的指令,其中`n`是函数在表中的索引。 --- #### 5. **多重继承的复杂场景** - **多个vptr**:若派生类继承自多个基类(每个基类有函数),则派生类对象会包含多个`vptr`,分别指向不同基类的vtable。 - **基类**:继承时可能引入额外的间接层(如基类指针),但核心仍依赖vtable机制。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值