• 结构体内存对齐详解


    结构体基础知识讲解

    如果我们想要了解结构体内存对齐的知识点,首先我们要对结构体有一定的认识。下面我们先讲解一下结构体的基本知识点。

    结构体的声明

    结构体是一些值的集合,这些值称为成员变量,结构的每个成员可以是不同类型的变量。我们可以使用结构体创建一个学生类型,里面的成员变量有:名字,年龄,性别,学号等。

    int main()
    {
    	struct str {
    		char name[20];//名字
    		int age;//年龄
    		char sex[5];//性别
    		char id[20];//学号
    
    	};
    }
    

    这样我们就创建了一个学生。struct str 是数据类型,我们想要使用这个学生类型,就需要创建变量和初始化,那么结构体如何创建变量和初始化呢?

    //结构体创建变量
    int main()
    {
    	struct str {
    		char name[20];
    		int age;
    		char sex[5];
    		char id[20];
    
    	}str1 = {"xiongda",20, "nan","123456"};
    
    	struct str str2 = {"xionger",21,"nan","234567"};
    
    	printf("%s %d %s %s\n", str1.name, str1.age, str1.sex, str1.id);
    	printf("%s %d %s %s", str2.name, str2.age, str2.sex, str2.id);
    
    }
    

    在这里插入图片描述

    如上图这样我们就可以创建两个学生变量并且初始化,同时我们打印了两个学生的信息。

    结构体特殊声明和自引用

    结构体特殊声明

    除了上面这样最基本的情况,还有些特殊情况例如结构体的不完全声明

    struct
    {
     int a;
     char b;
     float c; 
     } x;
    struct
    {
     int a;
     char b;
     float c; 
     } *p;
    

    上面两个结构在声明的时候省略掉了结构体标签,类似于这样的声明我们称为不完全声明。那么我们可以p = &x这样写嘛?答案是不可以的,编译器会把上面的两个声明当成完全不同的两个类型。

    结构体自引用

    我们在使用结构体时,可不可以在结构体中包含一个类型为该结构体本身的成员呢?

    struct Node
    {
     int data;
     struct Node next;
    };
    

    这样写是否正确呢?如果正确那么sizeof(struct Node)是多少呢?struct Node类型里面包含了一个struct Nodestruct Node里面又包含了struct Node类型,这样不是无限套娃么。所以这样自引用的方式是错误的,正确的自引用的方法是

    struct Node
    {
      int data;
      struct Node* next;
    };
    

    使用指针我们就可以找到下一个,struct Node类型。

    typedef

    我们每次定义结构体变量时,我们都需要写一长串类似struct Node p这样的东西,那么可不可以简化一下呢?typedef就很好的解决了这个问题。

    typedef struct 
    {
       int data;
       struct Node* next;
    }Node;
    

    这样声明的结构体,我们在创建结构体变量的时,可以直接使用Node p这样的语句来创建。这样的方式是否好用呢,见仁见智,有人认为加上struct可以更好的识别出结构体变量,有人认为不加struct更加简单,在使用时根据自己的习惯使用就好。

    结构体内存对齐

    在了解了上面的内容之后,就要进入我们的重点内容了,结构体的内存对齐问题。我们说变量在内存中存储,开辟的内存空间都有大小,char类型开辟1个字节内存空间,int类型开辟了4个字节的内存空间等,那么结构体变量时候有大小呢?大小又是多少呢?让我们来探究一下如何计算结构体的大小吧。
    首先我们来看两个结构体声明

    int main()
    {
    	struct s1 {
    		char a;
    		int b;
    		char c;
    	};
    
    	printf("%d\n", sizeof(struct s1));
    
    	struct s2 {
    		char a;
    		char c;
    		int b;
    	};
    
    	printf("%d", sizeof(struct s2));
    }
    

    如果我们没有了解过结构体内存对齐的化,我们会认为这两个结构体的大小都是6个字节,因为他们的成员变量都是两个char类型一个int类型,但是事实真的是这样的吗?

    在这里插入图片描述
    我们看到这两个结构体的大小并不相同,并且都不为6,这是为什么呢?我们需要引入一个概念叫做偏移量,偏移量是什么呢,把储存单元的实际地址与其所在段的地址之间的距离称为偏移量,在结构体中也就是相较于结构体起始位置的距离。我们可以使用offsetof来求偏移量。

    int main()
    {
    	struct s1 {
    		char a;
    		int b;
    		char c;
    	};
    
    	printf("%d\n", (int)offsetof(struct s1, a));
    	printf("%d\n", (int)offsetof(struct s1, b));
    	printf("%d\n", (int)offsetof(struct s1, c));
    }
    

    在这里插入图片描述
    我们可以看见他们的偏移量分别是多少,因为偏移量是相较于结构体起始位置的距离,所以我们可以猜测一下这个结构体的内存分布。
    在这里插入图片描述
    上图是我们的对于这个结构体类型的内存分布的猜想,到底是不是这样呢,我们需要学习一下结构体内存对齐的规则。

    1. 第一个成员在与结构体变量偏移量为0的地址处。
    2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
      对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。
      VS中默认的值为8
    3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
    4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

    我们根据结构体内存对齐规则,再来还原一下结构体内存的开辟,验证我们的猜想是否正确。
    首先第一条规则,结构体的一个成员放置在偏移量为0的地址处,所以我们将第一个char类型放置在0地址处。
    在这里插入图片描述
    结构体第一个成员之后的其他成员,要放置在某个数字(对齐数)的整数倍的地址处。对齐数 = 编译器默认的一个对齐数与该成员大小的较小值,vs默认对齐数为8。第二个成员变量int类型自身大小为4,默认对齐数为8,取较小值4,所以int这个成员变量应该放在4的倍数的地址处,4是4的倍数,所以int类型放在地址4处,自身大小占4个字节。同理,结构体第三个成员char,自身大小1个字节,默认对齐数8个字节,取较小值1,所以应该放在1的倍数的地址处,8是1的倍数,所以第三个成员变量放在8地址处。
    在这里插入图片描述

    到此为止,我们的成员变量都在内存中开辟了空间,一共占用了9个字节,但是这个结构体的大小为12个字节,原因是我们还有第三条规则,结构体总大小为最大对齐数(每一个成员变量都有一个对齐数)的整数倍
    在这个结构体成员中,最大对齐数为4,所以继续向后开辟了3个字节的空间,将结构体总大小扩大为12,变成4的倍数。

    在这里插入图片描述
    这样验证了我们的猜想是正确的。我们使用结构体内存对齐规则,计算一下另一个结构体s2的大小。

    	struct s2 {
    		char a;
    		char c;
    		int b;
    	};
    

    在这里插入图片描述
    地址总长度为8满足,规则三,所以该结构体的大小为8个字节。如果结构体有嵌套,那么结构体大小又该如何计算呢?

    struct S3
    {
     double d;
     char c;
     int i;
    };
    struct S4
    {
     char c1;
     struct S3 s3;
     double d;
    };
    

    通过计算S3的大小为16,那么如何计算S4的大小呢?我们需要使用规则四:如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
    在这里插入图片描述
    所以该结构体大小为32,让我们在编译器中验证一下。
    在这里插入图片描述
    以上就是结构体内存对齐规则的详细解析,我们为什么要实现结构体内存对齐呢?

    1. 平台原因(移植原因):
      不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特
      定类型的数据,否则抛出硬件异常。
    2. 性能原因:
      数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
      原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访
      问。
      总体来说:
      结构体的内存对齐是拿空间来换取时间的做法,那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到?让占用空间小的成员尽量集中在一起。

    我们还可以自己修改默认对齐数,使用#pragma这个预处理指令。

    int main()
    {
    #pragma pack(1)//设置默认对齐数为1
    	struct S2
    	{
    		char c1;
    		int i;
    		char c2;
    	};
    	printf("%d\n", sizeof(struct S2));
    }
    

    在这里插入图片描述

    结构体传参

    struct S {
    	int arr[100];
    	int num;
    };
    
    struct S s = { {1,2,3,4},100 };
    
    void print1(struct S s)
    {
    	printf("%d\n", s.num);
    }
    
    void printf2(struct S* s)
    {
    	printf("%d", s->num);
    }
    int main()
    {
    	print1(s);
    	printf2(&s);
    
    	return 0;
    }
    

    上面的两个函数都可以实现打印结构体内容的功能,那么哪一个函数比较好呢?print1还是print2?答案是print2。原因是函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。所以我们在使用结构体传参时,最好传递结构体的地址。

  • 相关阅读:
    【hive遇到的坑】—使用 is null / is not null 对string类型字段进行null值过滤无效
    Java—抽象类
    c语言宏相关高级用法
    Java学习 --- this关键字
    蓝桥杯每日一题2023.10.7
    【注入后端HTTP请求】服务器端HTTP重定向、HTTP参数注入
    AD快捷键
    vue css变量实现多主题皮肤切换
    智能科技助力服装业:商品计划管理系统的革命性变革
    Qt实现 图片处理器PictureEdit
  • 原文地址:https://blog.csdn.net/weixin_64182409/article/details/126955061