这样解决了第一个问题

发布时间:2025-06-24 20:07:22  作者:北方职教升学中心  阅读量:538


重点

  • 虚函数表指针:每个包含虚函数的类都会有一个虚表指针,占用4字节,用于在对象中指向虚函数表,支持运行时的多态。

    classPerson{public://多态实现的本质,调用虚函数//1、
  • 抽象类:包含纯虚函数的类,不能直接实例化,通常用作接口或基类。
  • func1指向Base::func1

    classCar{public://使用final不能被重写virtualvoidDrive()final	{}};classBenz:publicCar{public://检查是否完成了重写,在编译时进行检查virtualvoidDrive()override	{cout <<"Benz"<<endl;}};

    在这个代码中Drive函数被final修饰所以不能被重写/覆盖,同时在派生类中邪了override关键字用来检查函数时候被重写/覆盖,这样就会报错。

  • 由于内存对齐,_ch后面会填充3字节,使得成员变量部分占用8字节。

  • 🚀 2.1.2 虚函数

    类的成员函数前面加上virtual修饰才能称为是虚函数,不是成员函数不能加virtual修饰

    classPerson{public:virtualvoidBuyTicket(){cout<<"买票-全价"<<endl;}};

    🚀 2.1.3 虚函数的重写/覆盖

    虚函数的重写/覆盖:派生类中有一个跟基类完全相同的虚函数,我们这里说的完全相同是指虚函数的函数名,参数类型,返回值类型三个完全相同。

  • 抽象类可以包含已实现的普通成员函数,但这不会改变其抽象类的特性。

  • 📌 2 多态的定义及实现

    ✨ 2.1 实现多态所需要的条件

    多态是继承关系下的类对象,在调用同一函数的时候所产生的不同的行为

    🚀 2.1.1 实现多态的两个重要条件

    1. 必须指针或者引用调用函数

      要实现多态效果,第一必须是基类的指针或者引用,因为只有基类的指针和引用才能即指向派生类对象,又指向基类对象。

      📌1 多态

      ✨ 1.1 多态的概念

      多态的概念:通俗来讲就是多种状态,多态编译分为运行时多态(动态绑定)和编译时多态(静态绑定)我们可以发现 test中并没有传入参数但是我们忽略了作为一个成员函数总是有一个 this指针,并且是父类的 this指针,所以我们可以认为是 A类型的 this指针来调用的 func函数

虚函数表的优点

  • 实现多态:虚函数表是C++实现运行时多态的核心,允许程序在运行时根据对象的实际类型选择函数。
    然而多态不仅限于简单的继承和重写,它涉及虚函数表,动态绑定,菱形继承,虚继承等。
  • 派生类继承该类时必须实现纯虚函数,否则派生类也会成为抽象类。

对于基类和派生类,编译器会为每个类单独生成虚函数表。
  • 总内存占用

    • 成员变量8字节 + 虚表指针4字节 = 总共12字节。Derived::func3的地址。

    • 调用过程:当通过基类指针或引用调用虚函数时,编译器会先通过对象的虚表指针(vptr)找到虚函数表(vtable),然后在虚函数表中查找并调用对应的虚函数,实现多态。

  • 示例

    classAnimal{public:virtualvoidsound()=0;// 纯虚函数voidsleep(){std::cout <<"Animal is sleeping"<<std::endl;}};classDog:publicAnimal{public:voidsound()override {std::cout <<"Woof!"<<std::endl;}};intmain(){// Animal a; // 错误!无法实例化抽象类Dog d;d.sound();// 输出: Woof!d.sleep();// 输出: Animal is sleepingreturn0;}

    在该例子中,Animal是一个抽象类,它定义了一个纯虚函数sound

  • 特点
    • 纯虚函数没有函数体。
    • 抽象类只能作为基类使用,不能直接创建对象。同时,抽象类Animal也可以包含一个普通函数sleep,并且可以在派生类中使用。

    • 虚表指针(vptr):每个类的对象会在内存中包含一个指向其类对应虚函数表的指针,称为虚表指针(vptr)。ptr->BuyTicket();}voidFunc(Person&ptr){//虽然都是父类的指针在调用该函数,但是具体的实现是由ptr指向的对象实现的。

    • char类型的成员变量 _ch占用1字节。其声明格式为:
      virtualvoidfunctionName()=0;
      其中= 0表示该函数是纯虚函数。
  • 示例

    classShape{public:virtualvoiddraw()=0;// 纯虚函数};classCircle:publicShape{public:voiddraw()override {std::cout <<"Drawing Circle"<<std::endl;}};classSquare:publicShape{public:voiddraw()override {std::cout <<"Drawing Square"<<std::endl;}};intmain(){Shape*s1 =newCircle();Shape*s2 =newSquare();s1->draw();// 输出: Drawing Circles2->draw();// 输出: Drawing Squaredeletes1;deletes2;return0;}

    在上面的例子中,Shape类是一个基类,它定义了一个纯虚函数draw

  • 任何包含纯虚函数的类都无法直接实例化。
  • 虚表指针是对象级别的:每个对象实例都有自己的虚表指针,用于指向所属类的虚表。

    接下来我们来看 为什么继承的函数还是使用基类的缺省值

    🚀 2.1.5 override和final关键字

    C++11提供了override,可以帮助⽤⼾检测是否重写。

  • func2指向Base::func2
  • 目的:纯虚函数通常用于定义一个接口或规范,要求派生类必须提供该函数的具体实现。当一个类中包含虚函数时,编译器会自动为这个类生成一个虚函数表,用于存储类的虚函数地址。这个指针在对象创建时自动初始化,以指向该对象所属的类的虚函数表。

    ✨总结

    • 纯虚函数:只声明而没有定义的虚函数,用于要求派生类实现某些行为。
    • 派生类可以继承抽象类并实现其纯虚函数,从而可以实例化派生类对象。Func(&st);//传引用Func(ps);Func(st);return0;}

      🚀 2.1.4 多态场景的选择题

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

      A:  A->0  B: B->1  C:  A->1  D: B->0  E: 编译报错
      classA{public:virtualvoidfunc(intval =1){cout <<"A->"<<val <<endl;}virtualvoidtest(){func();}};classB:publicA{public://重写只是重写的函数的实现,使用父类函数的声明部分加上派生类的实现部分//本质上时重写虚函数的实现,所以可以不加virtual;//实际上的重写/*virtual void func(int val = 1)	{		cout << "B->" << val << endl;	}*///绝不重新定义继承来的缺省值//virtual void func(int val = 0)virtualvoidfunc(intval =1){cout <<"B->"<<val <<endl;}};intmain(){B*b =newB;//实际上时 A 中*this 来调用func所以构成多态b->test();//不构成多态//b->func();return0;}

      正确答案是 B : B->1
      分析:
      **接下来我们很定会有这样的疑问:**首先我们看到 b对象是一个指向 B类型的一个指针,但是 B是派生类,为什么还会构成多态呢?构成多态的条件不是需要父类的指针吗?

      **接下来我们带着疑问来解决上面的问题,**在 main函数中 b对象调用了 test函数,并且通过 test函数来调用 func函数。

    在这里插入图片描述

    ✨ 4.2 多态的原理

    🚀 4.2.1 多态是如何实现的

    classBase{public:virtualvoidfunc1(){cout <<"Base::func1"<<endl;}virtualvoidfunc2(){cout <<"Base::func2"<<endl;}voidfunc5(){cout <<"Base::func5"<<endl;}protected:inta =1;};classDerive:publicBase{public:// 重写基类的func1virtualvoidfunc1(){cout <<"Derive::func1"<<endl;}virtualvoidfunc3(){cout <<"Derive::func1"<<endl;}};intmain(){//同类型的虚函数表是一样的,防止数据冗余。因此,最终输出的是 B类型的函数体和 A类的缺省值

    个人主页:起名字真南的CSDN博客

    个人专栏:

    • 【数据结构初阶】 📘 基础数据结构
    • 【C语言】 💻 C语言编程技巧
    • 【C++】 🚀 进阶C++
    • 【OJ题解】 📝 题解精讲

    目录

    • 📌 前言
    • 📌1 多态
      • ✨ 1.1 `多态`的概念
    • 📌 2 `多态`的定义及实现
      • ✨ 2.1 实现多态所需要的条件
        • 🚀 2.1.1 实现多态的两个`重要条件`
        • 🚀 2.1.2 `虚函数`
        • 🚀 2.1.3 虚函数的`重写/覆盖`
        • 🚀 2.1.4 多态场景的选择题
        • 🚀 2.1.5 override和final关键字
        • 🚀 2.1.6 重载/重写/隐藏的对比
    • 📌 3 纯虚函数和抽象类
      • ✨ 3.1 纯虚函数(Pure Virtual Function)
      • ✨3.2 抽象类(Abstract Class)
      • ✨总结
    • 📌 4 多态的原理
      • ✨ 4.1 函数表指针
      • ✨ 4.2 多态的原理
        • 🚀 4.2.1 多态是如何实现的
        • 🚀 4.2.2 虚函数表
        • 虚函数表(Virtual Table)
        • 虚函数表的工作原理
        • 结构示例

    📌 前言

    在C++编程中,多态是面向对象设计(OOP)的核心特性之一,也是提高代码灵活性和可扩展性。由于包含纯虚函数,抽象类无法直接实例化。是根据将实参传给形参的参数匹配分别是在编译时确定的和运行时确定的。

  • 性能较高:通过虚表指针查找虚函数地址,只需一次指针查找和一次跳转,效率相对较高。
  • 特点
    • 任何包含纯虚函数的类都是抽象类。
  • 内存布局示例

    假设创建一个Derived类的对象d,则d的内存布局如下:

    • 虚表指针:指向Derived类的虚函数表。
    • 虚函数表只在有虚函数的类中存在:没有虚函数的类不会生成虚函数表。虚函数表包含了所有虚函数的地址。其实在继承虚函数时,我们不是将整个虚函数复制过来,而是通过虚函数表来调用它(虚函数表我们后面会详细讲解)。
  • Derived类的虚函数表

    • 继承自Base,会包含func1func2,但func1指向Derived::func1(因为重写了该函数),func2仍指向Base::func2Base::func2

      ✨3.2 抽象类(Abstract Class)

      • 定义:抽象类是包含一个或多个纯虚函数的类。
      • 目的:抽象类用于作为接口或基类,提供公共的接口规范,而不需要自己实现具体功能。

        在这里插入图片描述

        🚀 4.2.2 虚函数表

        虚函数表(Virtual Table)

        在C++中,虚函数表(vtable)是编译器用于支持运行时多态的一种机制。

      重要注意点

      • 虚表是类级别的:同一类的多个对象共享同一个虚函数表。
      • 内存对齐:成员变量按照对齐要求进行排列,使得结构体或类的内存大小可能大于简单的成员变量之和。以下是对这两个概念的详细介绍:

        ✨ 3.1 纯虚函数(Pure Virtual Function)

        • 定义:纯虚函数是没有实现的虚函数,只在类中声明但不提供具体实现。Base b1;Base b2;Derive d;return0;}

          在这里插入图片描述
          此时我们进行的是动态绑定,即运行时到指向的对象的虚表中确定对应函数的地址然后再进行调用这样就实现了指针或引⽤指向基类就调⽤基类的虚函数,指向派⽣类就调⽤派⽣类对应的虚函数。

        • 虚函数表:指向的虚函数表中包含Derived::func1
        • 虚表指针占用4字节,指向虚函数表,表中存储了类的虚函数地址。
      • 虚函数表指针

        • 含有虚函数的类会在其对象中包含一个虚函数表指针(也称虚表指针),用于在运行时支持多态。

        • 结构示例

          假设有如下类结构:

          classBase{public:virtualvoidfunc1(){std::cout <<"Base::func1"<<std::endl;}virtualvoidfunc2(){std::cout <<"Base::func2"<<std::endl;}};classDerived:publicBase{public:voidfunc1()override {std::cout <<"Derived::func1"<<std::endl;}virtualvoidfunc3(){std::cout <<"Derived::func3"<<std::endl;}};

          在这个例子中,虚函数表的布局如下:

          1. Base类的虚函数表

            • 包含func1func2的地址。基类的指针或者引用调用虚函数//2、

            • 被调用的函数必须是虚函数

              第二派生类必须对基类的虚函数进行重写/覆盖,只有经过重写和覆盖,派生类才能有不同的函数。

              虚函数表的工作原理

              1. 创建虚函数表:在包含虚函数的类中,编译器会为该类生成一个虚函数表。

              2. 成员变量:包含类定义中的其他成员变量(例如int a;等)。在派生类中不需要在继承的函数上重新加 virtual,这是因为继承时我们继承的是函数的定义,而重写的部分仅是函数的实现(即函数体部分)

                🚀 2.1.6 重载/重写/隐藏的对比

                在这里插入图片描述

                📌 3 纯虚函数和抽象类

                在C++中,纯虚函数抽象类是面向对象编程中的两个重要概念,主要用于定义接口和实现多态性。ptr.BuyTicket();}intmain(){//满足多态指向谁调用谁,不满足多态就只和ptr的类型有关系.Person ps;Student st;Func(&ps);//子类隐式转换切割父类的那一部分。被调用的函数一定是虚函数virtualvoidBuyTicket(){cout <<"买票-全价"<<endl;}};classStudent:publicPerson{public://返回类型,函数名,参数列表完全相同构成虚函数的重写//子类的virtual可以去掉virtualvoidBuyTicket(){cout <<"买票-半价"<<endl;}};//和Person这个类型没有关系//传派生类会将基类的那一部分切片voidFunc(Person*ptr){//虽然都是父类的指针在调用该函数,但是具体的实现是由ptr指向的对象实现的。这样解决了第一个问题

              3. func3Derived独有的虚函数,因此也会被添加到Derived类的虚函数表中。CircleSquare是派生类,它们实现了draw函数,因此可以被实例化。如果我们不想让派⽣类重写这个虚函数,那么可以⽤final去修饰。每个对象实例则包含一个指向虚函数表的指针(称为虚表指针,vptr)。通过虚函数和动态绑定,多态可以是代码在运行时根据对象的不同调用实现各自的作用,适应更复杂的业务需求。

            📌 4 多态的原理

            ✨ 4.1 函数表指针

            下面在32为系统下编译的结果为()
            A: 编译报错 B:运行报错 C: 8 D:12

            classBase{public:virtualvoidFunc1(){cout <<"Func1()"<<endl;}protected:int_b =1;char_ch ='x';}intmain(){Base b;cout <<sizeof(b)<<endl;returno;}

            正确答案是12
            分析:
            在C++中,类对象的内存布局会受到成员变量虚函数表指针的影响:

            1. 成员变量的大小和对齐

              • int类型的成员变量 _b占用4字节。派生类Dog实现了sound,因此可以实例化。