结构体的内存对齐与位段

简介: 当我们描述一个人的年龄时我们可以使用,int age = 18;但是如果我们要描述一个人呢?很显然我们无法仅靠一个age就实现对一个人的描述,所以就有了结构体,在结构体中我们可以包含多种类型的数据,这样就可以实现对一个人的描述比如身高、爱好、体重等等

什么是结构体?为什么要使用结构体?

结构体是由一系列具有相同或不同类型的数据构成的数据集合,也叫结构,它是一种数据类型

当我们描述一个人的年龄时我们可以使用,int age = 18;但是如果我们要描述一个人呢?很显然我们无法仅靠一个age就实现对一个人的描述,所以就有了结构体,在结构体中我们可以包含多种类型的数据,这样就可以实现对一个人的描述比如身高、爱好、体重等等

结构体的声明

struct 结构体名 
         {
        member-list(成员列表)
          }variable-list(变量列表);

用结构体描述一个学生:

struct Stu
{
char name[20]          //字符串数组存储名字
int age                //年龄
float score            //成绩
}s4,s5;               //s4和s5是结构体类型的全局变量
int main()
{
struct Stu s1,s2,s3;  //s1、s2和s3是结构体类型的局部变量
return 0;
}

该结构体类型就相当于一个建筑图纸,主函数中的三个局部变量就相当于三个房子,而这三个房子中又会分别包含三个成员:名字、年龄、成绩。



特殊的结构体声明

在声明结构的时候,可以不完全的声明,我们称之为:匿名结构体类型

struct
{
        char a;
        int c;
        float d;
}s= {0};

关于它的内容不过多陈述,只需要记住它只能在程序中出现一次,且一般情况下不建议使用

结构体的初始化

1、初始化列表:使用花括号 {} 来指定每个成员的初始值

#include <stdio.h>
struct Person {
    char name[20];
    int age;
};
int main()
{
    struct Person p = {"John", 25};
    return 0;
}

2、逐个赋值:使用”.“操作符逐个为每个成员赋值,赋值顺序随意

#include <stdio.h>
struct Stu
{
  char name[20];
    int age;
    float score;
}s3 = { "wangwu",24,98.0f };
  int main()
  {
    struct Stu s1 = { "zhangsan",20,98.5f };
    struct Stu s2 = { "lisi",18,68.5f };
    struct Stu s4 = { .age = 22,.name = "cuihua",.score = 55.6f };
    printf("name:%s\n", s1.name); //打印时的情况是一样的
    printf("name::%s\n", s4.name);
  }


结构体的自引用

一个结构体可以包含一个该结构体类型的成员吗?

#include <stdio.h>
struct Node
{
int data;      //存一个4字节大小的数据
struct Node next;
};
int main()
{
struct Node n;
return 0;
}


系统提示"Node::next使用正在定义的Node",这是因为成员next其实是一个struct Node类型的结构体它的内部还会包含自己的成员next,这个next还将包括自己的成员next,这样就像一个永无止境的递归公式,那我们该如何实现结构体的自引用呢?


结构体指针

#include <stdio.h>
struct Node
{
  int data;            //存一个4字节大小的数据
  struct Node* next;   //next是一个struct Node*类型的指针变量四个字节大小
};
int main()
{
  unsigned int s = sizeof(struct Node);
  printf("%d", s);
  return 0;
}

此时,next是一个struct Node*类型的指针变量,大小固定,因此就可以求出此时结构体的大小:


tips:

       结构体内部可以包含一个指向相同类型的结构体的指针,这样就可以创建具有自引用性质的数据结构,例如链表、树等。

结构体内存对齐

在我们求解一个结构体类型的变量大小时会遇到这样的情况:

#include <stdio.h> 
struct S1
{
  char c1;
  char c2;
  int a;
};
struct S2
{
  char c1;
  int a;
  char c2;
};
int main()
{
  struct S1 s1 = { 'a','b','c' };
  struct S2 s2 = { 'a',100,'b' };
  printf("%zd\n", sizeof(s1));
  printf("%zd\n", sizeof(s2));
  return 0;
}

我们会发现明明结构体S1和S2的成员是两个char一个int,为什么输出的结果却是8和12呢?s1和s2到底是如何分配内存的?这就涉及到了结构体内存对齐规则:

1、结构体的第一个成员对齐到相对结构体变量起始位置偏移量为0的地址处

偏移量:把存储单元实际地址与其所在段的段地址之间的距离称为段内偏移,也称为"有效地址或偏移量"。通俗来讲就是元素相对于首元素地址的位置,偏移量为0就相当于位于首元素地址处,偏移量为10,就相当于位于离首元素地址距离为10个字节的地方。一个偏移量就相当于一个地址,为了方便理解下面的内容我们引入偏移量这个概念  

2、其他成员变量要对齐到对齐数的整数倍的偏移量处

3、结构体总大小为最大对齐数的整数倍

4、嵌套结构体的整体大小就是最大对齐数(含嵌套结构体成员的对齐数)的整数倍

对齐数 = 编译器默认对齐数与该成员变量大小的较小值

  • VS中默认值为8字节
  • Linux  gcc没有默认对齐数,对齐数就是成员自身的大小


关于结构体s1的内存大小分析:

#include <stdio.h>
struct S1
{
  char c1;
  char c2;
  int a;
};
int main()
{
  struct S1 s1 = { 'a','b','c' };
  struct S2 s2 = { 'a',100,'b' };
  printf("%zd\n", sizeof(s1));
  printf("%zd\n", sizeof(s2));
  return 0;
}

①第一个成员变量c1为char类型占一个字节,VS默认对齐数为8字节,8字节>1字节,此时对齐数为1,c1应位于偏移量为0的位置处进行存放


②第二个成员变量c2为char类型占一个字节,VS默认对齐数为8字节,8字节>1字节,此时对齐数为1,c2应位于偏移量为1的倍数的位置处进行存放


③第三个成员变量a为int类型占四个字节,VS默认对齐数为8字节,8字节>4字节,此时对齐数为4,a应位于偏移量为4的倍数的位置处进行存放,此时偏移量为2和3的位置不是4的倍数跳过,然后从偏移量为4的位置开始向后数四个字节用于存放a

(即使跳过了这块内存空间依然存在只是不存放东西,我们称之为闲置)

关于结构体s2的内存大小分析:

①三个成员变量的存储不再过多阐述,但我们发现储存结束后的内存空间大小也仅仅是九字节与实际的12字节不符,这里就要用到结构体内存对齐规则的第三条:结构体总大小为最大对齐数的整数倍,此时s2中的最大对齐数为4,结构体大小为9字节不是4的倍数,所以最终的结构体大小为12字节,偏移量为9、10、11的地址就被闲置了。

关于结构体s4嵌套结构体s3的内存大小分析:

#include <stdio.h>
struct S3
{
  double d;
  char c;
  int i;
};
struct S4
{
  char c1;
  struct S3 s3;
  double d;
};
int main()
{
  printf("%d\n", sizeof(struct S4));
  return 0;
}

①先进入S4,c1为char类型位于结构体起始位置偏移量为0的地址处

②然后进入S3,这里不做过多解释......

③从S3出来后我们排d,此时最总的偏移量位置到了三十一,结构体的总字节大小为32了,根据第四条规则:嵌套结构体的整体大小就是所有最大对齐数(含嵌套结构体成员的对齐数)的整数倍,而S3中的最大对齐数为8,S4中的最大对齐数也为8,32为8的倍数所以最终嵌套结构体的字节大小为32字节,故最后大小为32

为什么要有内存对齐?

作用:为了更加合理的规划结构体占用的内存空间

做法:设计结构体的时候尽量让占用空间小的成员集中在一起

修改默认对齐数

此外,我们还可以通过#pragma预处理器命令更改默认对齐数:

#include <stdio.h>
#pragma pack(1)  //设置默认对齐数为1,此时每个成员变量在内存中都是一个接着一个存储的没有浪费空间,但是这样的运行缓慢了
struct S
{
  char c1;
  int i;
  char c2;
};
#pragma pack()  //取消设置的对齐数,还原为0
int main()
{
  printf("%d\n", sizeof(struct S));
  return 0;
}


修改对齐数的时候最好为偶数位

结构体传参

       结构体传参实质上是传递结构体的地址,在传递的过程中我们可以传递结构体类型的变量,也可以传递结构体的地址:

#include <stdio.h>
//定义结构体
struct S
{
 int data[1000];
 int num;
};
//传递结构体变量
void print1(struct S s)
{
 printf("%d\n", s.num);
}
//传递结构体地址
void print2(struct S* ps)
{
 printf("%d\n", ps->num);
}
int main()
{
    struct S s = {{1,2,3,4}, 1000};
    print1(s); //传递结构体类型变量------传值调用
    print2(&s); //传递结构体地址------传址调用
    return 0;
}

①在传递结构体类型变量的过程是传值调用,形参是实参的一个拷贝,在程序运行过程中开辟一块儿空间存放拷贝内容,如果结构体很大就会导致时间和空间的开销增大

②在传递结构体地址的过程是传址调用,形参是结构体的地址,并不会占用新的内存空间,时间和空间的开销相比于①就会好很多。

结论:结构体传参最好传递结构体的地址

结构体的位段

什么是位段?

概念:位段是一种用于定义结构体成员的特殊语法,它允许我们在占用较少内存空间的情况下,对成员进行位级别的精细控制。位段可以指定成员变量使用的位数,并且可以定义多个相邻成员共享同一个存储单元

作用:为某种类型的数据分配合理的比特位从而减少内存空间的消耗

位段的初始化:

struct B
{
  int _a : 2;           //_a成员只分配两个比特位
  int _b : 5;           //_b成员只分配五个比特位
  int _c : 10;         //_c成员只分配十个比特位
  int _d : 30;         //_d成员只分配三十个比特位
};          

位段的内存分配

#include <stdio.h>
//结构体
struct A
{
  char a;
  char b;
  char c;
  char d;
};
//基于结构体实现的位段
struct B
{
  char a : 3;
  char b : 4;
  char c : 5;
  char d : 4;
};            
int main()
{
  printf("%d\n", sizeof(struct A));
  printf("%d\n", sizeof(struct B));
  return 0;
}



在内存中存储的大概思路是这样的,但是仍然会有一些问题出现:

①开辟空间后,内存中的每个比特位是从左向右使用还是从右向左使用?

②当前面的比特位使用后,剩余空间不足下一个成员使用时,剩余空间是否使用?

答案是:不确定

因为位段本身涉及了很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使⽤位段。

下面我们就来看一看在VS2022上位段是如何存储的:

#include <stdio.h>
struct S
{
  char a : 3;
  char b : 4;
  char c : 5;
  char d : 4;
};            
int main()
{
  struct S s = { 0 };
  s.a = 10;
  s.b = 12;
  s.c = 13;
  s.d = 4;
  printf("%d\n", sizeof(struct S));
  return 0;
}

①char a被分配3个比特位,10的二进制为1010,只能放3个二进制数010;


②char b被分配4个比特位,12的二进制为1100,可以完全放下(紧接着010放)


③char c被分配5个比特位。13的二进制为1101,由于上一字节只剩1个空余比特位所以开辟另一个字节存放c,可以完全放下多余的位置补零;


④char d被分配4个比特位,4的二进制为0100,由于上一级字节只剩3个空余比特所以开辟另一个字节存放d,可以完全放下多余位置补零;


⑤最后按照十六进制在内存中的表示为62 0d 04

最后通过内存窗口验证分析的正确性:


综上所述,我们可以得到在VS2022环境下的位段内存分配规则:

开辟空间后,内存中的每个比特位是从右向左使用

当前面的比特位使用后,剩余空间不足下一个成员使用时,剩余空间不再使用用零补齐然后开辟新的字节空间

位段的跨平台问题

  1.  int 位段被当成有符号数还是⽆符号数是不确定的。
  2.  位段中最⼤位的数⽬不能确定。(16位机器最⼤16,32位机器最⼤32,写成27,在16位机器会出问题。(plc是十六位机器,32位机器) 
  3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
  4. 当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃剩余的位还是利⽤,这是不确定的。

总结: 跟结构体相⽐,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在,如果不追求极致的空间节省还是建议使用结构体

注意事项

位段的⼏个成员可以共享同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,这些位置处是没有地址的。因为 内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的 。 所以不能对位段的成员使⽤&操作符,也不能使用scanf直接给位段的成员输⼊值,只能是将值存储在变量中然后赋值给位段的成员:

struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
struct A sa = {0};
scanf("%d", &sa._b);//这是错误的
//正确的⽰范
int b = 0;
scanf("%d", &b);
sa._b = b;
return 0;
}


相关文章
|
编译器 C语言 C++
C/C++内存对齐规则(结构体、联合体、类)
C/C++内存对齐规则(结构体、联合体、类)
|
安全 C++
【自定义类型:结构体,枚举,联合】内存对齐的原理和原因
【自定义类型:结构体,枚举,联合】内存对齐的原理和原因
79 0
|
编译器 Linux C语言
【C语言】自定义类型:结构体(内存对齐),枚举,联合
【C语言】自定义类型:结构体(内存对齐),枚举,联合
|
6月前
|
存储 编译器 Linux
匿名结构体类型、结构体的自引用、结构体的内存对齐以及结构体传参
匿名结构体类型、结构体的自引用、结构体的内存对齐以及结构体传参
|
9天前
|
存储 Java 程序员
结构体和类的内存管理方式在不同编程语言中的表现有何异同?
不同编程语言中结构体和类的内存管理方式既有相似之处,又有各自的特点。了解这些异同点有助于开发者在不同的编程语言中更有效地使用结构体和类来进行编程,合理地管理内存,提高程序的性能和可靠性。
16 3
|
11天前
|
存储 缓存 Java
结构体和类在内存管理方面的差异对程序性能有何影响?
【10月更文挑战第30天】结构体和类在内存管理方面的差异对程序性能有着重要的影响。在实际编程中,需要根据具体的应用场景和性能要求,合理地选择使用结构体或类,以优化程序的性能和内存使用效率。
|
11天前
|
存储 缓存 算法
结构体和类在内存管理方面有哪些具体差异?
【10月更文挑战第30天】结构体和类在内存管理方面的差异决定了它们在不同的应用场景下各有优劣。在实际编程中,需要根据具体的需求和性能要求来合理选择使用结构体还是类。
|
3月前
|
存储 Go
Go 内存分配:结构体中的优化技巧
Go 内存分配:结构体中的优化技巧
|
5月前
|
编译器 测试技术 C语言
【C语言】:自定义类型:结构体的使用及其内存对齐
【C语言】:自定义类型:结构体的使用及其内存对齐
68 7
|
5月前
|
存储 编译器 C语言
C语言学习记录——结构体(声明、初始化、自引用、内存对齐、结构体设计、修改默认对齐数、结构体传参)一
C语言学习记录——结构体(声明、初始化、自引用、内存对齐、结构体设计、修改默认对齐数、结构体传参)一
57 2