当前位置:首页 » 《休闲阅读》 » 正文

C++ 多态:代码世界的神奇变身术

8 人参与  2024年12月13日 12:01  分类 : 《休闲阅读》  评论

点击全文阅读


? 快来参与讨论?,点赞?、收藏⭐、分享?,共创活力社区。?  


目录

一、多态概念:行为因 “对象” 而异

二、多态实现:规则与细节的交织

(一)构成要素

(二)虚函数的特殊情况

(三)C++11 关键字助力

(四)重载、重写与隐藏的辨析

三、抽象类:接口的蓝图

四、多态原理:背后的神秘力量

(一)虚函数表的奥秘

(二)多态的动态绑定

(三)动态与静态绑定的抉择

五、继承中的虚函数表:单继承与多继承的故事

(一)单继承的有序传承

(二)多继承的复杂分支

六、总结


在 C++ 的编程天地里,多态宛如一场奇妙的魔法秀,让程序充满了灵动与变幻。?

(文章篇幅较长,耐心阅读,受益匪浅)

一、多态概念:行为因 “对象” 而异

想象一下,在一个购票的场景中,有 Person 类、Student 类和 Soldier 类。Person 类的 BuyTicket 函数是全价购票,Student 类重写该函数实现半价购票,Soldier 类则重写为优先购票。这就像同一个舞台上,不同的演员(对象)表演相同的节目(行为),却有着截然不同的呈现效果,这就是多态的魅力。它使得程序能够依据对象的 “身份”,灵活地展现出多样的行为模式。


二、多态实现:规则与细节的交织

(一)构成要素

要开启多态的魔法,需满足两个关键条件。首先,得借助基类的指针或引用去召唤虚函数。比如,有一个 Person* 指针,它可以指向 Person 对象,也能指向 Student 对象。当通过这个指针调用 BuyTicket 函数时,就可能触发不同的购票行为。其次,被调用的函数必须是被 virtual 加持的虚函数,且派生类要对其进行重写。

class Person {public:    virtual void BuyTicket() { cout << "全价购票" << endl; }};class Student : public Person {public:    virtual void BuyTicket() { cout << "半价购票" << endl; }};

 在这个例子中,Student 类精准地重写了 Person 类的 BuyTicket 虚函数,为多态的实现奠定了基础。

(二)虚函数的特殊情况

虚函数的重写有两个有趣的例外。

协变就像是一种特殊的魔法变身,当基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或引用时,即便返回值类型看似不同,也能实现重写。
#include <iostream>using namespace std;// 定义基类Baseclass Base {public:    // 基类中的虚函数,返回基类对象的指针    virtual Base* func() {        // 输出表明此函数被调用的信息        cout << "Base::func() called" << endl;        // 返回指向当前对象的指针(this指针)        return this;    }};// 定义派生类Derived,公有继承自Base类class Derived : public Base {public:    // 派生类重写基类的虚函数,返回派生类对象的指针,符合协变规则实现重写    virtual Derived* func() override {        // 输出表明此函数被调用的信息        cout << "Derived::func() called" << endl;        // 返回指向当前对象的指针(this指针)        return this;    }};int main() {    // 通过基类指针指向派生类对象    Base* basePtr = new Derived();    // 调用func函数,会根据对象实际类型调用派生类中重写后的func函数    basePtr->func();    // 创建派生类对象并通过派生类指针指向它    Derived* derivedPtr = new Derived();    // 调用func函数,调用派生类中重写后的func函数    derivedPtr->func();    // 释放之前动态分配的内存,避免内存泄漏    delete basePtr;    delete derivedPtr;    return 0;}

而析构函数的重写更是别具一格,如果基类析构函数是虚函数,那么派生类析构函数只要存在,无论是否明确标注 virtual,都能与基类析构函数构成重写。这就像是一种默契的约定,确保在对象生命周期结束时,资源能被正确释放。
class Base {public:    virtual ~Base() { cout << "Base 析构" << endl; }};class Derived : public Base {public:    virtual ~Derived() { cout << "Derived 析构" << endl; }};

这里,Derived 类的析构函数与 Base 类的析构函数实现了重写,保障了对象销毁时的资源清理工作?。 

 

(三)C++11 关键字助力

C++11 带来的 override  final 关键字,如同编程世界的守护精灵?。override 会仔细检查派生类虚函数是否正确重写基类虚函数,若有偏差,编译时便会及时提醒,就像一个严谨的导师,防止我们在重写过程中犯错?。final 则像是一道坚固的封印,它修饰的虚函数将不能被派生类再次重写,为代码的稳定性保驾护航?。

class Vehicle {public:    virtual void Move() = 0;};class Car : public Vehicle {public:    virtual void Move() override { cout << "汽车行驶" << endl; }};class SuperCar : public Car {public:    // 以下代码会报错,因为 Car 类中的 Move 函数已被 final 修饰    // virtual void Move() { cout << "超级汽车极速行驶" << endl; } };

(四)重载、重写与隐藏的辨析

重载就像是一群同名的小伙伴,它们在同一作用域内,通过不同的参数列表来区分彼此。比如有多个 add 函数,一个接受整数相加,一个接受浮点数相加。重写则是派生类对基类虚函数的深度定制,除了协变情况,函数签名完全一致,并且通过基类指针或引用调用时能实现动态绑定。而隐藏就像是派生类的一个小 “任性”?,当派生类定义了与基类同名的非虚函数时,派生类对象调用该函数就会优先选择自己的版本,基类的同名函数仿佛被藏了起来。
class Parent {public:    void print() { cout << "Parent 打印" << endl; }    virtual void show() { cout << "Parent 展示" << endl; }};class Child : public Parent {public:    void print() { cout << "Child 打印" << endl; }  // 隐藏了 Parent 类的 print 函数    virtual void show() { cout << "Child 展示" << endl; }  // 重写了 Parent 类的 show 函数};

三、抽象类:接口的蓝图

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。

抽象类就像是建筑的蓝图?,它包含纯虚函数,自身无法构建出具体的 “建筑”(对象)。

例如,有一个 Shape 抽象类,它有纯虚函数 DrawCircle 类和 Rectangle 类继承自 Shape 类,并实现了 Draw 函数,这样它们才能被实例化,成为具体的图形对象。抽象类强制派生类遵循特定的接口规范,实现了接口继承,与普通函数的实现继承有着本质区别。如果不需要多态的灵动性,就不要随意将函数定义为虚函数,以免增加代码的复杂度和性能开销。

// 定义抽象基类Shape,用于表示图形的抽象概念class Shape {public:    // 定义纯虚函数Draw,意味着Shape类是抽象类,不能实例化对象,派生类必须重写这个函数来提供具体的绘制逻辑    virtual void Draw() = 0;};// 定义Circle类,它公有继承自Shape类,表示圆形,是一种具体的图形class Circle : public Shape {public:    // 重写基类Shape的纯虚函数Draw,实现具体的绘制圆形的逻辑,这里简单地输出提示信息表示绘制圆形这个操作    virtual void Draw() { cout << "绘制圆形" << endl; }};// 定义Rectangle类,它公有继承自Shape类,表示矩形,是一种具体的图形class Rectangle : public Shape {public:    // 重写基类Shape的纯虚函数Draw,实现具体的绘制矩形的逻辑,这里简单地输出提示信息表示绘制矩形这个操作    virtual void Draw() { cout << "绘制矩形" << endl; }};int main() {    Shape* shape1 = new Circle();    Shape* shape2 = new Rectangle();    shape1->Draw();    shape2->Draw();    delete shape1;    delete shape2;    return 0;}


四、多态原理:背后的神秘力量

(一)虚函数表的奥秘

// 这里常考一道笔试题:sizeof(Base)是多少?class Base{public:    virtual void Func1()    {        cout << "Func1()" << endl;    }private:    int _b = 1;};

 通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。

在 C++ 的神秘世界里,当一个类拥有虚函数时,它的对象内部会悄悄隐藏一个虚函数表指针(__vfptr)。这个指针如同指向神秘宝藏图(虚函数表)的指南针?。虚函数表则是一个装满虚函数指针的魔法盒子,通常盒子的最后会放置一个 nullptr 作为标记。

?派生类继承基类时,先复制基类虚函数表内容,重写则更新对应虚函数指针,其新定义虚函数按序添至虚函数表末尾。虚函数存于代码段,虚函数表通常也在代码段(各平台或有差异),对象中仅存放虚函数表指针。

#include <iostream>using namespace std;// 定义基类Baseclass Base {public:    // 定义虚函数func1,它是Base类的虚函数,会被派生类可能重写,用于演示虚函数相关机制    virtual void func1() {        cout << "Base::func1() called" << endl;    }    // 定义虚函数func2,同样是Base类的虚函数,用于演示虚函数相关机制    virtual void func2() {        cout << "Base::func2() called" << endl;    }};// 定义派生类Derived,公有继承自Base类,用于展示继承过程中虚函数表的变化情况class Derived : public Base {public:    // 重写基类Base的虚函数func1,体现重写时虚函数表中对应指针的更新机制    virtual void func1() override {        cout << "Derived::func1() called" << endl;    }    // 派生类Derived新定义的虚函数func3,用于演示新虚函数添加到虚函数表末尾的情况    virtual void func3() {        cout << "Derived::func3() called" << endl;    }};int main() {    // 通过基类指针指向派生类对象,利用多态机制调用虚函数,这里将展示虚函数表指针查找虚函数表及调用对应虚函数的过程    Base* basePtr = new Derived();    // 调用func1函数,由于Derived类重写了func1,会调用到Derived类中重写后的版本,通过虚函数表指针找到更新后的虚函数指针来调用    basePtr->func1();      // 调用func2函数,因为Derived类没有重写func2,会调用Base类中的func2版本,通过虚函数表指针找到对应Base类中func2的虚函数指针来调用    basePtr->func2();      // 通过派生类指针指向派生类对象,方便调用派生类自己定义的虚函数(如func3)    Derived* derivedPtr = new Derived();    // 调用派生类Derived新定义的虚函数func3,通过虚函数表指针找到对应的虚函数指针来调用    derivedPtr->func3();    // 释放之前通过new分配的内存,避免内存泄漏    delete basePtr;    delete derivedPtr;    return 0;}

希望通过这些注释能让你更清晰地理解代码与虚函数表等相关概念之间的关联。

(二)多态的动态绑定

多态的实现依赖于虚函数表和虚函数表指针的精妙配合。当通过基类指针或引用调用虚函数时,程序会在运行时开启一场神秘的探索之旅?。它会依据指针所指向对象的实际类型,借助虚函数表指针找到对应的虚函数表,然后在其中精准地找到并调用相应的虚函数。就像根据宝藏图找到宝藏一样,不同的对象会引导程序走向不同的虚函数 “宝藏”,从而实现多态的动态绑定。例如,有一个 Animal 基类和 CatDog 派生类,它们都有 MakeSound 虚函数。通过 Animal* 指针指向不同的对象,就能在运行时动态地调用相应的 MakeSound 函数,展现出不同动物的叫声??。

class Animal {public:    virtual void MakeSound() { cout << "动物叫声" << endl; }};class Cat : public Animal {public:    virtual void MakeSound() { cout << "喵呜~" << endl; }};class Dog : public Animal {public:    virtual void MakeSound() { cout << "汪汪汪!" << endl; }};void AnimalSound(Animal* animal) {    animal->MakeSound();}int main() {    Animal* cat = new Cat();    Animal* dog = new Dog();    AnimalSound(cat);    AnimalSound(dog);    delete cat;    delete dog;    return 0;}

 

(三)动态与静态绑定的抉择

动态绑定(后期绑定)如同一位灵活的舞者?,在程序运行的舞台上,根据对象的实际类型即兴创作,决定函数的调用。而静态绑定(前期绑定)则像一位严谨的舞者,在编译的排练室里就确定好了舞蹈动作(函数调用),比如函数重载就是静态绑定的典型。函数重载在编译时根据函数名和参数类型就确定了唯一的调用路径,而动态绑定则将这个决策推迟到运行时,根据对象的动态类型灵活选择,二者各有千秋,为程序的设计提供了丰富的选择。


五、继承中的虚函数表:单继承与多继承的故事

需要注意的是在单继承和多继承关系中,下面我们去关注的是派生类对象的虚表模型 

(一)单继承的有序传承

在单继承的故事里,派生类对象的虚函数表构建就像是一场有序的家族传承?‍?。

派生类会先接过基类的虚函数表 “衣钵”,将其内容完整拷贝。若派生类对基类虚函数进行了重写,就会在自己的虚函数表中更新对应的虚函数指针,仿佛在家族技艺上进行了创新。同时,派生类自己新创的虚函数会依次添加到虚函数表的末尾,续写家族的辉煌。

有时,编译器的监视窗口可能会隐藏部分函数,这时我们可以编写代码来揭开虚函数表的神秘面纱,打印出其中的函数指针并调用,一探究竟。

#include <iostream>#include <cstdio>using namespace std;// 定义基类Baseclass Base {public:    virtual void func1() {        cout << "Base::func1" << endl;    }    virtual void func2() {        cout << "Base::func2" << endl;    }private:    int a;};// 定义派生类Derive,公有继承自Base类class Derive : public Base {public:    virtual void func1() {        cout << "Derive::func1" << endl;    }    virtual void func3() {        cout << "Derive::func3" << endl;    }    virtual void func4() {        cout << "Derive::func4" << endl;    }private:    int b;};// 定义函数指针类型VFPTR,指向无参数且返回值为void的函数,用于操作虚函数表中的指针typedef void(*VFPTR)();// 函数用于打印虚函数表的相关信息,包括虚函数表地址以及表中每个虚函数的地址和调用该虚函数void PrintVTable(VFPTR vTable[]) {    // 输出虚函数表的地址    cout << " 虚表地址> " << vTable << endl;    // 循环遍历虚函数表,直到遇到空指针(表示虚函数表结束)    for (int i = 0; vTable[i]!= nullptr; ++i) {        // 输出当前虚函数在虚函数表中的序号以及其地址(十六进制格式),还有一个提示箭头,表示接下来要调用这个虚函数了        printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);        VFPTR f = vTable[i];        f();    }    cout << endl;}int main() {    Base b;    Derive d;    // 获取Base类对象b的虚函数表指针,步骤如下:    // 1. 先取b的地址,强转成一个int*的指针    // 2. 再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚函数表的指针    // 3. 再强转成VFPTR*,因为虚函数表就是一个存VFPTR类型(虚函数指针类型)的指针数组    // 4. 将虚函数表指针传递给PrintVTable进行打印虚函数表    // 注意:打印虚函数表的代码有时可能会崩溃,因为编译器对虚函数表的处理可能不干净,虚函数表最后没放nullptr导致越界,    //       可通过清理解决方案后再编译来尝试解决该问题    VFPTR* vTableb = (VFPTR*)(*(int*)&b);    PrintVTable(vTableb);    // 获取Derive类对象d的虚函数表指针,同样进行类型转换等操作后传递给PrintVTable打印其虚函数表信息    VFPTR* vTabled = (VFPTR*)(*(int*)&d);    PrintVTable(vTabled);    return 0;}

(二)多继承的复杂分支

多继承的情况则像是一个庞大家族的复杂分支?。派生类会从多个基类那里继承虚函数表,对于未被重写的虚函数,它们会分别在对应的基类部分的虚函数表中找到自己的位置。例如,有 BaseABaseB 两个基类,Derived 类多继承自它们。Derived 类重写的虚函数会在相应的虚函数表中更新指针,而新定义的虚函数则会按照一定规则添加到某个基类虚函数表的末尾。这其中的规则较为复杂,需要我们仔细梳理和理解。

class BaseA {public:    virtual void FuncX() { cout << "BaseA::FuncX" << endl; }    virtual void FuncY() { cout << "BaseA::FuncY" << endl; }};class BaseB {public:    virtual void FuncX() { cout << "BaseB::FuncX" << endl; }    virtual void FuncZ() { cout << "BaseB::FuncZ" << endl; }};class Derived : public BaseA, public BaseB {public:    virtual void FuncX() { cout << "Derived::FuncX" << endl; }    virtual void FuncW() { cout << "Derived::FuncW" << endl; }};typedef void(*VFPTR)();void PrintVTable(VFPTR vTable[]) {    cout << "虚表地址: " << vTable << endl;    for (int i = 0; vTable[i]!= nullptr; ++i) {        printf("第 %d 个虚函数地址: 0X%x, -> ", i, vTable[i]);        VFPTR f = vTable[i];        f();    }    cout << endl;}int main() {    Derived derivedObj;    // 打印 BaseA 部分的虚函数表    VFPTR* vTableBaseA = (VFPTR*)(*(int*)&derivedObj);    PrintVTable(vTableBaseA);    // 打印 BaseB 部分的虚函数表    VFPTR* vTableBaseB = (VFPTR*)(*(int*)((char*)&derivedObj + sizeof(BaseA)));    PrintVTable(vTableBaseB);    return 0;}

在这个多继承的例子中,我们可以看到 Derived 类对象的虚函数表在不同基类部分的分布与变化情况。不过,菱形继承和菱形虚拟继承由于其复杂性和性能损耗问题,在实际编程中应谨慎使用,这里就不再详细展开它们的虚函数表情况啦。


六、总结

掌握这些关于继承和多态的知识,无论是应对面试还是提升自己的编程能力,都能让我们在 C++ 的编程道路上更加自信地前行,轻松解决各种编程挑战,构建出更出色的软件系统。多态就像一颗璀璨的编程之星?,照亮我们在代码世界里探索的道路,让我们能够创造出更加精彩、富有变化和扩展性的程序作品。?


如果在学习过程中有任何疑问或建议,欢迎随时交流分享哦?! ?【A Charmer】

 


点击全文阅读


本文链接:http://zhangshiyu.com/post/200568.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1