• 一个类有多大


    c++中类所占的大小计算并没有想象中那么简单,因为涉及到虚函数成员,静态成员,虚继承,多继承以及空类等,不同情况有对应的计算方式,在此对各种情况进行总结。


    首先要明确一个概念,平时所声明的类只是一种类型定义,它本身是没有大小可言的。 我们这里指的类的大小,其实指的是类的对象所占的大小。因此,如果用sizeof运算符对一个类型名操作,得到的是具有该类型实体的大小。

    关于类/对象大小的计算

    首先,类大小的计算遵循结构体的对齐原则
    类的大小与普通数据成员有关,与成员函数和静态成员无关。即普通成员函数,静态成员函数,静态数据成员,静态常量数据成员均对类的大小无影响
    虚函数对类的大小有影响,是因为虚函数表指针带来的影响
    虚继承对类的大小有影响,是因为虚基表指针带来的影响
    空类的大小是一个特殊情况,空类的大小为1
    解释说明

    静态数据成员之所以不计算在类的对象大小内,是因为类的静态数据成员被该类所有的对象所共享,并不属于具体哪个对象,静态数据成员定义在内存的全局区。
    空类的大小,以及含有虚函数,虚继承,多继承是特殊情况,接下来会一一举例说明


    注意:因为计算涉及到内置类型的大小,接下来的例子运行结果是在64位gcc编译器下得到的。int的大小为4,指针大小为8

    一.简单情况的计算

    #include<iostream>
    using namespace std;
    
    
    class base
    {
    public:
    base()=default;
    ~base()=default;
    private:
    static int a;
    int b;
    char c;
    
    };
    
    
    int main()
    {
    base obj;
    cout<<sizeof(obj)<<endl;
    }

    计算结果:8
    静态变量a不计算在对象的大小内,由于字节对齐,结果为4+4=8

    二.空类的大小

    本文中所说是C++的空类是指这个类不带任何数据,即类中没有非静态(non-static)数据成员变量,没有虚函数(virtual function),也没有虚基类(virtual base class)。

    直观地看,空类对象不使用任何空间,因为没有任何隶属对象的数据需要存储。然而,C++标准规定,凡是一个独立的(非附属)对象都必须具有非零大小。换句话说,c++空类的大小不为0
    为了验证这个结论,可以先来看测试程序的输出。

    #include <iostream>
    using namespace std;
    
    class NoMembers
    {
    };
    
    int main()
    {
    NoMembers n; // Object of type NoMembers.
    cout << "The size of an object of empty class is: "
    << sizeof(n) << endl;
    }

    输出:

    The size of an object of empty class is: 1
    1
    C++标准指出,不允许一个对象(当然包括类对象)的大小为0,不同的对象不能具有相同的地址。这是由于:

    new需要分配不同的内存地址,不能分配内存大小为0的空间
    避免除以 sizeof(T)时得到除以0错误
    故使用一个字节来区分空类。

    但是,有两种情况值得我们注意

    第一种情况,涉及到空类的继承。
    当派生类继承空类后,派生类如果有自己的数据成员,而空基类的一个字节并不会加到派生类中去。例如

    class Empty {};
    struct D : public Empty { int a;};

    1
    2
    sizeof(D)为4。

    第二中情况,一个类包含一个空类对象数据成员。

    class Empty {};
    class HoldsAnInt {
    int x;
    Empty e;
    };

    sizeof(HoldsAnInt)为8。
    因为在这种情况下,空类的1字节是会被计算进去的。而又由于字节对齐的原则,所以结果为4+4=8。

    继承空类的派生类,如果派生类也为空类,大小也都为1。


    三.含有虚函数成员

    首先,要介绍一下虚函数的工作原理:

      虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。编译器必需要保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)
      每当创建一个包含有虚函数的类或从包含有虚函数的类派生一个类时,编译器就会为这个类创建一个虚函数表(VTABLE)保存该类所有虚函数的地址,其实这个VTABLE的作用就是保存自己类中所有虚函数的地址,可以把VTABLE形象地看成一个函数指针数组,这个数组的每个元素存放的就是虚函数的地址。在每个带有虚函数的类 中,编译器秘密地置入一指针,称为v p o i n t e r(缩写为V P T R),指向这个对象的V TA B L E。 当构造该派生类对象时,其成员VPTR被初始化指向该派生类的VTABLE。所以可以认为VTABLE是该类的所有对象共有的在定义该类时被初始化;而VPTR则是每个类对象都有独立一份的,且在该类对象被构造时被初始化

    假设我们有这样的一个类:

    class Base {
    public:
    virtual void f() { cout << "Base::f" << endl; }
    
    virtual void g() { cout << "Base::g" << endl; }
    
    virtual void h() { cout << "Base::h" << endl; }
    };

    当我们定义一个这个类的实例,Base b时,其b中成员的存放如下:



    指向虚函数表的指针在对象b的最前面。

    虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符””一样,其标志了虚函数表的结束。这个结束标志的值在不同的编译器下是不同的。在vs下,这个值是NULL。而在linux下,这个值是如果1,表示还有下一个虚函数表,如果值是0,表示是最后一个虚函数表。
    因为对象b中多了一个指向虚函数表的指针,而指针的sizeof是8,因此含有虚函数的类或实例最后的sizeof是实际的数据成员的sizeof加8

    例如:

    class Base {
    public:
    int a;
    
    virtual void f() { cout << "Base::f" << endl; }
    
    virtual void g() { cout << "Base::g" << endl; }
    
    virtual void h() { cout << "Base::h" << endl; }
    };

    sizeof(Base)为16。
    vptr指针的大小为8,又因为对象中还包含一个int变量,字节对齐得8+8=16。


    下面将讨论针对基类含有虚函数的继承讨论:

    (1)在派生类中不对基类的虚函数进行覆盖,同时派生类中还拥有自己的虚函数,比如有如下的派生类:

    class Derived: public Base
    {
    public:
    
    virtual void f1() { cout << "Derived::f1" << endl; }
    
    virtual void g1() { cout << "Derived::g1" << endl; }
    
    virtual void h1() { cout << "Derived::h1" << endl; }
    };

    基类和派生类的关系如下:


    当定义一个Derived的对象d后,其成员的存放如下:



    可以发现:

    1)虚函数按照其声明顺序放于表中

    2)基类的虚函数在派生类的虚函数前面

    此时基类和派生类的sizeof都是数据成员的大小+指针的大小8。

    (2)在派生类中对基类的虚函数进行覆盖,假设有如下的派生类:

    class Derived: public Base
    {
    public:
    
    virtual void f() { cout << "Derived::f" << endl; }
    
    virtual void g1() { cout << "Derived::g1" << endl; }
    
    virtual void h1() { cout << "Derived::h1" << endl; }
    };

    基类和派生类之间的关系:其中基类的虚函数f在派生类中被覆盖了


    当我们定义一个派生类对象d后,其d的成员存放为:

    可以发现:

    1)覆盖的f()函数被放到了虚表中原来基类虚函数的位置。

    2)没有被覆盖的函数依旧。

    派生类的大小仍是基类和派生类的非静态数据成员的大小+一个vptr指针的大小

    这样,我们就可以看到对于下面这样的程序,

    Base *b = new Derive();

    b->f();

    由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。


    (3)多继承:无虚函数覆盖
    假设基类和派生类之间有如下关系:

    对于派生类实例中的虚函数表,是下面这个样子:


    我们可以看到:

    1) 每个基类都有自己的虚表。

    2) 派生类的成员函数被放到了第一个基类的表中。(所谓的第一个基类是按照声明顺序来判断的)

    由于每个基类都需要一个指针来指向其虚函数表,因此d的sizeof等于d的数据成员加上三个指针的大小


    (4)多重继承,含虚函数覆盖
    假设,基类和派生类又如下关系:派生类中覆盖了基类的虚函数f

    下面是对于派生类实例中的虚函数表的图:

    我们可以看见,三个基类虚函数表中的f()的位置被替换成了派生类的函数指针。这样,我们就可以任一静态类型的基类类来指向派生类,并调用派生类的f()了。如:

    Derive d;

    Base1 *b1 = &d;

    Base2 *b2 = &d;

    Base3 *b3 = &d;

    b1->f(); //Derive::f()

    b2->f(); //Derive::f()

    b3->f(); //Derive::f()

    b1->g(); //Base1::g()

    b2->g(); //Base2::g()

    b3->g(); //Base3::g()

    此情况派生类的大小也是类的所有非静态数据成员的大小+三个指针的大小


    举一个例子具体分析一下大小吧:

    #include<iostream>
    using namespace std;
    
    class A
    {
        //1B
    };
    
    class B
    {
        char ch;
        virtual void func0() { }
        //vptr 8B
        // ch 1B+7B
        //16B
    };
    
    class C
    {
        char ch1;
        char ch2;
        virtual void func() { }
        virtual void func1() { }
        //vptr 8B
        // ch 1B+1B+6B
        //16B
    };
    
    class D: public A, public C
    {
        int d;
        virtual void func() { }
        virtual void func1() { }
        //vptr 8B
        // ch1,ch2 1B+1B+2B
        //d 4B
        //16B
    };
    class E: public B, public C
    {
        int e;
        virtual void func0() { }
        virtual void func1() { }
        //vptrB 8B
        //vptrC 8B
        // ch 1B+3B
        //ch1;ch2; 2B+2B
        //e 4B+4B
        //32B
    };
    
    int main(void)
    {
        cout<<"A="<<sizeof(A)<<endl; //result=1
        cout<<"B="<<sizeof(B)<<endl; //result=16
        cout<<"C="<<sizeof(C)<<endl; //result=16
        cout<<"D="<<sizeof(D)<<endl; //result=16
        cout<<"E="<<sizeof(E)<<endl; //result=32
        return 0;
    }

    结果分析:
    1.A为空类,所以大小为1
    2.B的大小为char数据成员大小+vptr指针大小。由于字节对齐,大小为8+8=16
    3.C的大小为两个char数据成员大小+vptr指针大小。由于字节对齐,大小为8+8=16
    4.D为多继承派生类,由于D有数据成员,所以继承空类A时,空类A的大小1字节并没有计入当中,D继承C,此情况D只需要一个vptr指针,所以大小为数据成员加一个指针大小。由于字节对齐,大小为8+8=16
    5.E为多继承派生类,此情况为我们上面所讲的多重继承,含虚函数覆盖的情况。此时大小计算为数据成员的大小+2个基类虚函数表指针大小
    考虑字节对齐,结果为8+8+2*8=32

    四.虚继承的情况

    对虚继承层次的对象的内存布局,在不同编译器实现有所区别。
    在这里,我们只说一下在gcc编译器下,虚继承大小的计算。

    它在gcc下实现比较简单,不管是否虚继承,GCC都是将虚表指针在整个继承关系中共享的,不共享的是指向虚基类的指针

    class A {
    
    int a;
    
    virtual void myfunA(){}
    
    };
    
    class B:virtual public A{
    
    virtual void myfunB(){}
    
    };
    
    class C:virtual public A{
    
    virtual void myfunC(){}
    
    };
    
    class D:public B,public C{
    
    virtual void myfunD(){}
    
    };

    以上代码中sizeof(A)=16,sizeof(B)=24,sizeof(C)=24,sizeof(D)=32.
    解释:A的大小为int大小加上虚表指针大小。B,C中由于是虚继承因此大小为int大小加指向虚基类的指针的大小。B,C虽然加入了自己的虚函数,但是虚表指针是和基类共享的,因此不会有自己的虚表指针,他们两个共用虚基类A的虚表指针。D由于B,C都是虚继承,因此D只包含一个A的副本,于是D大小就等于int变量的大小+B中的指向虚基类的指针+C中的指向虚基类的指针+一个虚表指针的大小,由于字节对齐,结果为8+8+8+8=32。


    c++类的大小计算

    ---*****old----
    #include <iostream>
    using namespace std;

    class A{};

    class B
    {
    int b;
    char c;
    };

    class C
    {
    //int c1;

    char c1;
    static int c2;
    };
    int C::c2 = 1;

    class D:public C,public B{
    int d;
    };
    int main()
    {
    cout<<"sizeof(A)="<<sizeof(A)<<endl;
    cout<<"sizeof(B)="<<sizeof(B)<<endl;
    cout<<"sizeof(C)="<<sizeof(C)<<endl;
    cout<<"sizeof(D)="<<sizeof(D)<<endl;

    return 0;
    }

    运行结果为:

     

    sizeof(A)=1

    sizeof(B)=8

    //sizeof(C)=4

    sizeof(C)=1

    sizeof(D)=16


    对于类A来说,虽然A是一个空类,但为了便于空类进行实例化,编译器往往会给它分配一个字节,这样A实例化后便在内存中有了一个独一无二的地址.对于类B,B的大小应为sizeof(int)+sizeof(char)=5,但是考虑内存对齐,B的大小应为8.对于类C,类的静态成员变量被放在全局区,和类的普通成员并没有放在一块。类的静态成员被声明后就已存在,而非静态成员只有类被实例化后才存在。所以C的大小为sizeof(char)=1。D的大小为B+C的大小+自身数据成员的大小,一共为[8+1+4]=16.

    http://www.cnblogs.com/luxiaoxun/archive/2012/09/01/2666395.html

    C++类的大小

     

    一个空类class A{};的大小为什么是1,因为如果不是1,当定义这个类的对象数组时候A objects[5]; objects[0]和objects[1]就在同一个地址处,就无法区分。

    单继承

    复制代码
    #include<iostream>
    using namespace std;
    class A
    {
    public:
        virtual void aa(){}
    private:
        char k[3];
    };
    
    class B: public A
    {
    public:
        virtual void bb(){}
    };
    
    int main()
    {
        cout<<"A's size is "<<sizeof(A)<<endl;
        cout<<"B's size is "<<sizeof(B)<<endl;
        return 0;
    }
    复制代码

    vs和gcc下
    执行结果:A's size is 8
                  B's size is 8

    说明:有虚函数的类有个virtual table(虚函数表),里面包含了类的所有虚函数,类中有个virtual table pointers,通常成为vptr指向这个virtual table,占用4个字节的大小。成员类B public继承于A,类B的虚函数表里实际上有两个虚函数A::aa()和B::bb(),类B的大小等于char k[3]的大小加上一个指向虚函数表指针vptr的大小,考虑内存对齐为8。

    复制代码
    #include<iostream>
    using namespace std;
    class A
    {
    public:
        virtual void aa(){}
    private:
        char k[3];
    };
    
    class B: public A
    {
    public:
        //virtual void bb(){}
    };
    
    int main()
    {
        cout<<"A's size is "<<sizeof(A)<<endl;
        cout<<"B's size is "<<sizeof(B)<<endl;
        return 0;
    }
    复制代码

    vs和gcc下
    执行结果:A's size is 8
                  B's size is 8
    说明:类B看上去没有虚函数,但实际上它有,只是没有重写,因为public继承,所以有从A继承过来的虚函数A::aa(),实际上类A和类B的虚函数表里的函数都是A::aa()。

    复制代码
    #include<iostream>
    using namespace std;
    class A
    {
    public:
        virtual void aa(){}
        virtual void aa2(){}
    private:
        char k[3];
    };
    
    class B: public A
    {
    public:
        virtual void bb(){}
        virtual void bb2(){}
    };
    
    int main()
    {
        cout<<"A's size is "<<sizeof(A)<<endl;
        cout<<"B's size is "<<sizeof(B)<<endl;
        return 0;
    }
    复制代码

    vs和gcc下
    执行结果:A's size is 8
                  B's size is 8

    说明:一个类里若有虚函数,无论有多少个虚函数都只有一个指向虚表的指针,虚表中的每一个表项保存着一个虚函数的入口地址。当调用虚函数时,先找到虚表中它对应的表项,找到入口地址再执行。对于直接单继承,无论类B中有无虚函数,由于它继承了类A,且类A里含有虚函数,因此如果类B有虚函数,那么它和类A的是在同一个属于类B的虚表里,这张虚表的虚函数为A::aa()、A::aa2()、B::bb()、B::bb2()。注意:类A里的私有成员在类B里仍占有内存。

    多继承

    复制代码
    #include<iostream>
    using namespace std;
    class A
    {
    public:
        virtual void aa(){}
        virtual void aa2(){}
    private:
        char k[3];
    };
    
    class B
    {
    public:
        virtual void bb(){}
        virtual void bb2(){}
    };
    
    class C: public A,public B
    {
    public:
        virtual void aa(){} //重写了A的aa()
        virtual void cc(){}
    };
    
    int main()
    {
        cout<<"A's size is "<<sizeof(A)<<endl;
        cout<<"B's size is "<<sizeof(B)<<endl;
        cout<<"C's size is "<<sizeof(C)<<endl;
        return 0;
    }
    复制代码

    vs和gcc下
    执行结果:A's size is 8
              B's size is 4
              B's size is 12

    说明:类A和B的大小就不解释了,参照上面。类C多重继承于A和B(有虚函数覆盖),那么类C的大小是多少?先看成员变量,有一个继承A的char k[3]。再看虚函数,类C的中虚函数是怎么分布的?先有一个虚函数表,里面有继承于类A的虚函数和C自己的虚函数(C::aa(), A::aa2(), C::cc()),如果C没有重写aa(),那么第一个虚函数就是A::aa(),接着有第二张虚函数表是继承包含类B的虚函数B::bb()、B::bb2()(类C没有重写B的虚函数)。总的大小就是2张虚表的大小(也即两个虚函数指针的大小)8字节加上3字节的k[3],考虑内存对齐,就是12字节。

    虚继承

    复制代码
    #include<iostream>
    using namespace std;
    class A
    {
    public:
        virtual void aa(){}
    private:
        char k[3];
    };
    
    class B: virtual public A
    {
    public:
        //virtual void bb(){}
    };
    
    int main()
    {
        cout<<"A's size is "<<sizeof(A)<<endl;
        cout<<"B's size is "<<sizeof(B)<<endl;
        return 0;
    }
    复制代码

    vs和gcc下
    执行结果:A's size is 8
                  B's size is 12
    说明:类B里包含,继承的char k[3],继承的虚函数,类B的虚函数表里有A::aa(),因为是虚继承,还有一个指向父类的指针,该指针为指向虚基类的指针(Pointer to virtual base class)。考虑内存对齐,总大小为12。

    复制代码
    #include<iostream>
    using namespace std;
    class A
    {
    public:
        virtual void aa(){}
    private:
        char k[3];
    };
    
    class B: public virtual A
    {
    public:
        virtual void bb(){}
    };
    
    int main()
    {
        cout<<"A's size is "<<sizeof(A)<<endl;
        cout<<"B's size is "<<sizeof(B)<<endl;
        return 0;
    }
    复制代码

    VS执行结果:A's size is 8
                      B's size is 16

    gcc执行结果:A's size is 8
                      B's size is 12

    说明:对于虚继承,类B虚继承类A时,首先要通过加入一个指针来指向父类A,该指针被称为虚基类指针。然后包含从父类继承过来的3个char,再加上一个虚函数指针。考虑内存对齐,在gcc下结果是4+4+4=12。在VS下,结果是16,why?这一题和上一题区别只是在类B中添加了一个虚函数,但是两个题目中类B都有虚函数表。在VS下调试查看汇编代码,发现多出来的4字节什么也没有。

     

  • 相关阅读:
    Linux基础(Ubuntu16.04):安装vim及配置
    Ubuntu16.04 安装ROS及其IDE
    python格式转换的记录
    python爬虫selenium相关
    【LAMP】搭建Web网站过程中的记录【Ubuntu18.04+Apache2.4+PHP7.2+MySQL5.7】
    【疯狂挖坑】linux服务器尝试中的问题(nohup等)
    逻辑回归与全连接神经网络联系的简单理解记录
    Unity3d开发中遇到的问题记录
    python中numpy库的一些使用
    Deep Learning论文翻译(Nature Deep Review)
  • 原文地址:https://www.cnblogs.com/guxuanqing/p/4918862.html
Copyright © 2020-2023  润新知