[基础知识]2.关于size of(空类)的三个问题及其扩展

版权声明:By TimepassbyZ 转载请注明出处! https://blog.csdn.net/TimepassbyZ/article/details/84898226

1.sizeof(没有任何成员变量和成员函数的空类)是几,为什么?

  • 是1B。空类型的实例中不包含任何信息,本来求sizeof应该是0,但是当我们声明该类型的实例的时候,它必须在内存中占有一定的空间,否则无法使用这些实例。至于占用多少内存,由编译器决定。例如:在Code::Blocks和Visual Studio中每个空类型的实例占1B。
    注意:一旦类中有其他的占用空间成员,则这1个字节就不在计算之内。

2.sizeof(没有任何成员变量但有一个构造函数和析构函数的类)是几,为什么?

  • 是1B。调用构造函数和析构函数只需要直到函数的地址即可,而这些函数的地址只与类型相关,而与类型的实例无关,编译器也不会因为这两个函数而在实例内添加任何额外的信息。
    注意:如果有其他成员函数(非虚函数),则还是只占用1个字节。

3.sizeof(没有任何成员变量但有一个构造函数和虚析构函数的类)是几,为什么?

  • 是4B。C++的编译器一旦发现一个类型中有虚函数,就会为该类型生成虚函数表,并在该类型的每一个实例中添加一个指向虚函数表的指针。在32位的机器上,一个指针占4字节的空间,如果在64位的机器上,一个指针占8字节的空间。
    注意:虚函数表是C++实现多态的一种机制。

总结1

  1. C++标准规定类的大小不为0,空类的大小为1,当类不包含虚函数和非静态数据成员时,其对象大小也为1。
  2. 如果在类中声明了虚函数(不管是1个还是多个),那么在实例化对象时,编译器会自动在对象里安插一个指针指向虚函数表VTable;在32位机器上,一个对象会增加4个字节来存储此指针,它是实现面向对象中多态的关键。
  3. 虚函数本身和其他成员函数一样,是不占用对象的空间的。虚函数本身和其他成员函数一样,是不占用对象的空间的。

相关知识点

  1. 由于类只是一个类型定义,没有大小可言,因此, 用sizeof运算符对一个类型名操作时,得到的实际上是该类型实例的大小。
  2. 一个C++的空类,即使没有任何成员变量,编译器也会自动生成默认构造函数、默认拷贝构造函数、默认析构函数、默认赋值函数、默认取值函数。
  3. 构造函数是一种特殊的成员函数, 主要用于为对象分配空间,进行初始化。
  4. 构造函数没有返回值(不能说明为void类型),可以被重载,不能为虚函数。
  5. 默认构造函数不带任何参数,函数体是空的,它只能为对象开辟数据成员存储空间,而不能给对象中的数据成员赋初值。
  6. 拷贝构造函数是一种特殊的构造函数,其形参是本类对象的引用。用于在建立一个新对象时,使用一个已经存在的对象去初始化这个新对象。
  7. 默认拷贝构造函数用于复制出数据成员值完全相同的新对象。
  8. 析构函数是一种特殊的成员函数,它执行与构造函数相反的操作,通常用于执行一些清理任务,如释放分配给对象的内存空间等。
  9. 析构函数没有返回值(不能说明为void类型),不能被重载,可以为虚函数。
  10. 采用默认赋值函数实现的数据成员逐域赋值的方法是一种浅层复制方法。通常,默认赋值函数是能够胜任工作的。但是,对于类似指针悬挂的问题来说,还需要用户根据实际自己对赋值运算符进行重载(进行深层复制)。
  11. 浅拷贝:源对象的指针和拷贝对象的指针都指向同一个空间;
    深拷贝:源对象的指针指向一个空间,拷贝对象的指针指向另一个新的空间(两个空间的数据成员相同)。
  12. 虚函数允许函数调用与函数体之间的联系在运行时才建立,也就是在运行时才决定如何动作,即所谓的动态联编。
  13. 虚函数的作用是允许在派生类中定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。其定义是在基类中进行的。

扩展
下列sizeof()的大小分别是?

class Base{}; // sizeof(Base)?
class Derived:public Base{
private:
	int a;
} // sizeof(Derived)?

sizeof(Base) = 1;

sizeof(Derived) = 4;

  • 在空基类被继承后,子类会优化掉空基类的1字节大小,从而节省空间大小,提高运行效率。

class Base1{
private:
    char a;
    int b;
    char c;
}; // sizeof(Base1)?
class Base2{
private:
    char a;
    char b;
    int c;
}; // sizeof(Base2)?

sizeof(Base1) = 12;

  • 首先,char a从0偏移开始存储,占一个字节,即占用0空间,现在可用偏移为1偏移;
    接下来存int b,由于1不是四的倍数,所以向后偏移2、3,都不是四的倍数,偏移到4时,4是四的倍数,所以,b从4空间开始存储,占四个字节,即占用4、5、6、7空间,现在可用偏移为8偏移;
    然后存char c,由于8是一的倍数,所以,c从8空间开始存储,占一个字节,即占用8空间,现在可用偏移为9偏移;
    最后,由于9不是类型最大字节数四的倍数,所以向后偏移10、11,都不是四的倍数,偏移到12时,12是四的倍数,因此,该类大小为12个字节。(如图1所示)
    图1【sizeof(Base1)】:
    图1

sizeof(Base2) = 8;

  • 首先,char a从0偏移开始存储,占一个字节,即占用0空间,现在可用偏移为1偏移;
    接下来存char b,由于1是一的倍数,所以,b从1空间开始存储,占一个字节,即占用1空间,现在可用偏移为2偏移;
    然后存int c,由于2不是四的倍数,所以向后偏移,3不是四的倍数,偏移到4时,4是四的倍数,所以,c从4空间开始存储,占四个字节,即占用4、5、6、7空间,现在可用偏移为8偏移;
    最后,由于8是类型最大字节数四的倍数,因此,该类大小为8个字节。(如图2所示)
    图2【sizeof(Base2)】:
    图2

class Base{
private:
    char a;
public:
    virtual void f();
    virtual void g();
}; // sizeof(Base)?
class Derived1:public Base{
private:
    int b;
public:
    void f();
}; // sizeof(Derived1)?
class Derived2:public Base{
private:
    int c;
public:
    void g();
    virtual void h();
}; // sizeof(Derived2)?

sizeof(Base) = 8;

  • 只要含虚函数,一定有虚函数表指针(vptr),而且该指针一定位于类内存模型最前端。
    首先,vptr从0偏移开始存储,占四个字节,即占用0、1、2、3空间,现在可用偏移为4偏移;
    接下来存char a,由于4是一的倍数,所以,a从4空间开始存储,占一个字节,即占用4空间,现在可用偏移为5偏移;
    最后,由于5不是类型最大字节数四的倍数,所以向后偏移6、7,都不是四的倍数,偏移到8时,8是四的倍数,因此,该类大小为8个字节。(如图3所示)
    图3【sizeof(Base)】:
    图3

sizeof(Derived1) = 12;

  • 虽然vtbl中的Base::f()已经被替换为Derived1::f(),但是vptr并没有改变。
    首先,vptr从0偏移开始存储,占四个字节,即占用0、1、2、3空间,现在可用偏移为4偏移;
    接下来存基类成员char a,由于4是一的倍数,所以,a从4空间开始存储,占一个字节,即占用4空间,现在可用偏移为5偏移;
    然后存int b,由于5不是四的倍数,所以向后偏移6、7,都不是四的倍数,偏移到8时,8是四的倍数,所以,b从8空间开始存储,占四个字节,即占用8、9、10、11空间,现在可用偏移为12偏移;
    最后,由于12是类型最大字节数四的倍数,因此,该类大小为12个字节。(如图4所示)
    图4【sizeof(Derived1)】:
    图4

sizeof(Derived2) = 12;

  • 虽然vtbl中的Base::g()已经被替换为Derived2::g(),并且新添加了虚函数h(),但是vptr并没有改变。
    首先,vptr从0偏移开始存储,占四个字节,即占用0、1、2、3空间,现在可用偏移为4偏移;
    接下来存基类成员char a,由于4是一的倍数,所以,a从4空间开始存储,占一个字节,即占用4空间,现在可用偏移为5偏移;
    然后存int c,由于5不是四的倍数,所以向后偏移6、7,都不是四的倍数,偏移到8时,8是四的倍数,所以,c从8空间开始存储,占四个字节,即占用8、9、10、11空间,现在可用偏移为12偏移;
    最后,由于12是类型最大字节数四的倍数,因此,该类大小为12个字节。(如图5所示)
    图5【sizeof(Derived2)】:
    图5

class Base1{
private:
    char a;
public:
    virtual void f();
    virtual void x();
}; // sizeof(Base1)?
class Base2{
private:
    int b;
public:
    virtual void f();
    virtual void y();
}; // sizeof(Base2)?
class Base3{
private:
    double c;
public:
    virtual void f();
    virtual void z();
}; // sizeof(Base3)?
class Derived:public Base1, public Base2, public Base3{
private:
    double d;
public:
    void f();
    virtual void derived_func();
}; // sizeof(Derived)?

sizeof(Base1) = 8;

sizeof(Base2) = 8;

sizeof(Base3) = 16;

sizeof(Derived) = 40;

  • 由于Derived类的虚函数表指针与声明继承顺序的第一个基类Base1的虚函数表指针合并,所以,
    vtbl1中的Base1::f()被替换为Derived::f(),并且添加了新的虚函数derived_func();
    vtbl2中的Base2::f()被替换为Derived::f();
    vtbl3中的Base3::f()被替换为Derived::f();
    而Derived中新添加的成员变量位于类的最后面。(如图6所示)
    图6【sizeof(Derived)】:
    图6

class Base{
public:
    int a;
    virtual void f();
}; // sizeof(Base)?
class Base1:virtual public Base{
public:
    int b;
    virtual void x();
}; // sizeof(Base1)?
class Base2:virtual public Base{
public:
    int c;
    virtual void y();
}; // sizeof(Base2)?
class Derived:public Base1,public Base2{
private:
    double d;
    void f();
    virtual void z();
}; // sizeof(Derived)?

注意:不同编译器下虚继承对类大小的影响是不同的!

在vs环境下,采用虚拟继承的继承类会有自己的虚函数表指针(假如基类有虚函数,并且继承类添加了自己新的虚函数)
在gcc环境下及mac下使用clion,采用虚拟继承的继承类没有自己的虚函数表指针(假如基类有虚函数,无论添加自己新的虚函数与否),而是共用父类的虚函数表指针
详情请看:虚拟继承对类大小的影响

这里以32位GCC环境为例:
sizeof(Base) = 8;

sizeof(Base1) = 16;
sizeof(Base2) = 16;

  • 虚继承条件下,基类内存位于类的最后。
    虚拟继承会给继承类添加一个虚基类指针(virtual base ptr 简称vbptr),其位于类虚函数指针后面,成员变量前面;
    若基类没有虚函数,则vbptr其位于继承类的最前端。(如图7所示)
    图7【sizeof(Base1)】:
    图7

sizeof(Derived) = 32;

  • 内存顺序:
    Base1的虚函数表指针(GCC无|VS有)->Base1的虚基类指针->Base1的成员变量->Base2的虚函数表指针(GCC无|VS有)->Base2的虚基类指针->Base2的成员变量->Derived的成员变量->Base的虚函数表指针->Base的成员变量。(如图8所示)
    图8【sizeof(Derived)】:
    图8

总结2

  1. 为了优化存取效率,需要进行边缘调整(对齐)。
  2. 类的大小等于类的非静态成员数据类型的大小之和。
  3. 类的大小只与它当中的成员数据有关,与类中的构造函数、析构函数以及普通成员函数无关(虚函数除外)。
  4. 前面的地址必须是后面地址的整数倍,不是就补齐。
  5. 整个类的地址必须是最大字节的整数倍。
  6. 每个含有虚函数的类在内存中都会多一根指针(vptr),它存储的是虚函数表(vtbl)所在的位置。
  7. 虚函数表(vtbl)存储着所有虚函数的位置,由于其动态绑定特性,在覆写(override)后在子类中存储的虚函数位置与父类中不相同。
  8. 虚继承主要是为了解决菱形继承下公共基类的多份拷贝问题(即二义性问题和重复继承下的空间浪费问题)。

参考文章
sizeof(空类)问题总结
sizeof() 类大小,空类大小
类可以没有构造函数和析构函数吗
C++类大小详尽讲解
结构体深度剖析(内存对齐,对齐参数,偏移量)
C++对象内存模型2 (虚函数,虚指针,虚函数表)

猜你喜欢

转载自blog.csdn.net/TimepassbyZ/article/details/84898226