首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >虚实穿梭:用C++多态解锁代码的“平行宇宙”(2)

虚实穿梭:用C++多态解锁代码的“平行宇宙”(2)

作者头像
用户11295429
发布2025-05-19 09:05:07
发布2025-05-19 09:05:07
12500
代码可运行
举报
文章被收录于专栏:王的博客专栏王的博客专栏
运行总次数:0
代码可运行

前言:

(请想象《哈利波特》的魔法课本自动翻开,飘出以下文字...)

"程序员阁下,您是否见过会自我进化的代码?" 在C++的奇幻大陆上,存在着这样一群神秘生物:

  • 🦄 普通虚函数:像会分身的魔法师,本体在基类,幻影在子类
  • 🔮 纯虚函数:则是封印在羊皮卷中的咒语原典,唯有继承者亲自补全才能唤醒力量
  • 🏛️ 抽象类:堪比奥林匹斯神殿,神明(基类)制定法则,凡人(子类)负责实现

        今天,我们将揭开多态魔法的终极奥义——用"抽象"绘制代码的星辰大海。这不是哲学课,而是一场让编译器颤抖的造物主之旅!

🔥 剧透预警:你即将掌握
  1. 纯虚函数的"灵魂契约"
    • 如何用=0符号在代码里刻下血契:"不实现此方法,休想实例化!"
    • 抽象类为何被称为"编程界的未完成交响乐"
  2. 多态原理的黑暗料理
    • 虚函数表(vtable)的"俄罗斯套娃"内存模型
    • 动态绑定的量子纠缠现象:基类指针->虚函数()如何触发时空跳跃
  3. 抽象类的降维打击
    • 设计模式中的"雅典学院":用抽象类构建框架宪法
    • Open/Closed原则的具象化身:对扩展开放,对修改关闭
🌰 先咬一口知识坚果
代码语言:javascript
代码运行次数:0
运行
复制
class 神之模具 {          // 抽象类
public:
    virtual void 创世() = 0; // 纯虚函数:必须被子类实现
};
​
class 女娲 : public 神之模具 {
public:
    void 创世() override { 
        cout << "捏土造人+补天套餐"; 
    }
};
​
class 盘古 : public 神之模具 {
public:
    void 创世() override {
        cout << "开天辟地+化身万物"; 
    }
};
​
// 客户端代码
神之模具* 创世神 = new 女娲();
创世神->创世(); // 输出:捏土造人+补天套餐

        这串代码藏着三个惊天秘密: 1️⃣ =0 是C++的达摩克利斯之剑,悬在子类头顶的强制实现令 2️⃣ 抽象类如同没有演员的剧本,只规定剧情走向 3️⃣ override 关键字是程序员与编译器的安全契约:"我在认真重写,不是手滑!"

1.纯虚函数和抽象类

        纯虚函数的定义还是比较简单的,只要在虚函数的后面写上“=0”,那么这个函数就是纯虚函数。纯虚函数不需要定义实现(实际上没啥意义,因为它最后还是会被派生类进行重写的,但是语法上明没有限制它,所以你想写定义实现还是可以滴),仅需声明就可以。包含纯虚函数的类叫做抽象类,抽象类是不可以实例化的(重点!),如果派生类在继承基类后不重写纯虚函数,那么派生类其实也是抽象类。其实小编认为,纯虚函数的定义和C++新增的override有点类似,一定程度上,纯虚函数强制了派生类重写函数,因为不重写无法实例化对象,下面小编先给各位展示一下纯虚函数的用法:

代码语言:javascript
代码运行次数:0
运行
复制
class Car
{
public:
    virtual  void Print() = 0; //这就是纯虚函数的写法,其实就是比一般的虚函数后面加了个=0
    //{
    //  cout << "请写车的品牌" << endl;    //定义可写可不写
    //}
};

        如果此时我们想要去调用抽象类,那么编译器会报错:

代码语言:javascript
代码运行次数:0
运行
复制
int main()
{
    Car s1;
    return 0;
}

        一般如果我们写了抽象类,一般都会有派生类去继承它,不然写这个也是没有意义的(大约在后期,我会在Linux用的它,敬请期待我后来的文章吧)。

代码语言:javascript
代码运行次数:0
运行
复制
class Benz: public Car
{
public:
    virtual void Print()
    {
        cout << "奔驰" << endl;
    }
};
class Ferrari
{
public:
    virtual void Print()
    {
        cout << "法拉利" << endl;
    }
};
int main()
{
    //Car s1;  // 用不了
    Benz s1;
    Ferrari s2; //只要重写了纯虚函数,派生类就可以实例化
    return 0;
}

        以上就是关于多态的纯虚函数和抽象类的相关介绍,难度不大,相信屏幕前的你可以轻松掌握~

2.多态的原理

2.1.虚函数表指针

        在讲虚函数表指针之前,各位读者可以思考一下,下面这个题目的结果是什么?

下面编译为32位程序的运行结果是什么?()

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

代码语言:javascript
代码运行次数:0
运行
复制
class wang
{
public:
    virtual void Func1()
    {
        cout << "测试用例" << endl;
    }
protected:
    int _b = 1;
    char c = 'w';
};
​
int main()
{
    wang s1;
    cout << sizeof(s1) << endl; 
    return 0;
}

        相信大部分学过内存对齐的读者看到这个题目,一看计算结构体的大小(类的内存对齐和结构体是一样的),此时一看32位的环境,默认对齐的字节为4字节,并且一个整型一个字符型,4+1==5,此时经过内存对齐以后,自然而然的就是8字节,所以高高兴兴的选了一个C。当然,小编当时第一眼看到这个题目的时候,和各位的思考逻辑是一样的,但是,这个题目出现在这个位置是有原因的,这个题目C是错误的,正确答案是:D

        可能到这里很多读者都懵了,这和我学的内存对齐不一样啊?当时我也是这么想的,但是这涉及到了一个新的知识点:虚函数表指针!在wang里面的成员中,除了已经设定好的_b和c,还有一个隐藏的__vfptr指针在这些成员的前面【和平台有关】,它也算是成员,并且它还是一个指针,指针的大小一般是跟随着编译器的环境走的,此时为32位的环境,所以这个指针大小是四字节,4+4+1 == 9,经过内存对齐以后,此时这个对象的大小就是12字节~这也算是一个小小的坑,这个指针我们通过debug以后才可以发现,如下图所示:

        在C++中,若一个类声明了虚函数(包括继承的虚函数),编译器会为该类的每个对象隐式添加一个虚函数表指针(vptr)。此指针指向该类对应的虚函数表(vtable)——一个由编译器生成的静态数组,其中按声明顺序存储了该类所有虚函数的实际地址。

2.2.多态的原理
2.2.1.多态是如何实现的
代码语言:javascript
代码运行次数:0
运行
复制
class Person
{
public:
    virtual void BuyTicket()
    {
        cout << "买票全价" << endl;
    }
};
​
​
class Student : public Person
{
public:
    virtual void BuyTicket()
    {
        cout << "买票半价" << endl;
    }
};
​
​
void Func(Person* ptr)
{
    ptr->BuyTicket();
}
​
​
int main()
{
    Person s1;
    Student s2;
    Func(s1);
    Func(s2);
    return 0;
}

        从底层的角度来看,此时的Func函数中的ptr->BuyTicket(),是如何作为ptr指向Person对象调用Person::BuyTicket,ptr指向Student对象调用Student::BuyTicket的呢?此时通过下图我们就可以知晓。

        在满足多态的条件以后,底层不再是编译时通过调用对象来确定函数的地址,而是在运行时的时候到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或者引用指向基类就调用基类的虚函数,指向派生类就调用派生类对应的虚函数,其调用的逻辑就是下面的两张图:

2.2.2.动态绑定和静态绑定
静态绑定(早绑定)
代码语言:javascript
代码运行次数:0
运行
复制
void non_virtual_func() { /* 非虚函数 */ }  
obj->non_virtual_func();  
  • 🔒 编译时锁定:函数地址直接写进二进制
  • 🚀 闪电速度:无运行时查找开销
  • 🧩 适用场景:普通函数调用、模板函数
动态绑定(晚绑定)
代码语言:javascript
代码运行次数:0
运行
复制
virtual void virtual_func() {}  
obj->virtual_func();  
  • 🕵️ 运行时寻址:通过vptr→vtable查找函数地址
  • 🛠️ 灵活多变:实现运行时多态
  • ⚖️ 性能代价:多一次指针寻址操作
2.2.3.虚函数表

        1.基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象是共用一张虚表的,不同类型的对象各自有各自独立的虚表。通过这个,我们可以知道,基类和子类各有一份自己的虚表,下图就可以知晓:

        尽管子类是继承基类,但是类型归根到底还是和基类不同的,所以各位要知道基类和子类是不同的虚表的。

        2.派生类是由两部分构成的,继承下来的基类和自己的成员,一般情况下,继承下来的基类中有虚函数表指针【上图就是】,自己就不会再生成虚函数表指针。但是要注意的这里继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同一个,就像基类对象的成员和派生类对象中的基类对象成员也是独立的。这个我也在上面细说了,各位要知道这一点。

        3.派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。如果不写的话,基类和子类的虚函数表中虚函数的地址是一样的(虚函数表不一样)。通过下面的代码和图各位就可以知晓这个定义。

代码语言:javascript
代码运行次数:0
运行
复制
class Teathcer :public Person
{
public:
};
int main()
{
    Person s1;
    Student s2;
    Teathcer s3;
    Func(s1);
    Func(s2);
    return 0;
}

        4.派生类的虚函数表中包含,(1)基类的虚函数地址,(2)派生类重写的虚函数地址完成覆盖,(3)派生类自己的虚函数地址三个部分。这里我也通过代码来让各位进行了解:

代码语言:javascript
代码运行次数:0
运行
复制
class Person
{
public:
    virtual void BuyTicket()
    {
        cout << "买票全价" << endl;
    }
    virtual void 忘梓()  //防伪认证
    {
        cout << "是不是很好奇为什么可以中文命名函数";
    }
};
​
​
class Student : public Person
{
public:
    virtual void BuyTicket()
    {
        cout << "买票半价" << endl;
    }
};

        可能不少读者对我上面代码中的忘梓函数感觉很疑惑:我怎么记着C语言不支持中文命名呢?其实我是悄咪咪的用了一个宏函数把函数的名字改成了中文~

        5.虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个0x00000000标记。(这个C++并没有进行规定,各个编译器自行定义的,vs系列编译器会再后面放个0x00000000标记,g++系列编译不会放),这个就不多讲解了。

        6.虚函数存在哪的?虚函数和普通函数⼀样的,编译好后是⼀段指令,都是存在代码段的,只是虚函数的地址又存到了虚表中。

        以上就是虚函数表的内容,各位大致了解就好,以后我们一般不会接触到它的(个人理解)。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-05-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言:
    • 🔥 剧透预警:你即将掌握
    • 🌰 先咬一口知识坚果
  • 1.纯虚函数和抽象类
  • 2.多态的原理
    • 2.1.虚函数表指针
    • 2.2.多态的原理
      • 2.2.1.多态是如何实现的
      • 2.2.2.动态绑定和静态绑定
      • 2.2.3.虚函数表
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档