• C++基类和派生类的内存分配,多态的实现


    基类和派生类的内存分配

    类包括成员变量(data member)和成员函数(member function)。
    成员变量分为静态数据(static data)和非静态数据(non-static data),成员函数分为静态成员函数(static function)、非静态成员函数(non-static function)和虚拟成员函数(vritual function)。

    C++编译器将类的静态数据、静态成员函数以及非静态成员函数存储在类对象存储空间之外,并且无论该类声明了多少对象,在内存中只存有一份。
    虚拟成员函数也存储在类对象存储空间之外,编译器为每个虚拟成员函数产生一个指针,并将这些指针存储在一个被称为虚基表的表格中。

    类对象的存储空间中包括:非静态数据以及指向虚基表的指针。
    看个例子,为了方便观察做个字节对齐。

    //4字节对齐
    #pragma pack(push, 4)
    
    class C
    {
        int age;
        static int year;//静态成员变量,不占用类的空间
    public:
        C()
        {
            age = 12;
            printf("C()\n");
        }
        ~C() = default;
    //    virtual ~C() = default;//定义了虚函数,则是多态类,会生成虚函数地址表
    
        void TestFunc(){
            printf("age=%d\n",age);
        }
    };
    
    class D : public C
    {
        int price;
    public:
        D(){
            price = 2000;
            printf("D()\n");
        }
    
        void TestFunc(){
            printf("price=%d\n",price);
        }
    };
    #pragma pack(pop)
    
    //测试调用
        D dd;
        printf("sizeof(C)=%ld\n",sizeof(C));
        printf("sizeof(D)=%ld\n",sizeof(D));
    
    • 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
    • 40

    打印

    sizeof(C)=4
    sizeof(D)=8
    
    • 1
    • 2

    观察一:非多态类
    类C的析构函数不是虚函数,此时C和D都不是多态类,也就是普通的类。类的大小就是非静态成员变量的大小之和。
    观察D dd的内存分配:

    dd	@0x7fffffffe388	D
    	[C]	@0x7fffffffe388	C
    		age	12	int
    		year	<optimized out>	
    	price	2000	int
    
    • 1
    • 2
    • 3
    • 4
    • 5

    观察dd占用的内存,共8字节,前4字节是0c,转换成十进制是12,也就是基类C中age的大小;后4字节转换成十进制是2000,也就是派生类D中price的大小。

    0c 00 00 00 d0 07 00 00
    
    • 1

    观察二:多态类
    把上面例子基类C的虚函数定义为virtual虚函数,则C和D是多态类,打印:

    sizeof(C)=12
    sizeof(D)=16
    
    • 1
    • 2

    C和D的大小分别比非多态类大了8字节,多的8字节其实是指向虚函数表的指针,看下面,比上面多了个vptr。

    dd	@0x7fffffffe380	D
    	[C]	@0x7fffffffe380	C
    		[vptr]	_vptr.C	 
    		age	12	int
    		year	<optimized out>	
    	price	2000	int
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    观察dd占用的内存,共16字节,前8字节是指向虚函数表的指针;后8字节的前4字节转换10进制是12,也就是基类C中age的大小,后4字节转换成十进制是2000,也就是派生类D中price的大小。

    50 d9 55 55 55 55 00 00 0c 00 00 00 d0 07 00 00
    
    • 1

    一个总结
    1、一个类的对象所占用的空间大小:非静态成员变量之和,多态类再加上指向虚基表的指针大小。
    2、静态变量year是全局变量被优化,不占用类的大小。
    3、类D的对象dd,和基类C的指针地址一样。
    4、多态类占用空间比非多态类大8字节,多的8字节其实是指向虚函数表的指针[vptr]。
    5、创建一个派生类对象时,先执行基类的构造,再执行派生类的构造,因此内存分配中,前面是基类的非静态成员变量,后面是派生类新增的非静态成员变量。
    6、虚函数表指针是在基类构造时创建的,属于基类的一个成员,但派生类也可以访问。

    一个多态派生类的对象所占用的内存空间:
    在这里插入图片描述

    基类和派生类的成员归属

    访问范围
    1、保护成员的可访问范围比私有成员大,比共有成员小。能访问私有成员的地方都能访问保护成员。
    2、基类的私有成员只能在基类访问,派生类不能访问。
    3、基类的保护成员可以在派生类的成员函数访问。
    4、私有成员只能在类的成员函数访问,这和普通类的定义一致。

    覆盖和扩充
    1、派生类是对基类进行扩充和修改得到的,基类的所有成员自动成为派生类的成员(私有成员除外)。
    2、所谓扩充,指的是派生类中可以添加新的成员变量和成员函数。
    3、所谓覆盖,指的是派生类中可以重写从基类继承得到的成员。

    一个总结
    1、构造与析构顺序:构造时先执行基类的构造函数,再执行派生类的构造函数;析构时先执行派生类的析构函数,再执行基类的构造函数。
    2、基类的私有成员,不能在派生类的成员函数访问。
    3、基类的保护成员,可以在派生类的成员函数中访问。
    4、派生类可以定义和基类中同名的成员变量和非虚成员函数,比如例中的age,基类内存中有个age,派生类新增成员内存中也有一个age,这两个成员变量没有联系。
    5、派生类成员函数访问基类非私有成员,可以使用基类::访问。
    6、基类的析构函数要定义为虚函数,否则在释放基类指针时不会执行派生类的析构函数,造成隐式的内存泄漏。
    7、非多态情况下,派生类和基类是包含和被包含的关系,派生类包含了基类,因此派生类指针可以转换为基类指针,但基类指针不能转换为派生类指针(‘A’ is not polymorphic)。
    8、多态情况下,基类和派生类指针可以相互转换,但要关注转换后指针是否有效,可以使用dynamic_cast转换,返回nullptr则转换失败。

    
    //4字节对齐
    #pragma pack(push, 4)
    class A //基类
    {
    private:
        int price;//私有成员,只能在基类的成员函数访问
    protected:
        int age;//保护成员,可以在派生类的成员函数中访问
    public:
        char name[20]= "chw";//公有成员,可以在任何地方访问
        A()
        {
            price = 2000;
            age = 17;
            printf("A()\n");
        }
    
        virtual ~A()
        {
            printf("~A()\n");
        }
    
        void TestFunc()
        {
            printf("price=%d\n",price);
            printf("age=%d\n",age);
            printf("name=%s\n",name);
        }
    
        virtual void PrintThis()
        {
            printf("A=%p\n",this);
        }
    };
    
    class B : public A  //派生类
    {
    private:
        int age;//派生类中可以重写从基类继承得到的成员
        char addr[20];//派生类可以扩充新的成员变量
    public:
        B()
        {
            age = 27;
            printf("B()\n");
        }
    
        ~B()
        {
            printf("~B()\n");
        }
    
        //覆盖了基类的同名成员函数
        void TestFunc()
        {
            //不能访问基类的私有成员
    //        printf("price=%d\n",price);// error: 'price' is a private member of 'A'
    
            //可以访问基类的保护成员和公有成员
            printf("age=%d\n",age);
            printf("name=%s\n",name);
            printf("A::age=%d\n",A::age);//基类成员被派生类覆盖,可以使用A::访问基类的成员
    //        A::TestFunc();//使用A::也可以访问基类的同名成员函数
    
            printf("B=%p\n",this);
            A::PrintThis();
        }
    };
    
    //测试调用
        B* bb = new B;
        bb->TestFunc();
        printf("**********分割线***********\n");
        A* bb_a = dynamic_cast<A*>(bb);
        bb_a->TestFunc();
    
        printf("sizeof(A)=%ld\n",sizeof(A));
        printf("sizeof(B)=%ld\n",sizeof(B));
    
        delete bb;
    
        //基类不能转换为派生类,因为类A没有虚函数,不是多态的
        //如果类A成员函数TestFunc定义为virtual的,可以转换,但转换完成后aa_b==nullptr,不能使用
    //    A* aa = new A;
    //    B* aa_b = dynamic_cast(aa);//error: 'A' is not polymorphic
    #pragma pack(pop)
    
    • 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
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87

    打印

    A()
    B()
    age=27
    name=chw
    A::age=17
    B=0x5555559e15b0
    A=0x5555559e15b0
    **********分割线***********
    price=2000
    age=17
    name=chw
    sizeof(A)=36
    sizeof(B)=60
    ~B()
    ~A()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    内存占用

    bb	@0x5555559e15b0	B
    	[A]	@0x5555559e15b0	A
    		[vptr]	_vptr.A	 
    		age	17	int
    		name	"chw\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"	char[20]
    		price	2000	int
    	addr	"nj\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"	char[20]
    	age	27	int
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    打印分析:
    1、派生类和基类指针地址是一样的(0x5555559e15b0)。
    2、派生类重新定义了age,和基类的age是两个没有联系的变量。
    3、sizeof(A) = 虚函数表指针(8字节) + price(4字节) + age(4字节) + name(20字节) = 36。
    4、sizeof(B) = sizeof(A) + age(4字节) + addr(20字节) = 60。

    多态的实现

    多态的介绍参考:https://blog.csdn.net/weixin_40355471/article/details/124368317#_844

    通过基类指针或基类引用实现多态
    1、对于普通函数,不用管指针是指向基类还是派生类,只和指针变量的数据类型相关,即定义指针变量时的指针数据类型,如果是基类,则始终调用基类的普通函数,如果是派生类,则始终调用派生类的普通函数。
    2、对于虚函数,要看基类指针当前指向的是基类还是派生类,如果指向基类则调用基类的虚函数,如果指向派生类则调用派生类的虚函数。
    3、派生类指针可以赋值给基类指针,但基类指针赋值给派生类指针时要注意转换的有效性,通常使用dynamic_cast转换,失败时返回nullptr。
    4、因此通常使用基类指针或引用,根据基类指针是指向基类还是派生类,实现多态。

    class A
    {
    public:
         void out1()//普通函数
        {
            printf("A(out1)\n");
        };
        virtual ~A(){};
        virtual void out2()//虚函数
        {
            printf("A(out2)\n");
        }
    };
    
    class B:public A
    {
    public:
        virtual ~B(){};
        void out1()
        {
            printf("B(out1)\n");
        }
        void out2()
        {
            printf("B(out2)\n");
        }
    };
    
    //测试调用
        A *aa = new A;//基类指针,无论aa后面指向基类还是派生类,普通函数都是调用基类的普通函数
        B *bb = new B;//派生类指针
    
        aa->out1();//A(out1)
        aa->out2();//A(out2)
    
        bb->out1();//B(out1)
        bb->out2();//B(out2)
    
        aa = bb;//派生类指针赋值给基类指针
        bb = dynamic_cast<B*>(aa);//基类指针可以转换成派生类指针,转换失败时返回nullptr
    
        aa->out1();//A(out1)
        aa->out2();//B(out2)
        bb->out1();//B(out1)
        bb->out2();//B(out2)
    
    • 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
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    打印

    A(out1)
    A(out2)
    B(out1)
    B(out2)
    A(out1)
    B(out2)
    B(out1)
    B(out2)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
  • 相关阅读:
    转换类的具体使用教程
    解锁C语言结构体的力量(初阶)
    【SA8295P 源码分析】107 - AIS Camera 美信max96712解串器 - max9295加串器 寄存器初始化及工作过程详解
    Python Opencv实践 - 车牌定位(纯练手,存在失败场景,可以继续优化)
    c语言:初识指针
    centos 中:Nginx开启https和局域网访问配置
    Android源码——ComponentCallbacks源码解析
    Effective C++改善程序与设计的55个具体做法 4. 设计与声明
    SpringBoot项目调用openCV报错:nested exception is java.lang.UnsatisfiedLinkError
    Linux安装Redis 手把手教程
  • 原文地址:https://blog.csdn.net/weixin_40355471/article/details/133990934