C/C++内存对齐以及类的大小计算详解

尽管内存是以字节为单位,但是大部分处理器并不是按字节块来存取内存的。它一般会以双字节,四字节,8字节,16字节甚至32字节为单位来存取内存,我们将上述这些存取单位称为内存存取粒度。

现在考虑4字节存取粒度的处理器取int类型变量(32位系统),该处理器只能从地址为4的倍数的内存开始读取数据。

每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。gcc中默认#pragma pack(4),可以通过预编译命令#pragma pack(n),n = 1,2,4,8,16来改变这一系数。

有效对其值:是给定值#pragma pack(n)和结构体中最长数据类型长度中较小的那个。有效对齐值也叫对齐单位

内存对其规则:

(1)结构体第一个成员的偏移量(offset)为0,以后每个成员相对于结构体首地址的 offset 都是该成员大小与有效对齐值中较小那个整数倍,如有需要编译器会在成员之间加上填充字节。

(2)结构体的总大小为有效对齐值 的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。

参考下面的几种情况来理解。 

//32位系统
#include<stdio.h>
struct
{
    int i;    
    char c1;  
    char c2;  
}x1;

struct{
    char c1;  
    int i;    
    char c2;  
}x2;

struct{
    char c1;  
    char c2; 
    int i;    
}x3;

int main()
{
    printf("%d\n",sizeof(x1));  // 输出8
    printf("%d\n",sizeof(x2));  // 输出12
    printf("%d\n",sizeof(x3));  // 输出8
    return 0;
}

上面三个结构体的内存布局为:

假定对齐系数为4,因为有效对齐值为 对齐系数和最长数据类型值中较小的那个为4,所以结构体中的成员除了第一个偏移量为0,其他成员的偏移量为该成员的整数倍。

对于结构体x1,第一个数据成员为int i ,4个字节大小,char c1 1个字节大小,偏移为1的整数倍即为4,即接着int 后面存放,char c2 1个字节大小,偏移为5接着c1后面放,整体大小为4+1+1=6,根据规则(2),结构体的大小为4的整数倍,即为8;

对于结构体x2,第一个数据成员为char c1 1个字节大小,int i 4个字节大小,偏移为4的整数倍即为4,所以存放在偏移量为4的位置,占4个字节,char c2 1个字节大小,偏移为1的整数倍即为8,紧接着int i 后面存放,整体大小为 4+4+1=9,根据规则(2),结构体的大小为4的整数倍,即为12;

对于结构体x3,第一个数据成员为char c1 1个字节大小,char c2 1个字节大小,偏移为1接着c1后面放,int i 4个字节大小,偏移为4,所以存放在偏移量为4的位置,占4个字节,所以整体大小为4+4=8,根据规则(2),结构体的大小为4的整数倍,即为8。

 由上面的学习,对于内存对齐的计算已经了解,下面说一下如何计算类的大小。

C++类涉及空类、静态成员、普通成员函数、静态成员函数、虚成员函数、多继承、虚继承等。

类作为一种类型定义是没有大小可言的,这里的大小指的是类的对象所占的大小,使用sizeof对一个类型名操作,得到的是具有该类型实体的大小,计算遵循结构体的对齐原则。

类的大小与普通数据成员有关,与成员函数和静态成员无关。虚函数对类的大小有影响,因为虚函数表指针带来的影响,同样虚继承也是同理。(静态数据成员之所以不计算在对象大小内,因为类的静态数据成员被该类所有对象所共享,并不属于哪个对象,定义在内存的全局区);

1.空类的大小

特别的,空类的大小为1,C++标准规定,一个独立对象必须具有非零大小,因为new需要分配不同的内存地址,不能分配内存大小为0的空间;同时避免除以sizeof(T)时得到除以0的错误,因此使用1个字节来区分空类。

  • 空类的继承:当派生类继承空类后,如果派生类有自己的数据成员,空基类的一个字节大小并不会加到派生类中,比如下面这种情况,sizeof(A)结果为4。
    class Empty{};
    struct A:public Empty{ int a;};
    //sizeof(A)为4
  • 一个类包含一个空类对象数据成员,空类的1字节会被计算进去,比如下面这种情况,根据对齐原则,则sizeof(B)结果为8。  

    class Empty{};
    class B{
        int x;
        Empty e;
    };  //sizeof(B)为8

2.含有虚成员函数的类的大小 

虚函数是通过一张虚函数表来实现的。编译器必需要保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)。

每当创建一个包含有虚函数的类或从包含有虚函数的类派生一个类时,编译器就会为这个类创建一个虚函数表(VTABLE)保存该类所有虚函数的地址,其实这个VTABLE的作用就是保存自己类中所有虚函数的地址,可以把VTABLE形象地看成一个函数指针数组,这个数组的每个元素存放的就是虚函数的地址。在每个带有虚函数的类中,编译器秘密地置入一指针,称为vpointer(缩写为VPTR),指向这个对象的VTABLE。 当构造该派生类对象时,其成员VPTR被初始化指向该派生类的VTABLE。所以可以认为VTABLE是该类的所有对象共有的,在定义该类时被初始化;而VPTR则是每个类对象都有独立一份的,且在该类对象被构造时被初始化。

因此含有虚成员函数的类实例化对象中包含了指向虚函数表的指针,指针的大小为4(32位系统),因此计算大小时,需要算上虚表指针的大小且在对象实例中最前面,如下情况。

class Base{
public:
    int a;
    virtual void f(){ cout<<"Base::f"<<endl; }
    virtual void g(){ cout<<"Base::f"<<endl; }
};  //sizeof(Base)为8

3.基类含有虚函数的继承

  1. 在派生类中不对基类的虚函数进行覆盖,同时派生类中还有自己的虚函数,如下派生类。虚函数按照声明顺序放于表中,基类的虚函数在派生类的虚函数的前面,此时基类和派生类的sizeof都是一个指针大小+数据成员大小。即只有一个虚表指针。
    class Son:public Base{ 
    public: 
        virtual void f1(){ cout<<"Son::f1"<<endl; }
        virtual void g1(){ cout<<"Son::g1"<<endl; } 
    };
  2. 在派生类中对基类的虚函数进行覆盖,派生类的大小仍然是基类和派生类的数据成员+一个虚表指针的大小。

  3. 多重继承:无论是否对虚函数进行覆盖,每个基类都需要一个指针来指向其虚函数表,派生类的虚函数存放在第一个基类的虚函数表中。因此派生类的大小为继承的基类个数的指针加上他的所有数据成员大小,比如下面情况。

    class A    
    {    
    };   
     
    class B    
    { 
        char ch;    
        virtual void f0()  {  }  
    };  
     
    class C   
    { 
        char ch1; 
        char ch2; 
        virtual void f()  {  }   
        virtual void f1() {  }  
    }; 
     
    class D: public B, public C 
    {    
        int d;    
        virtual void f0()  {  }  
        virtual void f1()  {  } 
        virtual void f2()  {  }
    }; 
    //sizeof(A)为1
    //sizeof(B)为8
    //sizeof(C)为8
    //sizeof(D)为20

     对于D类,继承与B和C,有两个虚表指针分别指向B和C的虚函数表,首先是指向B虚函数表的指针,然后类B中的数据成员,再然后是指向类C的虚函数表指针,然后类C中的数据成员,最后是类D中的数据成员d。

  4. 虚继承的情况 :虚继承时,不仅要计算指向基类的虚函数表指针,如果自身也有虚函数,则会有单独的虚函数表,即也有一个指向自身类的虚函数表的指针。

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 在C/C++编程语言,内存是一个非常重要的概念。内存是计算机用于存储和访问数据的地方,它可以被看作是一个巨大的存储器数组,每个元素都有一个独特的地址。 在C/C++,我们可以使用指针来访问和操作内存。指针是一个特殊型的变量,它存储了一个内存地址。通过指针,我们可以间接访问和修改内存的数据。 当我们在程序声明一个变量时,系统会为该变量分配一块内存空间,并将其地址存储在变量名。我们可以通过使用变量名来访问和修改该内存空间的值。 另外,我们可以使用动态内存分配函数来在运行时动态地分配内存。这在需要在程序创建变量长度的数组或者临时存储空间时非常有用。动态内存分配函数包括malloc、calloc和realloc。在使用这些函数分配内存后,我们需要记得通过使用free函数来释放这些内存空间。 值得注意的是,C/C++的内存管理是程序员的责任。这意味着我们在使用指针和动态内存分配函数时需要小心,以避免内存泄漏和悬挂指针等问题。我们需要确保我们在使用完内存后及时释放它,以避免浪费内存资源。 总结来说,C/C++的内存是一个重要的概念,我们可以使用指针来访问和操作内存。通过动态内存分配函数,我们可以在程序运行时动态地分配内存。然而,我们也需要负责管理内存,以避免出现内存泄漏和悬挂指针等问题。 ### 回答2: C/C++的内存填空题是指填写一段代码,完成特定的内存操作。以下是一个例子: ```c #include <stdio.h> int main() { int array[5]; // 声明一个包含5个整数的数组 int *p = array; // 声明一个指向数组首元素的指针 // 使用循环将数组的元素赋值为0到4 for (int i = 0; i < 5; i++) { *(p + i) = i; } // 打印数组的元素 for (int i = 0; i < 5; i++) { printf("%d ", array[i]); } return 0; } ``` 在这个例子,我们声明了一个包含5个整数的数组`array`,然后使用指针`p`指向数组的首元素。接下来,通过循环遍历数组,利用指针`p`对数组元素进行赋值操作,赋值的值为数组下标。最后,再通过循环遍历数组,利用数组`array`打印出各个元素的值。这段代码展示了C/C++的指针和数组的使用,以及对内存空间的操作。 ### 回答3: C/C++ 内存填空题一般涉及指针和内存管理的知识。下面给出一个例子以300字来回答: 以下是一道关于C/C++ 内存填空题的解答。 ```c #include <stdio.h> #include <stdlib.h> int main() { int* ptr = (int*)malloc(sizeof(int)); int* arr = (int*)calloc(5, sizeof(int)); *ptr = 10; for (int i = 0; i < 5; i++) { arr[i] = i; } printf("Ptr: %d\n", *ptr); printf("Arr: "); for (int i = 0; i < 5; i++) { printf("%d ", arr[i]); } printf("\n"); free(ptr); free(arr); return 0; } ``` 上述代码包含了两个关于内存的填空处,首先是通过`malloc(sizeof(int))`来分配存储 int 型数据的内存空间,并将其地址赋值给`ptr`指针;另一个是通过`calloc(5, sizeof(int))`来分配存储 5 个 int 型数据的连续内存空间,并将其地址赋值给`arr`指针。 接着通过`*ptr = 10`给指针 `ptr` 所指向的内存位置赋值为 10。并用一个 for 循环给数组 `arr` 赋值为 0 到 4。 最后通过`printf`打印结果。Ptr 输出为 10, Arr 输出为 0 1 2 3 4,表示内存填空处正确。 最后需要调用`free`函数手动释放内存,以避免内存泄漏。 在实际编程,动态内存分配是一个常见的操作,合理地申请内存并及时释放内存对于提高程序的性能和效率十分重要。因此对于这题目要熟悉`malloc`、`calloc`、`realloc`、`free`等函数的使用规则和注意事项,以及指针的正确使用。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值