C++多态

多态

1.多态的定义及实现

多态指同一行为呈现多种形态,分 编译时(静态)和运行时(动态) 两类。

编译时多态靠函数重载模板实现:同一函数名,因参数类型、数量不同,编译时就确定调用哪个版本,比如print(int)和print(string)会被区分。

运行时多态则通过继承虚函数实现:基类指针指向派生类对象时,调用虚函数会根据实际对象类型,在程序运行时确定执行派生类还是基类的实现,比如Animal指针调用eat(),实际执行Cat或Dog的具体方法。

两者核心区别在确定调用版本的时机:前者在编译期,后者在运行期。

1.1构成条件

多态是一个继承关系下的类对象去调用同一函数,产生了不同的行为。

两个重要条件:

  • 存在基类和派生类的继承结构
  • 派生类重写基类中的虚函数
  • 通过基类的指针或引用调用虚函数

在这里插入图片描述

1.2虚函数

类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数。

  • 虚函数声明允许派生类重写实现多态
  • 如果没有虚函数声明,派生类的同名函数不是重写,而是隐藏。
  • C++11引入的override关键字明确用于强制检查是否是重写了基类的虚函数,如果基类中不是虚函数会报错。
  • 虚函数的核心作用是支持多态,而不是控制是否允许重写。
  • 虚函数的具有传递性。当基类声明了虚函数,派生类重写该函数时,即使不显式添加virtual关键字,该函数仍然会被视为虚函数,并且可以被进一步的派生类继续重写。
  • 函数被声明为 virtual,启用了动态绑定机制,使得运行时能够根据对象的实际类型(B)来决定调用哪个版本的函数。

1.3动态绑定(查虚函数表)

实现原理:

  1. 虚函数表
    • 每个包含虚函数的类(或其派生类)会有一个虚函数表,这是一个存储函数指针的数组,记录了该类所有虚函数的地址
    • 基类和派生类各自维护自己的虚函数表。如果派生类重写了基类的虚函数,派生类的虚函数表中会替换为自身的函数地址;未重写的虚函数则沿用基类的地址
  2. 虚指针
    • 每个包含虚函数的类的对象会隐含一个指向其类的虚函数表的指针(虚指针),通常在对象内存布局的最开始位置。
    • 当创建对象时,虚指针会被自动初始化,指向所属类的虚函数表。
  3. 动态绑定过程
    • 当通过基类指针或引用调用虚函数时,程序会先通过对象的虚指针找到对应的虚函数表。
    • 再根据函数在虚函数表中的索引找到具体的函数地址,最终调用该地址对应的函数(可能是基类或派生类的实现)。
    • 这个过程在运行时完成,因此称为动态绑定(或运行时绑定)。

1.4函数重写

对基类虚函数进行重写才是真正的重写。

函数重写的本质是虚函数表中函数地址的动态替换

编译器会为包含虚函数的类创建虚函数表(vtable),重写函数会替换派生类虚表中对应位置的函数地址。运行时通过对象的虚表指针(vptr)找到实际应调用的函数版本。

1.4.1函数重写中的协变

协变(Covariant) 是指派生类重写基类虚函数时,返回值类型可以与基类虚函数的返回值类型形成 “父子关系”(即派生类返回值类型是基类返回值类型的派生类),这种情况仍然被视为有效的重写。

即允许其他条件满足函数重写,返回值构成父派生类型,也是函数重写。但这样的返回值必须是指针或引用,值类型的是错误的。

#include <iostream>

// 基类
class Base {
public:
    // 基类虚虚函数,返回Base*
    virtual Base* clone() {
        std::cout << "Base::clone()" << std::endl;
        return new Base();
    }
    virtual ~Base() {} // 虚析构函数,确保正确析构
};

// 派生类
class Derived : public Base {
public:
    // 重写clone(),返回Derived*(Base*的派生类指针)
    Derived* clone() override { // 协变返回值
        std::cout << "Derived::clone()" << std::endl;
        return new Derived();
    }
};
int main() {
    Base* b = new Derived();
    Base* copy = b->clone(); // 调用Derived::clone(),返回Derived*(隐式转换为Base*)
    delete copy;
    delete b;
    return 0;
}
1.4.2析构函数的重写

基类析构函数必须声明为虚函数,此时派生类析构函数就会与基类析构函数构成重写。

虽然基类与派生类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,所以基类的析构函数加了virtual修饰,派生类的析构函数就构成重写。

这就解释了派生类B中显式调用基类A的析构函数~A()调不到,原因是两者构成隐藏,得加上类域。不过一般不这么做。

1.4.3 override 和 final 关键字

虚函数重写时函数名写错参数写错等导致无法构成重写,而这种错误在编译期间是不会报出的,只有在程序运行时才会发现。C++11提供了override关键字,可以强制检测是否构成重写。

class Car {
public:
    virtual void Drive() {}
};
class AE86 {
public:
    virtual void Drive() override {cout << "漂一下啊" << endl;}
};

C++11提供的final关键字不仅可以声明一个类不再被允许继承

class Person final {};

还可以声明一个函数不再被允许重写

class Person {
public:
    void func() final {}
};
class Student {
public:
    void func() override {...} // 报错
};

1.5静态绑定(直接确定函数地址)

静态绑定又称编译时绑定,是指函数调用在程序编译阶段就确定了具体要执行的函数版本,而无需等到运行时再做决定。这种绑定方式依赖于编译器在编译时对函数调用的解析,主要基于调用者的类型(而非对象的实际类型)。

核心是编译器在编译阶段通过函数名、参数列表、调用者类型等信息,直接确定要调用的函数地址,并生成对应的机器码。由于绑定过程在编译时完成,运行时不会产生额外的开销(如查找虚函数表的操作)。

默认参数数值在编译时静态绑定,与虚函数动态绑定机制分离。

1.6选择题

以下程序输出结果是什么()

A: A->0

B: B->1

C: A->1

D: B->0

E: 编译出错

F: 以上都不正确

class A{
public:
    virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
    virtual void test(){ func();}
};
class B : public A{
public:
	void func(int val = 0){ std::cout<<"B->"<< val <<std::endl; }
};
int main(){
	B*p = new B;
    p->test();
    p->func();
	return 0;
}

在这里插入图片描述

  1. 虚函数表和虚指针的构建

    虚函数表结构:

    • A 类的虚函数表:
      • func() → A::func() 地址
      • test() → A::test() 地址
    • B 类的虚函数表:
      • func() → B::func() 地址 (重写)
      • test() → A::test() 地址 (继承)

    每个对象包含一个指向对应类虚函数表的虚指针(vptr)。

  2. p->test()执行过程

    B* p = new B;
    

    创建 B 对象,该对象包含指向 B 类虚函数表的虚指针。

    p->test() 执行过程:

    1. 通过指针 p 访问对象的虚指针(vptr)
    2. 通过vptr找到 B 类的虚函数表
    3. 在虚函数表中查找 test() 对应的函数地址,发现是 A::test()
    4. 调用 A::test(),在该函数内部执行 func()

    在**A::test()内部调用func()**时

    1. 仍然是通过当前对象(this指针)的虚指针进行查找

    2. this指针指向的是 B 对象,所以使用 B 类的虚函数表

    3. 在 B 类虚函数表中查找到 func() 对应的是 B::func()

    4. 调用 B::func(),但使用编译时确定的默认参数值 1(来自 A::func() 的声明,原因是默认参数数值在编译时静态绑定,与虚函数动态绑定机制分离

    因此输出:B->1

  3. p->func()执行过程

    1. 通过指针p访问对象的虚指针(vptr)
    2. 通过vptr找到B类的虚函数表
    3. 在虚函数表中查找func()对应的虚函数地址,是B::func()
    4. 直接调用B::func(),使用B::func()声明中的默认参数值0

    因此输出:B->0

  4. 最终输出结果

    B->1
    B->0
    

1.7重载/重写/隐藏的对比

在这里插入图片描述

2.纯虚函数和抽象类

2.1纯虚函数

纯虚函数是在基本类中声明但没有具体实现的虚函数。

virtual 返回类型 func(参数列表) = 0;
  • 纯虚函数通过在声明末尾添加=0 来标识
  • 纯虚函数没有函数体,它的作用是为派生类提供一个必须实现的接口规范
  • 包含纯虚函数的类被称为抽象类。

2.2抽象类

含有纯虚函数的类被称为抽象类,具有以下特点:

  1. 抽象类不能被实例化,即不能创建抽象类的对象。

    class abstractClass {
    public:
        virtual void func() = 0; // 纯虚函数
    };
    
    // 错误:不能实例化抽象类
    abstractClass obj; // 编译错误
    
  2. 抽象类的派生类必须实现所有纯虚函数才能称为非抽象类(可实例化)

    class normalClass : public abstractClass {
    public:
        void func() override {
            cout << "实现纯虚函数" << endl;
        }
    };
    
    // 正确:normalClass是具体类,可以实例化
    normalClass obj;
    
  3. 抽象类可以包含普通成员函数和成员变量

  4. 可以声明抽象类的指针或引用,用于实现多态。

// car.h
#include <string>
#include <iostream>
using namespace std;
class Car {
public:
    virtual void run() const = 0;
    virtual void stop() const = 0;
    Car() = default;
    Car(const Car& car) = default;
    Car(const char* name, const char* carId, const int price, const int speed)
        : _name(name), _carId(carId), _price(price), _speed(speed) {
    }
    virtual ~Car() = default;
protected:
    string _name;
    string _carId;
    int _price = 0;
    int _speed = 0;
};
class Bmw final : public Car {
public:
    Bmw() = default;
    Bmw(const char* name, const char* carId, const int price, const int speed)
        : Car(name, carId, price, speed) {
    }
    Bmw(const Bmw& bmw) = default;
    void run() const override {
        cout << "Bmw is running" << endl;
    }
    void stop() const override {
        cout << "Bmw is stoping" << endl;
    }
    ~Bmw() override = default;
};
class Audi final : public Car {
public:
    Audi() = default;
    Audi(const char* name, const char* carId, const int price, const int speed)
        : Car(name, carId, price, speed) {}
    Audi(const Audi& audi) = default;
    void run() const override {
        cout << "Audi is running" << endl;
    }
    void stop() const override {
        cout << "Audi is stoping" << endl;
    }
    ~Audi() override = default;
};

void run(const Car& car) {car.run();} // 多态
// main.cpp
#include "car.h"
int main(){
    const Car* car = new Bmw("Bmw", "BMW-X5", 1000000, 200);
    const Audi audi("Audi", "AUDI-A6", 500000, 180);
    
    car->run();
    run(audi);
    
    delete car;
    return 0;
}

在这里插入图片描述

3.多态的原理

3.1虚函数表和虚函数表指针

下⾯编译为64位程序的运行结果是什么() B

A. 编译报错 B. 运行报错 C. 8 D. 12 E. 16

class Base {
public:
    virtual ~Base() = default;
    virtual void func() {
        cout << "Base" << endl;
    }
protected:
    int _i = 0;
    char _c = 'a';
};
int main(){
    Base base;
    cout << sizeof(base) << endl;
    return 0;
}
  1. 成员变量的大小与对齐

    • int _i:4 字节(对齐要求为 4 字节)
    • char _c:1 字节(对齐要求为 1 字节)

    这两个成员变量本身共占用4 + 1 = 5字节,但由于内存对齐规则:

    • 成员变量需按各自对齐值排列
    • 整体需满足类的最大对齐要求(此处为虚函数表指针的 8 字节对齐)

    因此,_i_c实际占用 8 字节(包含 3 字节填充,用于满足 8 字节对齐)。

  2. 虚函数表指针(vptr)

    Base类中包含虚函数,类在实例化时,编译器会为对象添加一个指向虚函数表的指针。

    这个指针的类型是函数指针数组指针,是一个指向虚函数表的指针。

    虚函数表是一个数组,存放着类里面每个虚函数的函数指针。

    64位下指针的大小为8字节

    在这里插入图片描述

因此总大小为16字节。

再看上面的题:

class A {
public:
    virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
    virtual void test() { func(); }
};
class B : public A {
public:
    void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main() {
    A a;
    B* p = new B;
    p->test();
    p->func();
    return 0;
}

在这里插入图片描述

3.2多态的原理

多态的原理也就是虚函数表里函数指针指向谁调用谁。

虚函数表

  • 基类对象的虚函数表中存放基类所有的虚函数指针。

  • 每个包含虚函数的类(或其派生类)会有一个虚函数表,这是一个存储函数指针的数组,记录了该类所有虚函数的地址

  • 同类对象共享一个虚函数表。

    在这里插入图片描述

  • 基类和派生类各自维护自己的虚函数表。如果派生类重写了基类的虚函数,派生类的虚函数表中会替换为自身的函数地址;未重写的虚函数则沿用基类的地址

  • 没有重写的普通派生类,和基类的虚函数表按理上是一样的,但是不是同一个表

    在这里插入图片描述

  • **虚函数存在哪里?**虚函数和普通函数一样,编译好是一段指令,都是存在代码段,虚表中存放的是虚函数指针。

  • **虚函数表存在哪里?**具体没有规定,和编译器有关。

3.3动态绑定和静态绑定

  • 对不满足多态条件(指针或者引用+调用虚函数)的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定

  • 满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数的地址,也就做动态绑定

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

等你涅槃重生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值