结构体的内存对齐 && 位段

目录

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

结构体的声明

特殊的结构体声明 

结构体的初始化

结构体的自引用 

结构体指针

结构体内存对齐

为什么要有内存对齐?

修改默认对齐数

结构体传参

结构体的位段

什么是位段?

 位段的内存分配 

位段的跨平台问题

注意事项


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

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

当我们描述一个人的年龄时我们可以使用,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;
}

~over~

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值