• 【c++】虚函数和虚函数表(多态)


    多态

    静态多态:静态多态是指在编译时实现的多态,比如函数重载,看似是调用同一个函数其实是在调用不同的。
    动态多态:动态多态是在运行中实现的,当一个父类对象的引用或者指针接收不同的对象(父类对象or子类对象)后,调用相同的函数会调用不同的函数。

    重载、重写、重定义

    在这里插入图片描述

    简介

    虚函数和纯虚函数的作用

    • 定义一个函数为虚函数,不代表函数为不被实现的函数。virtual void foo() ;
    • 定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
    • 定义一个函数为纯虚函数,才代表函数没有被实现。virtual void foo() = 0;
    • 定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。

    代码示例

    class A  
    {  
    public:  
        virtual void foo()  
        {  
            cout<<"A::foo() is called"<<endl;  
        }  
    };  
    class B:public A  
    {  
    public:  
        void foo()  
        {  
            cout<<"B::foo() is called"<<endl;  
        }  
    };  
    int main(void)  
    {  
        A *a = new B();  
        a->foo();   // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!  
        return 0;  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    在这里插入图片描述
    基类A里面有一个虚函数表,用数组实现,存放的是子类虚函数指针。

    对象的内存布局

    class C {
    	void fun_a();
    	void fun_b();
    	int var
    }
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述
    其中成员函数放在代码区,为该类的所有对象公有,即不管新建多少个该类的对象,所对应的都是同一个函数存储区的函数。而成员变量则为各个对象所私有,即每新建一个对象都会新建一块内存区用来存储var值。在调用成员函数时,程序会根据类的类型,找到对应代码区所对应的函数并进行调用。
    如果是普通函数的调用,子类继承父类之后会调用父类的函数。
    那么虚函数又是怎么实现的呢?对于

    class C {
    	void fun_a();
    	virtul void fun_b();
    	int var
    }
    • 1
    • 2
    • 3
    • 4
    • 5

    其内存布局为:
    在这里插入图片描述
    虚函数表指针vptr。这个指针指向一张名为“虚函数表”(vtbl)的表,而表中的数据则为函数指针,存储了虚函数fun_b()具体实现所对应的位置。
    注意,普通函数、虚函数、虚函数表都是同一个类的所有对象公有的,只有成员变量和虚函数表指针是每个对象私有的,sizeof的值也只包括vptr和var所占内存的大小(也是个常出现的问题),并且vptr通常会在对象内存的最起始位置。
    另外,当类有多个虚函数时,仍然只有一个虚函数表指针vptr,而此时的虚函数表vtbl中会有多个函数指针,分别指向对应的虚函数实现区域。在重复一遍虚函数实现的过程:通过对象内存中的vptr找到虚函数表vtbl,接着通过vtbl找到对应虚函数的实现区域并进行调用。

    析构函数和构造函数可以是虚函数吗?

    答案是构造函数不能是虚函数,析构函数可以是虚函数且推荐最好设置为虚函数。
    首先,我们已经知道虚函数的实现则是通过对象内存中的vptr来实现的。而构造函数是用来实例化一个对象的,通俗来讲就是为对象内存中的值做初始化操作。那么在构造函数完成之前,vptr是没有值的,也就无法通过vptr找到作为虚函数的构造函数所在的代码区,所以构造函数只能作为普通函数存放在类所指定的代码区中。
    那么为什么析构函数推荐最好设置为虚函数呢?当我们delete()的时候,如果析构函数不是虚函数,那么调用的将会是基类base的析构函数。而当继承的时候,通常派生类会在基类的基础上定义自己的成员,此时我们当时是希望可以调用派生类的析构函数对新定义的成员进行析构。

    class A
    {
    public:
        A(int data) :data(data) {
            p = new int(data);
            cout << "调用A类的构造函数" << endl;
            cout << p << endl;
        }
        ~A() {
            cout << "调用A类的析构函数" << endl;
            delete p;
        }
    private:
        int data;
        int* p;
    };
    class B :public A
    {
    public:
        //using A::A;//使用基类的构造函数
        B(int data) :A(data) {
            cout << "调用B类的构造函数" << endl;
            p = new int(data);
            cout << p << endl;
        }
        ~B() {
            cout << "调用B类的析构函数" << endl;
            delete p;
        }
    private:
        int data;
        int* p;
    };
    int main(void)
    {
        A* a = new B(10);
        delete 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

    调用结果如下:
    在这里插入图片描述
    基类指针指向子类,调用基类的析构函数,导致内存泄漏。
    改为:virtual ~A()
    在这里插入图片描述

  • 相关阅读:
    spark源码阅读总纲
    2022,软件测试行业岗位细分,薪资分布
    每日亿题之20220904
    《上海悠悠接口自动化平台》-4.注册用例集实战演示
    【数据结构】冒泡排序,快速排序的学习知识总结
    Beego框架学习
    开发工程师必备————【Day32】Django补充(七)
    idea搭建ssm项目全过程详解:
    Wireshark-win32-1.8.4 给winxp和win2003用
    你10点钟还在做数据报表?套用模板,让你提前下班3小时
  • 原文地址:https://blog.csdn.net/wenningker/article/details/126512111