• C++虚基类、虚函数、虚析构函数、纯虚函数


    什么时多态

    多态,即多种形态,是一种“泛型技术”,它企图使用不变的模板代码来实现可变的算法。在C++中,多态分为两种:

    1. 静态多态,就是说在编译时就能确定函数地址,通过复用函数名实现:如函数重载、运算符重载。
    2. 动态多态,就是能够在运行时确定函数地址,通过派生类和虚函数一起在运行时实现。

    它们两者的区别就在于函数地址绑定的时间不同。函数重载和运算符载比较好理解。我们接下来主要了解派生类与虚函数一起是如何实现多态的。

    虚函数

    首先,我们要区分一下虚基类与虚函数,它们是不同的。基类是使用基类唯一化,虚函数则是能够调用派生类的函数,自身的函数实现被隐藏。

    什么是虚基类

    举个例子来说明一下什么是虚基类吧。

    #include 
    
    using namespace std;
    
    class Base {
    public:
        Base(){
            cout<< "Base" << endl;
        }
    };
    
    class DerivedA:  public Base{
    public:
        DerivedA(){
            cout<< "Derived A" << endl;
        }
    };
    
    class DerivedB:  public Base{
    public:
        DerivedB(){
            cout<< "Derived B"<<endl;
        }
    };
    
    class DerivedAll: public DerivedA,public DerivedB{
    public:
        DerivedAll(){
            cout<< "Derived All"<<endl;
        }
    };
    
    
    int main()
    {
       DerivedAll a;
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    打印结果:

    Base
    Derived A
    Base
    Derived B
    Derived All
    
    • 1
    • 2
    • 3
    • 4
    • 5

    从上面打印出来的结果可以看出Base在内存中有两个副本。但实际上只需要一个Base副本就可以了。此时在继承类前加上关键字virtual,即:

    class DerivedA:  virtual public Base{
    public:
        DerivedA(){
            cout<< "Derived A" << endl;
        }
    };
    
    class DerivedB: virtual public Base{
    public:
        DerivedB(){
            cout<< "Derived B"<<endl;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    再次运行结果:

    Base
    Derived A
    Derived B
    Derived All
    
    • 1
    • 2
    • 3
    • 4

    此时Base在内存只有一份了。因此虚基类就是让基类在内存中唯一化。

    什么是虚函数

    虚函数是指一个类中要重载的成员函数,当一个基类指针或引用指向一个继承类对象的时候,调用一个虚函数,实际调用的是派生类中的。否则,调用的就是基类中的。

    #include 
    
    using namespace std;
    
    class Base {
    public:
        void func(){
            cout<< "Base"<<endl;
        }
    };
    
    class DerivedA: public Base{
    public:
        void func(){
            cout<< "Derived A"<<endl;
        }
    };
    
    int main()
    {
       Base * pb = new DerivedA();
       pb->func();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    打印结果:

    Base
    
    • 1

    本来我们想通过基类指针调用派生类中的func方法,现在去调用了基类的。其实只需将基类中的要重载的方法前加上关键字virtual,使其成为虚函数,就可以实现用基类指针或引用来调用派生类中重载了的方法:

    class Base {
    public:
        virtual void func(){
            cout<< "Base"<<endl;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    再次运行的结果:

    Derived A
    
    • 1

    编译器给每个对象和虚函数添加了一个隐形的成员:指向虚函数表的指针。虚函数表包含了虚函数的地址,由所有虚函数对象共用。
    当派生类重新定义虚函数时,则将该函数的地址添加到虚函数表中。当一个基类指针指向一个派生类对象时,虚函数表指针指向派生类对象的虚函数表。
    当调用虚函数时,由于派生类对象重写了派生类对应的虚函数表项,基类在调用时会调用派生类的虚函数,从而产生多态。

    请记住,无论对象中定义了多少个虚函数,虚函数表指针只有一个,相应地,每个对象在内存中的大小要比没有虚函数大8B(64位机)或4B(32位机)。这是指针的大小。

    派生类继承了基类的虚函数表指针,因此大小与基类一致。如果多重继承的另一个类也包括了虚函数的基类,那么隐藏成员就包括了两个虚函数表指针。例如:

    #include 
    
    using namespace std;
    
    class Base {
    public:
        void func1(){
            cout<< "Base func1"<<endl;
        }
        virtual void func2(){
            cout<< "Base func2"<<endl;
        }
        virtual void func3(){
            cout<< "Base func3"<<endl;
        }
    };
    
    class Derived: public Base{
    public:
        virtual void func2(){
            cout<< "Derived func2"<<endl;
        }
    };
    
    int main()
    {
       Base * pb = new Derived();
       pb->func1();
       pb->func2();
       pb->func3();
        return 0;
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    Base func1
    Derived func2
    Base func3
    
    • 1
    • 2
    • 3
    1. 首先,使用new关键字创建一个Derived对象,pb指针指向它。调用派生类构造函数会先调用基类的构造函数,然后再调用派生类的构造函数。
    2. 由于Derived继承了Base,所以Derived拥有Base的属性与方法,因此,对于pb->func1()时会调用Base的func1()函数。
    3. 由于Derived重写了func2()函数,在Derived对象中的虚函数表项中指向func2()函数的指针被修改为Derived::func2(),由于虚函数表指针为类对象的第一个字段,即基类指针指向派生类对象时,仍然会获取到派生类的虚函数表指针。因此pb->func2()时,程序会先通过派生类的虚函数表指针获取func2()的入口。
    4. 当进行pb->func3()时,由于派生类没有重写它,因此派生类的虚函数表里的func3()的入口仍然是Base的func3()函数。

    虚析构函数

    一个基类指针可以通过new产生一个派生类对象,如果delete关键字去删除这个指针时,仅仅会调用基类的析构函数,而派生类的空间没有被释放,这样会造成内存泄露。

    为了防止内存泄露,当派生类中有指针成员变量时,才会使用到虚析构函数。虚析构函数使在删除指向派生类对象的基类指针时,可以通过调用派生类的析构函数来实现释放派生类所占内存,从而防止内存泄露。

    #include 
    
    using namespace std;
    
    class Base {
    public:
        virtual ~ Base(){
            cout<< "delete Base" << endl;
        }
        virtual void func(){
            cout<< "Base func"<<endl;
        }
    };
    
    class Derived: public Base{
    public:
        ~ Derived(){
            cout << "delete Derived"<< endl;
        }
        void func(){
            cout<< "Derived func2"<<endl;
        }
    };
    
    int main()
    {
       Base * pb = new Derived();
       pb->func();
       delete pb;
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    Derived func2
    delete Derived
    delete Base
    
    • 1
    • 2
    • 3

    如果基类的析构函数不加virtual关键字,那它就是一个普通的析构函数。如果没有将基类的析构函数声明为虚析构函数,那么删除基类指针时,只会调用基类的析构函数,而不会调用派生类的析构函数。如果基类的析构函数声明为虚析构函数,那么删除基类指针时,会先调用派生类的析构函数,再调用基类的析构函数。

    纯虚函数

    纯虚函数相当于java、kotlin中的接口中的方法,只是声明,实现留给派生类来完成。纯虚函数的语法:

    virtual 返回类型 函数名(参数列表)=0
    • 1

    纯虚函数与虚函数的区别是前者不需要实现,后者需要实现。在虚函数后加上“=0”,虚函数就变成纯虚函数了。

    当类中有了纯虚函数,这个类就叫抽象类。由于抽象类中没有实现函数,所以它不能被实例化,其派生类必须重写纯虚函数,否则派生类也是一个抽象类。抽象类只是为了给其他类提供一个适合的基类。抽象类可以实现部分功能,把不能确定的部分声明为纯虚函数,留给其派生类来实现。纯虚函数一般会作为接口使用,用它约束代码,使用代码符合规范,利于开发大型项目。

    class Base {
    public:
        virtual void func() = 0;
    };
    
    class Derived: public Base{
    public:
        virtual void func(){
            cout<< "Derived func"<<endl;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
  • 相关阅读:
    什么是正则表达式?
    NOIP2023模拟5联测26 零
    如何去除视频上的文字?免费无痕去水印分享!视频制作良器!
    Oracle和Random Oracle
    会员题-力扣408-有效单词缩写
    抖音开放平台探索价值共生新生态,促进高校招生就业提质增效
    第五章 数据库设计和事务 ① 笔记
    第七章 贝叶斯分类器(下)
    Linux内核面试题(1)
    基于反馈机制的鲸鱼优化算法-附代码
  • 原文地址:https://blog.csdn.net/weixin_40763897/article/details/126338554