施磊老师c++笔记(五)

news2025/3/14 10:36:48

继承与多态-深入掌握oop语言最强大的机制

文章目录

  • 继承与多态-深入掌握oop语言最强大的机制
    • 1.继承的基本意义
    • 2.派生类的构造过程
    • 3.重载,隐藏,覆盖
    • 4.虚函数, 静态绑定和动态绑定--面试重点
    • 5.虚析构函数--重点在于什么呢时候用
    • 6.再讨论虚函数和动态绑定
    • 7.理解多态到底是什么
    • 8.理解抽象类----纯虚函数
    • 9.笔试问题讲解

1.继承的基本意义

  1. 继承的本质和原理?
    继承的本质: 1.做代码复用; 2.
    类和类的关系:
    组合: a part of … 一部分的关系
    继承: a kind of … 一种的关系

    #include <iostream>
    using namespace std;
    
    class A
    {
    public:
    	int ma;
    protected:
    	int mb;
    private:
    	int mc;
    };
    // A 12字节
    
    //class B : public A //继承    A是基类/父类  B是派生类/子类
    //{
    //public:
    //	void func()
    //	{
    //		cout << "ma:" << ma << endl;
    //	}
    //	int md;
    //	int me;
    //	int mf;
    //};
    
    
    //class B : protected A //继承    A是基类/父类  B是派生类/子类
    //{
    //public:
    //	void func()
    //	{
    //		
    //	}
    //	int md;
    //	int me;
    //	int mf;
    //};
    
    class B : private A //继承    A是基类/父类  B是派生类/子类
    {
    public:
    	void func()
    	{
    
    	}
    	int md;
    	int me;
    	int mf;
    };
    
    //B有两部分, A+B本身, 24字节
    
    class C : public B //继承    A是基类/父类  B是派生类/子类
    {
    public:
    	void func()
    	{
    
    	}
    	int md;
    	int me;
    	int mf;
    };
    
    int main()
    {
    
    }
    
  2. 继承的细节

    继承方式基类的访问限定派生类的访问限定(main)外部的访问限定
    publicpublicpublicok
    protectedprotectedno(main只能访问公有的)
    private不可见,无法访问,不是私有no
    protectedpublic降级, protectedno
    protectedprotectedno
    private不可见,无法访问,不是私有no
    privatepublicprivateno
    protectedprivateno
    private不可见,无法访问,不是私有no

    private只有自己和友元能访问!!

  3. 对于更多的继承, class C ,要看直接继承的B里面的各个访问限定和继承方式

  4. 总结:
    外部只能访问对象public的成员, protected和private的成员无法直接访问
    继承结构中, 派生类从基类可以继承过来private的成员, 但是派生类无法访问
    protected和private的区别?----基类中定义的成员, 想被派生类访问, 但是不想被外部访问, 那么基类可以定义为protected; 若 派生类和外部都不访问, 基类中设置为private

  5. 默认继承方式------要看派生类是class(private)还是struct(public)!!!

2.派生类的构造过程

  1. 派生类怎么初始化从基类继承的成员变量呢?
    派生类可以从继承得到 继承的所有的成员(变量+方法), 除了构造和析构
    解决办法: 调用基类的 构造函数
    派生类的构造和析构, 负责初始化和清理派生类部分
    派生类从基类继承来的成员–的初始化和清理由谁负责?----由基类的构造和析构
  2. 派生类对象构造和析构过程?
    派生类调用基类构造(初始化基类继承来的成员)—>派生类自己构造—>派生类自己的析构—>调用基类的析构
#include <iostream>
using namespace std;

class Base
{
public:
    Base(int data) :ma(data) { cout << "Base()" << endl; }
    ~Base() { cout << "~Base()" << endl; }
protected:
    int ma;
};

class Derive : public Base
{
public:
    /*Derive(int data) :ma(data), mb(data)*/
    //改为
    Derive(int data) :Base(data), mb(data)
    {
        cout << "Derive()" << endl;
    }
    ~Derive()
    {
        cout << "~Derive()" << endl;
    }
private:
    int mb;
};

int main()
{
    Derive d(20);

    return 0;
}

/*输出:
Base()
Derive()
~Derive()
~Base()*/

3.重载,隐藏,覆盖

  1. 重载关系
    一组函数要重载, 必须在一个作用域内;且函数名相同, 参数列表不同
    基类和派生类是两个不同的作用域!!
  2. 隐藏的关系
    在继承结构中, 派生类的同名成员, 把基类的同名成员给 隐藏了, 也就是默认调用派生类的同名成员
    想要基类, 就要 加基类作用域
#include <iostream>
using namespace std;

class Base
{
public:
    Base(int data=10) :ma(data) {  }
    ~Base() {  }
    void show() { cout << "Base:show" << endl; }
    void show(int) { cout << "Base:show(int)" << endl; }
protected:
    int ma;
};

class Derive : public Base
{
public:
    Derive(int data=20) :Base(data), mb(data)
    {
    }
    ~Derive()
    {
    }
    void show() { cout << "Derive:show" << endl; }
private:
    int mb;
};

int main()
{
    Derive d;
    d.show(); // 隐藏了 基类的同名成员,  没有才去基类    Derive:show
    d.Base::show();  //Base:show
    //d.show(10);//Derive 没有函数重载
    return 0;
}

//输出
Derive:show
Base:show

  1. 把继承结构, 也理解为 从上(基类)到下(派生类)的结构

  2. 基类对象变为–>派生类对象, 派生类对象变为—>基类对象, 基类对象(引用)指向–>派生类对象, 派生类对象(引用)指向—>基类对象
    这是不强转时, 直接=的情况, 实际强转还是可以的

    int main()
    {
        Base b(10);
        Derive d(20);
        //派生类对象--->基类对象    类型从下到上的转换 yes
        //怎么理解? 派生类看做学生, 基类看做人, 想要人, 把学生给你, 是可以的
        b = d;  //相当于把派生类里继承的基类给了 基类
    
        //基类对象-->派生类对象   类型从上到下的转换 no
        //d = b; //这是不行的, 多出来了内存 派生类自己的  
        
        //基类对象(引用)-->指向  派生类对象  类型从下到上的转换 yes
        Base* pb = &d; // 是可以的, 解引用这能访问 Base那么大的区域, 后面的派生类自己的那部分, 管不着, 本来也管不着
        pb->show(); //yes  想要访问派生类show, 可以强转
        pb->show(20); //yes
    
        //派生类对象(引用)--->指向  基类对象 类型从上到下的转换 no
        //Derive* pd = &b; //会访问b没有的内存区域, 属于非法访问
    
    
    
        return 0;
    }
    
  3. 总结: 继承结构中, 只支持从下到上的类型转换!!!-----前提是不强转

4.虚函数, 静态绑定和动态绑定–面试重点

  1. 什么时候是动态绑定?—并不是有虚函数就是动态绑定, 这是误区, 这个老师这节课开始没说明白!!到了第六节课才说

    1.函数是虚函数:基类中的函数必须声明为 virtual。
    
    2.通过 指针或引用 调用:通过基类指针或基类引用调用虚函数。
    
    这是 必要条件!!!二者必须满足
    
    即 
        Derive d(20);
        Base *pb=&d;
    
  2. 无虚函数时:

    #include <iostream>
    using namespace std;
    
    class Base
    {
    public:
        Base(int data=10) :ma(data) {  }
        ~Base() {  }
        void show() { cout << "Base:show" << endl; }
        void show(int) { cout << "Base:show(int)" << endl; }
    protected:
        int ma;
    };
    
    class Derive : public Base
    {
    public:
        Derive(int data=20) :Base(data), mb(data)
        {
        }
        ~Derive()
        {
        }
        void show() { cout << "Derive:show" << endl; }
    private:
        int mb;
    };
    
    int main()
    {
        Derive d(50);
        Base* pb = &d;
        pb->show(); // 静态(编译时期)的绑定(函数的调用)   Base::show (07FF7C6ED1406h) 
        pb->show(10); // 静态(编译时期)的绑定(函数的调用) Base::show (07FF7C6ED10DCh)
    
        cout << sizeof(Base) << endl;  // 4
        cout << sizeof(Derive) << endl; // 8
    
        cout << typeid(pb).name() << endl; //class Base* __ptr64
           
        cout << typeid(*pb).name() << endl; // class Base
    
        return 0;
    }
    
    
  3. 虚函数的总结-1:
    如果类里面定义了虚函数, 编译阶段 就会给这个类类型 产生一个 惟一的 vftable 虚函数表, 这里面主要存储的内容就是 RTTI指针和虚函数的地址
    RTTI–run-time type infomation
    程序运行时, 每一张虚函数表 都会加载到内存的 .rodata区, 只能读, 不能写

  4. 虚函数的总结-2:
    一个类里有虚函数, 这个类定义的对象, 在其运行时, 内存的开始部分, 多存储一个 vfptr虚函数指针, 指向相应类型的 虚函数表vftable. 一个类型定义的n个对象, vfptr都指向同一个虚函数表

  5. 虚函数的总结-3:
    一个类里 虚函数的个数, 不影响对象的大小(对象里只是指针), 影响的是 虚函数表的大小

  6. 虚函数的总结-4:----覆盖!!!
    如果派生类中的方法和基类继承来的某个方法, 函数名,参数列表,返回值都一样, 切基类这个方法是虚函数virtual,
    则 派生类的 该方法 会 自动处理成虚函数—这是cpp标准
    因此, 派生类的 虚函数表里, 该函数会覆盖基类的

  7. 有虚函数:
    Base将不再是只有ma了, 还有虚函数指针vfptr, 指向虚函数表
    因此不再是4字节, 而是8字节

  8. 静态绑定和动态绑定
    静态(编译时期)的绑定(函数的调用)
    动态(运行时期)的绑定(函数的调用)

  9. 从汇编看 静动态绑定

    //静态绑定的汇编   
    mov         rcx,qword ptr [pb]   //this指针存储
    call        Base::show (07FF7C6ED1406h)
        
        
    //动态绑定:  x64的反汇编
     mov         rax,dword ptr [pb]   //把pb里面的地址给eax, 即虚函数表地址
     mov         rax,dword ptr [rax]  //取存储虚函数表地址里的地址,即虚函数表的地址
    
    mov         rcx,qword ptr [pb]   //this指针存储
    call        qword ptr [rax+8] //调用虚函数表里偏移8字节的地址里面的地址, 即 show的地址, 8字节是RTTI的信息
    
    
  10. 总体代码:
    该代码在show 是不是虚函数时, 输出不一样的-----x64

    //是虚函数-------注意考虑内存对齐
    Derive:show
    Base:show(int)
    16
    24
    class Base * __ptr64
    class Derive
    
    //不是虚函数
    Base:show
    Base:show(int)
    4
    8
    class Base * __ptr64
    class Base
    
    #include <iostream>
    using namespace std;
    
    class Base
    {
    public:
        Base(int data=10) :ma(data) {  }
        ~Base() {  }
        virtual void show() { cout << "Base:show" << endl; }
        virtual void show(int) { cout << "Base:show(int)" << endl; }
    protected:
        int ma;
    };
    
    class Derive : public Base
    {
    public:
        Derive(int data=20) :Base(data), mb(data)
        {
        }
        ~Derive()
        {
        }
        void show() { cout << "Derive:show" << endl; }
    private:
        int mb;
    };
    
    int main()
    {
        //有虚函数时
        Derive d(50);
        Base* pb = &d;
        pb->show(); // 静态(编译时期)的绑定(函数的调用)   
        //静态绑定的汇编   call  Base::show (07FF7C6ED1406h) 
        // pb 是Base指针, 如果, Base::show 是普通函数, 则进行 静态绑定
        // pb 是Base指针, 如果, Base::show 是虚函数, 就进行动态绑定
    
        /*
        动态绑定:  x64的反汇编
         mov         rax,dword ptr [pb]   把pb里面的地址给eax, 即虚函数表地址
         mov         rax,dword ptr [rax]  取存储虚函数表地址里的地址,即虚函数表的地址
    
        mov         rcx,qword ptr [pb]   this指针存储
         call        qword ptr [rax+8] 调用虚函数表里偏移8字节的地址里面的地址, 即 show的地址, 这个+8, 不是RTTI的, 一般RTTI是反偏移的,  是因为覆盖是原来的不要了, 新的放在最下面
        */
    
    
        pb->show(10); // 静态(编译时期)的绑定(函数的调用) Base::show (07FF7C6ED10DCh)
    
        cout << sizeof(Base) << endl;  
        cout << sizeof(Derive) << endl; 
    
        cout << typeid(pb).name() << endl; //class Base* __ptr64
           
        cout << typeid(*pb).name() << endl; // class Base
    
        return 0;
    }
    
    
  11. 命令行展示 虚函数结构
    vs命令行工具—>进入文件目录—>输入
    cl 文件.cpp /d1reportSingleClassLayoutDerive

  12. 使用命令行也解释了, call qword ptr [rax+8]

    Derive::$vftable@:
            | &Derive_meta
            |  0
     0      | &Base::show   // 重载在这里, 看不出来, 实际这个是 int
     1      | &Derive::show  // 覆盖是 之前最上面的不要了, 新的放到最下面
    

5.虚析构函数–重点在于什么呢时候用

  1. 哪些函数不能实现成 虚函数?–接上一节
    虚函数能产生函数地址
    虚函数表位置在 vfptr里, vfptr在内存里----- 对象必须存在, 依赖对象
    构造函数–不能是–虚函数, 构造函数中调用虚函数, 不会发生动态绑定, 构造函数中调用的任何函数,都是静态绑定---->这是因为在构造函数执行期间,对象的动态类型尚未完全确定,虚函数表(vtable)也没有完全初始化。
    派生类构造过程–>先调用基类构造–>才调用–>派生类构造
    static静态成员方法–>不能是—>虚函数, 因为不依赖对象
    析构函数–>可以!!–>虚函数, 因为析构时,对象是存在的

  2. 特别注意:基类析构和派生类虚构!!
    基类虚构是虚函数-----> 派生类析构 自动成为虚函数—> 尽管名字不一样!!

  3. 虚析构使用实例:
    为什么 不是虚析构时, 会出问题? 因为使用了
    因为此时是静态绑定, Base类的指针即使指向Derive类, 使用的还是Base的析构

    派生类使用虚析构, 就有了虚函数表, 派生类的虚函数表还会覆盖 基类 的 虚析构, 使用自己的虚析构, 使得可以 正确析构派生类, —>动态绑定
    pb是Base类, 但是指向Derive, 动态绑定, 使得本来是使用Base的析构, 但是发现是虚析构, 于是去找虚函数表, 此时虚函数表的析构已经被 Derive覆盖了, 因此使用了 Derive的析构, 后续继承基类的部分也会正确析构

    #include <iostream>
    using namespace std;
    
    class Base
    {
    public:
        Base(int data=10) :ma(data) { cout << "Base" << endl; }
        virtual ~Base() { cout << "~Base" << endl; }
        void show() { cout << "call Base:show" << endl; }
        
    protected:
        int ma;
    };
    
    class Derive : public Base
    {
    public:
        Derive(int data=20) :Base(data), mb(data)
        {
            cout << "Derive" << endl;
        }
        ~Derive()
        {
            cout << "~Derive" << endl;
        }
        void show() {  }
    private:
        int mb;
    };
    
    int main()
    {
        Base* pb = new Derive(10);
        pb->show(); // 静态绑定,pb是Base*类型,   而*pb类型取决于show是不是虚函数, 若是, 则是Derive,因为指向了派生类的show, 且是动态绑定
        delete pb;  // 不使用虚析构, 会导致派生类析构没有调用-->若有指针.容易内存泄漏
    
    
    
       /* Derive d(50);
        Base* pb = &d;
        pb->show(); */
        return 0;
    }
    
    
    
  4. 什么时候用?—特别重点
    基类的指针(引用)指向 堆上new出来的 派生类, delete 基类指针时

6.再讨论虚函数和动态绑定

  1. 虚函数的调用就是 动态绑定?
    不是!!!
    构造函数就是例外, 构造函数中调用虚函数, 不会发生动态绑定, 构造函数中调用的任何函数,都是静态绑定

  2. 什么时候发生动态绑定?

    1.函数是虚函数:基类中的函数必须声明为 virtual。
    
    2.通过 指针或引用 调用:通过基类指针或基类引用调用虚函数。
    
    这是 必要条件!!!二者必须满足
    
    
    
  3. 实例:---- 一定要搞清楚什么时候动态绑定!!!

    #include <iostream>
    using namespace std;
    
    class Base
    {
    public:
        Base(int data=10) :ma(data) { cout << "Base" << endl; }
        virtual ~Base() { cout << "~Base" << endl; }
        virtual void show() { cout << "call Base:show" << endl; }
        
    protected:
        int ma;
    };
    
    class Derive : public Base
    {
    public:
        Derive(int data=20) :Base(data), mb(data)
        {
            cout << "Derive" << endl;
        }
        ~Derive()
        {
            cout << "~Derive" << endl;
        }
        void show() {  }
    private:
        int mb;
    };
    
    int main()
    {
        Base b;
        Derive d;
        b.show();  // 二者都是静态绑定,
        d.show();
    
        Base* pb1 = &b; //二者是动态绑定
        pb1->show();
    
        Base* pb2 = &d;
        pb2->show();
    
        Base& rb1 = b;//二者是动态绑定
        pb1->show();
    
        Base& rb2 = d;
        pb2->show();
    
        Derive* pd2 = (Derive*)&b;//动态绑定---满足两个条件-----这里必须强转, 回看第三节的总结
        pd2->show();//---这里是调用的基类的show, 因为实际的b是Base, 其虚函数表是基类的, 这种强转并不会 改变原本的虚函数表指向
    
    
        return 0;
    }
    
    

7.理解多态到底是什么

  1. 如何理解多态?
    静态的 多态: — 编译阶段就确定好 — 函数重载和模板(函数模板,类模板),

    动态的 多态: — 继承结构中, 基类指针(引用) 指向派生类对象, 通过该指针(引用)调用同名覆盖方法(虚函数),基类指针指向哪个派生类, 就调用哪个派生类对象的同名覆盖方法, 称为多态
    多态底层是通过 动态绑定来实现的的

  2. 实例: 虚函数配合基类, 完成 开-闭 设计, 使用指针时切记虚析构

    #include <iostream>
    using namespace std;
    
    // 动物的基类
    class Animal
    {
    public:
        Animal(string name) : _name(name) {}
        virtual void bark() {}
    protected:
        string _name;
    };
    
    class Cat : public Animal
    {
    public:
        Cat(string name) : Animal(name) {}
        void bark() { cout << _name << " bark: miao miao!" << endl; }
    };
    
    class Dog : public Animal
    {
    public:
        Dog(string name) : Animal(name) {}
        void bark() { cout << _name << " bark: wang wang!" << endl; }
    };
    
    class Pig : public Animal
    {
    public:
        Pig(string name) : Animal(name) {}
        void bark() { cout << _name << " bark: heng heng!" << endl; }
    };
    
    
    //下面的API无法达到 软件设计的 开-闭 原则: 对修改关闭, 对扩展开放
    //void bark(Dog &dog) 
    //{ 
    //    dog.bark(); 
    //}
    //
    //void bark(Pig& pig)
    //{
    //    pig.bark();
    //}
    //
    //void bark(Cat& cat)
    //{
    //    cat.bark();
    //}
    
    
    //使用基类指针
    void bark(Animal *p)
    {
        p->bark(); // 虚函数 覆盖
    }
    
    int main()
    {
        Dog dog("dog");
        Pig pig("pig");
        Cat cat("cat");
    
        bark(&dog);
        bark(&cat);
        bark(&pig);
    
        return 0;
    }
    
    
  3. 继承的好处?
    1.做代码复用
    2.在基类中给所有的派生类提供统一的虚函数接口, 让派生类重写, 然后就可以多态了

8.理解抽象类----纯虚函数

  1. 类 是 抽象一个实体的类型

  2. 什么是纯虚函数?
    纯虚函数(Pure Virtual Function)是 C++ 中用于定义抽象类的一种机制。纯虚函数在基类中声明但不提供实现,要求派生类必须重写(实现)该函数。包含纯虚函数的类称为抽象类,抽象类不能被实例化。

    在函数声明的末尾加上 = 0,表示这是一个纯虚函数
    virtual void foo() = 0;

  3. 什么是 抽象类?
    拥有纯虚函数的类, 叫做抽象类
    抽象类不能再实例化对象, 但是可以定义指针和引用变量
    一般是基类作为抽象类, 派生类去实例化对象

  4. 实例: — 注意 外部接口是 基类

    #include <iostream>
    using namespace std;
    
    // 动物的基类
    class Animal
    {
    public:
        Animal(string name) : _name(name) {}
        virtual void bark() {}
    protected:
        string _name;
    };
    
    class Cat : public Animal
    {
    public:
        Cat(string name) : Animal(name) {}
        void bark() { cout << _name << " bark: miao miao!" << endl; }
    };
    
    class Dog : public Animal
    {
    public:
        Dog(string name) : Animal(name) {}
        void bark() { cout << _name << " bark: wang wang!" << endl; }
    };
    
    class Pig : public Animal
    {
    public:
        Pig(string name) : Animal(name) {}
        void bark() { cout << _name << " bark: heng heng!" << endl; }
    };
    
    
    //下面的API无法达到 软件设计的 开-闭 原则: 对修改关闭, 对扩展开放
    //void bark(Dog &dog) 
    //{ 
    //    dog.bark(); 
    //}
    //
    //void bark(Pig& pig)
    //{
    //    pig.bark();
    //}
    //
    //void bark(Cat& cat)
    //{
    //    cat.bark();
    //}
    
    
    //使用基类指针
    void bark(Animal *p)
    {
        p->bark(); // 虚函数 覆盖
    }
    
    int main()
    {
        Dog dog("dog");
        Pig pig("pig");
        Cat cat("cat");
    
        bark(&dog);
        bark(&cat);
        bark(&pig);
    
        return 0;
    }
    
    
    
    
  5. 实例-2:

    #include <iostream>
    #include <string>
    using namespace std;
    
    // 抽象基类 Car
    class Car {
    public:
        Car(string name) : _name(name) {}
        virtual double getMilesPerGallon() = 0; // 纯虚函数
        string getName() { return _name; }
        double getLeftMiles(double fuel) {
            return fuel * getMilesPerGallon();
        }
    protected:
        string _name;
    };
    
    // 派生类 Audi
    class Audi : public Car {
    public:
        Audi(string name) : Car(name) {}
        double getMilesPerGallon() override {
            return 18.0;
        }
    };
    
    // 派生类 BMW
    class BMW : public Car {
    public:
        BMW(string name) : Car(name) {}
        double getMilesPerGallon() override { //override 是 C++11 引入的一个关键字,用于显式地标记派生类中的函数是对基类虚函数的重写。它的主要作用是提高代码的可读性和安全性,帮助开发者避免一些常见的错误。
            return 19.0;
        }
    };
    
    // 给外部提供一个统一的获取汽车剩余路程数的API
    void showCarLeftMiles(Car &car, double fuel) {
        cout << car.getName() << " left miles: " << car.getLeftMiles(fuel) << " 公里" << endl;
    }
    
    int main() {
        Audi a("奥迪");
        BMW b("宝马");
    
        showCarLeftMiles(a, 10.0); // 假设有10加仑的油
        showCarLeftMiles(b, 10.0);
    
        return 0;
    }
    

9.笔试问题讲解

  1. 实例-1
    重点: 函数调用, 参数压栈是在编译时期 就确定的
    因此, 派生类虚函数参数默认值 是用不到的
    默认参数值是静态绑定的,而虚函数的调用是动态绑定的。即使 基类无,派生类有,也没用

    #include <iostream>
    using namespace std;
    
    class Base {
    public:
        virtual void show(int i = 10) {
            cout << "call Base::show i:" << i << endl;
        }
    
        virtual ~Base() {} // 虚析构函数
    };
    
    class Derive : public Base {
    public:
        void show(int i = 20) override {
            cout << "call Derive::show---" << i << endl;
        }
    };
    
    int main() {
        Base* p = new Derive(); // 使用基类指针指向派生类对象
        p->show(); // 动态绑定,调用Derive::show, 但是输出却是 10, 不是20
        delete p; // 释放内存
        return 0;
    }
    
    //为什么会是10 呢
    从函数调用角度讲, 先压参数列表, 才压函数符号
    而在编译阶段, 看不到动态绑定, 只能看到是Base*类, 因此压入的是10
    push 0Ah   编译时
    mov eax, dword ptr[p]  运行时   
    mov ecx, deord ptr[eax]
    call eax
    
    push的是死的, 不会因为后面调虚函数而改变
        
        
    
  2. 实例-2
    重点:成员的权限, 是在编译阶段 确定好的!!!
    编译阶段只能看见 p是Base的, 而基类里是 public的
    千万不要去 运行时看 成员权限!!

    #include <iostream>
    using namespace std;
    
    class Base {
    public:
        virtual void show() {
            cout << "call Base::show i:" << endl;
        }
    
        virtual ~Base() {} // 虚析构函数
    };
    
    class Derive : public Base {
    private:
        void show() override {
            cout << "call Derive::show---"  << endl;
        }
    };
    
    int main() {
        Base* p = new Derive(); 
        p->show(); // 运行时确定
        delete p; 
        return 0;
    }
    
    
        
        
    
  3. 实例-3
    重点: vfptr什么时候拿到虚函数表地址? 是重点!!!
    每个类(无论是基类还是派生类)都有自己的虚函数表(vtable)。
    每个对象(无论是基类对象还是派生类对象)都有自己的虚函数表指针(vfptr),指向其所属类的虚函数表。

    #include <iostream>
    using namespace std;
    
    class Base {
    public:
        Base() {
            /*
        push ebp
        mov ebp, esp
        sub esp, 4Ch
        rep stos esp<->ebp     0xCCCCCCCC (windows VS GCC/G++)
        此时, 就会进行  vfptr->vftable地址
        进入函数后, 第一件事就是虚函数表指针的存储
        */
            cout << "call Base()" << endl;
            clear();
        }
        void clear()
        {
            memset(this, 0, sizeof(*this));
        }
        virtual void show() {
            cout << "call Base::show()" << endl;
        }
    
        virtual ~Base() {
            cout << "call ~Base()" << endl;
        }
    };
    
    class Derive : public Base {
    public:
        Derive() {
            cout << "call Derive()" << endl;
        }
    
        void show() override {
            cout << "call Derive::show()" << endl;
        }
    
        ~Derive() {
            cout << "call ~Derive()" << endl;
        }
    };
    
    int main() { 
        //Base* pbl = new Base();   
        //pbl->show(); // 动态绑定
        //delete pbl;  //这一段肯定会出错, vfptr是0了, 肯定访问不到了
    
        Base* pb2 = new Derive();  
        pb2->show(); // 动态绑定,
        delete pb2;  // 这一段是可以的, 先基类构造, 再派生类构造
        //涉及到了  vfptr什么时候得到的vftable的 地址, 在构造函数里
        //将会把派生类虚函数地址写入vfptr, 
    
        
    
        return 0;
    }
    

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2314810.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

µCOS-III从入门到精通 第十四章(软件定时器)

参考教程&#xff1a;【正点原子】手把手教你学UCOS-III实时操作系统_哔哩哔哩_bilibili 一、软件定时器简介 1、定时器的概念与种类 &#xff08;1&#xff09;定时器的概念&#xff1a;从指定的时刻开始&#xff0c;经过一个指定时间&#xff0c;然后触发一个超时事件&…

MySQL数据库复杂的增删改查操作

在前面的文章中&#xff0c;我们主要学习了数据库的基础知识以及基本的增删改查的操作。接下去将以一个比较实际的公司数据库为例子&#xff0c;进行讲解一些较为复杂且现时需求的例子。 基础知识&#xff1a; 一文清晰梳理Mysql 数据库基础知识_字段变动如何梳理清楚-CSDN博…

KCD 北京站丨Volcano 邀您畅聊云原生智能调度技术与应用

AI与云原生技术正以前所未有的速度改变着我们的世界&#xff0c;而云原生技术则如同一座坚实的桥梁&#xff0c;连接着传统IT与现代化的数字世界。当AI与云原生相遇&#xff0c;它们相互赋能&#xff0c;相互促进&#xff0c;为开发者们打开了一个全新的技术宇宙。 3 月 15 日&…

BLEU评估指标

一、介绍 用于评估模型生成的句子和实际句子差异的指标&#xff0c;取值在[0,1]&#xff0c;匹配度高就距离1近&#xff0c;反之距离0近。这个指标计算代价小&#xff0c;容易理解&#xff0c;与语言无关&#xff0c;与人类评价结果高度相关。 BLEU主要基于n-gram匹配&#x…

高效自动化测试:打造Python+Requests+Pytest+Allure+YAML的接口测试框架

一、背景 在快节奏的开发周期中&#xff0c;如何确保接口质量&#xff1f;自动化测试是关键。通过构建标准化、可复用的测试框架&#xff0c;能显著提升测试效率与准确性&#xff0c;为项目质量保驾护航[1][7]。 二、目标 ✅ 核心目标&#xff1a; ● 实现快速、高效的接口测试…

BSides Vancouver: 2018 (Workshop)

BSides Vancouver: 2018 (Workshop) 来自 <https://www.vulnhub.com/entry/bsides-vancouver-2018-workshop,231/> 1&#xff0c;将两台虚拟机网络连接都改为NAT模式 2&#xff0c;攻击机上做namp局域网扫描发现靶机 nmap -sn 192.168.23.0/24 那么攻击机IP为192.168.23…

rStar论文精读

论文简介 论文标题&#xff1a;《Mutual reasoning makes smaller LLMs stronger problem-solvers》 论文地址&#xff1a;https://arxiv.org/abs/2408.06195 录用会议&#xff1a;ICLR2025 背景与挑战 挑战1&#xff1a;在SLM中平衡exploration与exploitation。一些方法有很…

247g 的工业级电调,如何让无人机飞得更 “聪明“?——STONE 200A-M 深度测评

一、轻量化设计背后的技术取舍 当拿到 STONE 200A-M 时&#xff0c;247g 的重量让人意外 —— 这个接近传统 200A 电调 70% 的重量&#xff0c;源自 1205624.5mm 的紧凑结构&#xff08;0.1mm 公差控制&#xff09;。实测装机显示&#xff0c;相比同规格产品&#xff0c;其体积…

Node.js:快速启动你的第一个Web服务器

Node.js 全面入门指南 文章目录 Node.js 全面入门指南一 安装Node.js1. Windows2. MacOS/Linux 二 配置开发环境1. VSCode集成 三 第一个Node.js程序1. 创建你的第一个Node.js程序 四 使用Express框架1. 快速搭建服务器 一 安装Node.js 1. Windows 以下是Windows环境下Node.j…

自定义日志回调函数实现第三方库日志集成:从理论到实战

一、应用场景与痛点分析 在开发过程中&#xff0c;我们经常会遇到以下场景&#xff1a; 日志格式统一&#xff1a;第三方库使用自己的日志格式&#xff0c;导致系统日志混杂&#xff0c;难以统一管理和分析。日志分级过滤&#xff1a;需要动态调整第三方库的日志输出级别&…

Linux练级宝典->任务管理和守护进程

任务管理 进程组概念 每个进程除了进程ID以外&#xff0c;还有一个进程组&#xff0c;进程组就是一个或多个进程的集合 同一个进程组&#xff0c;代表着他们是共同作业的&#xff0c;可以接收同一个终端的各种信号&#xff0c;进程组也有其唯一的进程组号。还有一个组长进程&a…

C语言:计算并输出三个整数的最大值 并对三个数排序

这是《C语言程序设计》73页的思考题。下面分享自己的思路和代码 思路&#xff1a; 代码&#xff1a; #include <stdio.h> int main() {int a,b,c,max,min,mid ; //设置大中小的数分别为max&#xff0c;mid&#xff0c;min&#xff0c;abc为输入的三个数printf("ple…

工具(十二):Java导出MySQL数据库表结构信息到excel

一、背景 遇到需求&#xff1a;将指定数据库表设计&#xff0c;统一导出到一个Excel中&#xff0c;存档查看。 如果一个一个弄&#xff0c;很复杂&#xff0c;耗时长。 二、写一个工具导出下 废话少絮&#xff0c;上码&#xff1a; 2.1 pom导入 <dependency><grou…

ACL初级总结

ACL–访问控制列表 1.访问控制 在路由器流量流入或者流出的接口上,匹配流量,然后执行相应动作 permit允许 deny拒绝 2.抓取感兴趣流 3.ACL匹配规则 自上而下逐一匹配,若匹配到了则按照对应规则执行动作,而不再向下继续匹配 思科:ACL列表末尾隐含一条拒绝所有的规则 华为:AC…

调优案例一:堆空间扩容提升吞吐量实战记录

&#x1f4dd; 调优案例一&#xff1a;堆空间扩容提升吞吐量实战记录 &#x1f527; 调优策略&#xff1a;堆空间扩容三部曲 # 原配置&#xff08;30MB堆空间&#xff09; export CATALINA_OPTS"$CATALINA_OPTS -Xms30m -Xmx30m"# 新配置&#xff08;扩容至120MB&am…

C语言 —— 此去经年梦浪荡魂音 - 深入理解指针(卷一)

目录 1. 内存和地址 2. 指针变量和地址 2.1 取地址操作符&#xff08;&&#xff09; 2.2 指针变量 2.3 解引用操作符 &#xff08;*&#xff09; 3. 指针的解引用 3.1 指针 - 整数 3.2 void* 指针 4. const修饰指针 4.1 const修饰变量 4.2 const修饰指针变量 5…

计算机毕业设计:留守儿童的可视化界面

留守儿童的可视化界面mysql数据库创建语句留守儿童的可视化界面oracle数据库创建语句留守儿童的可视化界面sqlserver数据库创建语句留守儿童的可视化界面springspringMVChibernate框架对象(javaBean,pojo)设计留守儿童的可视化界面springspringMVCmybatis框架对象(javaBean,poj…

golang算法二叉树对称平衡右视图

100. 相同的树 给你两棵二叉树的根节点 p 和 q &#xff0c;编写一个函数来检验这两棵树是否相同。 如果两个树在结构上相同&#xff0c;并且节点具有相同的值&#xff0c;则认为它们是相同的。 示例 1&#xff1a; 输入&#xff1a;p [1,2,3], q [1,2,3] 输出&#xff1a…

Chatbox通过百炼调用DeepSeek

解决方案链接&#xff1a;评测&#xff5c;零门槛&#xff0c;即刻拥有DeepSeek-R1满血版 方案概览 本方案以 DeepSeek-R1 满血版为例进行演示&#xff0c;通过百炼模型服务进行 DeepSeek 开源模型调用&#xff0c;可以根据实际需求选择其他参数规模的 DeepSeek 模型。百炼平台…

【数据结构】6栈

0 章节 3&#xff0e;1到3&#xff0e;3小节。 认知与理解栈结构&#xff1b; 列举栈的操作特点。 理解并列举栈的应用案例。 重点 栈的特点与实现&#xff1b; 难点 栈的灵活实现与应用 作业或思考题 完成学习测试&#xff12;&#xff0c;&#xff1f; 内容达成以下标准(考核…