C++类的sizeof大小

C++ 类的sizeof大小

  涉及一些内存对齐的知识

声明: 本文涉及的编译环境为 g++ 4:7.4.0-1ubuntu2.3-std=c++11,系统为 Ubuntu18.04 64位 系统,编

1. 空类

class Base{};

image.png
  大小为 $1$,类的实例化就是给每一个实例在内存中分配一块地址。空类被实例化时,会由编译器隐含的添加一个字节。所以空类的 $size$ 为 $1$。

2. 虚函数

class Base{
    virtual void fun1(){};
    virtual void fun2(){};
    virtual void fun3(){};
};

image.png
  当 $C++$ 类中有虚函数的时候,会有一个指向虚函数表 $(V-table)$ 的指针,所有的虚函数都在这个表中。指针大小为 $8$,所以 $size$ 为 $8$。

内存对齐

class A{
public:
    char name[5];
    int num;
    short score;
};

class B {
    int num;
    char name[5];
    short score;
};

image.png

基本概念

  字节对齐:计算机存储系统中以Byte为单位存储数据,不同数据类型所占的空间不同,如:整型(int)数据占4个字节,字符型(char)数据占一个字节,短整型(short)数据占两个字节,等等。计算机为了快速的读写数据,默认情况下将数据存放在某个地址的起始位置,如:整型数据(int)默认存储在地址能被4整除的起始位置,字符型数据(char)可以存放在任何地址位置(被1整除),短整型(short)数据存储在地址能被2整除的起始位置。这就是默认字节对齐方式。

基本满足以下3个原则:

  • 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
  • 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal padding);
  • 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)。

举例说明

class A{
public:
    char name[5];
    int num;
    short score;
};

class B {
    int num;
    char name[5];
    short score;
};

  上述代码中,类 $A$ 中只用了 11bytes(5+4+2)的空间,但是由于 $int$ 型默认 $4$ 字节对齐,存放在地址能被4整除的起始位置,即:如果 $name[5]$ 从 $0$ 开始存放,它占 $5bytes$ ,而 $num$ 则从第8(偏移量)个字节开始存放。所以 $sizeof(A) = 16$。于是中间空出几个字节闲置着。但这样便于计算机快速读写数据,是一种以空间换取时间的方式。其数据对齐如下图:
|char|char|char|char|
|char|----|----|----|
|--------int--------|
|--short--|----|----|

如果我们将结构体中变量的顺序改变类 $B$ 则对齐方式如下:
  $num$ 从 $0$ 开始存放,而 $name$ 从第4(偏移量)个字节开始存放,连续5个字节,$score$ 从第 $10$(偏移量)开始存放,故 $sizeof(B) = 12$。其数据对齐如下图:
|--------int--------|
|char|char|char|char|
|char|----|--short--|

如果我们将 $int$ 换成虚函数,会事什么结果呢

class A{
public:
    char name[5];
    virtual int a(){}
    short score;
};

class B {
    virtual int a(){}
    char name[5];
    short score;
};

image.png
  上述代码的大小均为 $16$ 字节,但是和上面的对齐方式不同。这是因为,为了效率问题,编译器(gcc 和 微软)一般会把虚指针放在类的内存空间的最前面的位置,不管虚函数声明的位置。

  为了节省空间,我们可以在编码时通过 #pragma pack() 命令指定程序的对齐方式,括号中是对齐的字节数,若该命令括号中的内容为空,则为默认对齐方式。例如,对于上面第一个结构体,如果通过该命令手动设置对齐字节数如下:

#pragma pack(2)
class A{
public:
    char name[5];
    int a();
    short score;
};
#pragma pack()

image.png
  $num$ 从第 $6$(偏移量)个字节开始存放,$score$ 从第 $10$(偏移量)个字节开始存放,故 $sizeof(A) = 12$,其数据对齐如下图:
|char|char|
|char|char|
|char|-----|
|----int----|
|----int----|
|--short---|

  这样改变默认的字节对齐方式可以更充分地利用存储空间,但是这会降低计算机读写数据的速度,是一种以时间换取空间的方式。

3. 静态数据成员

class A{
public:
    char b;
    virtual void fun() {}
    static int c;
};

image.png
  静态数据成员被编译器放在程序的一个 global data members 中,它是类的一个数据成员,但不影响类的大小。不管这个类产生了多少个实例,还是派生了多少新的类,静态数据成员只有一个实例。静态数据成员,一旦被声明,就已经存在。 考虑到数据对齐, 最终是 $16$ 字节。

4. 普通成员函数

class A{
public:
    void fun() {}
};

image.png
类的大小与构造函数,析构函数,普通成员函数无关。

5. 普通单继承

class A{
public:
    int a;
};

class B:public A {
public:
    int b;
};

image.png
可以看到普通的继承就是基类的大小+派生类自身的大小。注意数据对齐。
注意类的数据成员按其声明顺序加入内存,无访问权限无关,只看声明顺序。

class A{
public:
    int a;
    char b;
};

class B:public A {
public:
    char c;
};

上面这段代码,不同的编译器结果不同。
image.png
A的大小为8,对齐值为4, 则考虑总体对齐 8 + 1 + 3(padding) = 12

6. 含虚函数的单继承

class A {
    virtual void fun () {}
};
class B : public A {
public:
    virtual void fun2() {}
};

image.png
派生类继承了基类的虚指针,所以大小为 $8$。

7. 虚单继承

class A {
    virtual void fun () {}
};
class B : virtual public A {
public:
    virtual void fun2() {}
};

8. 普通多继承

class A {
    int a;
    char b;
};
class B {
    char b;
};
class C : public  A, public B {
public:
    char c;
};

image.png

9. 虚函数多继承

class A {
    virtual void fun() {}
};
class B {
    virtual void fun2() {}
};
class C : public  A, public B {
public:
    virtual void fun3() {}
};

image.png
分析:类A一个虚函数表,类B一个虚函数表,类C继承了两个虚函数表,并把自己的虚函数写在了继承顺序中第一个虚函数表中。

10. 虚继承

class A {
    virtual void fun() {}
};
class B {
    virtual void fun2() {}
};
class C : virtual public  A, virtual public B {
public:
    virtual void fun3() {}
};

image.png

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://neo00.top/archives/c类的sizeof大小