(自定义类型)结构体以及结构体的存储

结构体的声明

结构体的概念: 结构是⼀些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
结构体的声明:

struct tag//结构体类型名
{
	member-list;//成员列表,多个成员
}variable-list;//变量列表

上述代码块中struct是关键字tag是自定义类型名,member-list是成员变量,可以有多个不同类型的结构体成员,variable-list是定义的结构体变量,也可以不定义结构体变量,用的时候再定义。但是最后的分号不能丢。
上述代码块是声明一个结构体类型,相当于画图纸,指定结构体里面都有什么,然后创建结构体才是真正申请了一块空间去存结构体变量的成员。

例如描述一个学生:

struct Stu
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
};//这相当于声明一个结构体类型。
struct Stu S1, S2;//这是创建了两个结构体变量

特殊的声明

//匿名结构体类型
struct
{
	int a;
	char b;
	float c;
}x;
struct
{
	int a;
	char b;
	float c;
}*p;
int main()
{
	p = &x;
	return 0;
}

上述两个结构体的声明均省略了结构体类型名,这样就相当于直接创建了一个结构体类型。
而第二个相当于创建了结构体指针,指向这样类型的结构体。

但是后面执行的p = &x;是错误的,虽然两个结构体的变量和变量顺序都一样,但是编译器还是认为这是两个结构体,所以p = &x语句是错误的。

结构体的自引用

在结构中包含一个类型为该结构本身的成员是否可以呢?

比如,定义一个链表的节点:

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

此时的定义是错误的,这样结构体会无限展开,变得无限大。

正确的自引用方式:

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

在结构体自引用使用的过程中,夹杂了typedef对匿名结构体类型重命名,也容易引入问题,看看下面的代码:

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

因为Node是对前面的匿名结构体类型的重命名产生的,在我们还没有重命名完成就直接使用,这样是不合理的。

所以解决方案如下:

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

结构体变量的创建和初始化

有了结构体类型,那如何定义变量?

可以在声明的同时直接创建变量,也可以声明以后再创建变量

struct Point
{
	int x;
	int y;
}p1;//声明类型的同时定义变量p1
struct Point p2;//定义结构体变量p2

如何给结构体成员赋值:

初始化:定义变量的同时赋初值。既可以在声明结构体的时候直接赋初值(下面第二个),也可以在创建结构体的时候赋初值(下面第一个)
同时下面第二个演示了结构体的嵌套初始化。结构体中的结构体也可以直接初始化,加大括号即可。

struct Point p3 = {x, y};
struct Node
{
	int data;
	struct Point p;
	struct Node* next;
}n1 = { 10, {4,5}, NULL };

结构体默认是按照顺序初始化的,也可以不按照顺序初始化:
例如:

struct Stu
{
	char name[15];
	int age;
};
struct Stu s = { .age = 20, .name = "zhangsan" };

结构体的对齐规则

struct Node1{
	char c1;
	int a;
	char c2;
};
struct Node2{
	char c1;
	char c2;
	int a;
};
int main()
{
	struct Node1 node1 = {'a', 100, 'b'};
	struct Node2 node2 = { 'a','b', 100} ;
	printf("%zd\n", sizeof(node1));//结果是12
	printf("%zd\n", sizeof(node2));//结果是8
	return 0;
}

(VS2022环境下)上述代码跑的结果第一个是12,第二个是8,不一样的原因就是结构体内存对齐的原因。
对齐规则:

1.结构体的第一个成员对齐到相对结构体变量起始位置偏移量为0的地址处。
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
3.结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍。
4.如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。

对齐数=编译器默认的一个对齐数与该成员变量大小的较小值
Linux中gcc编译器没有默认对齐数,对齐数就是成员自身的大小。

通过上述规则就可以解释上面两个结构体的大小了。
对于struct Node1 node1 = {'a', 100, 'b'};

第一个变量是char类型,默认在第一位,偏移量为0的位置,第二个为int类型,4个字节的大小,而编译器默认是8,所以取4,所以在偏移量为4的位置处。中间有三个空间浪费了。第三个变量大小为1,所以取1,然后就接着往下存,此时整个空间占9个字节,按照第三个规则,总大小为最大对齐数的整数倍,最大对齐数是4,所以应该总大小应该是12。
在这里插入图片描述

对于struct Node1 node2 = { 'a','b', 100} ;

第一个变量是char,存放在第一个内存,偏移量为0的位置,第二个变量也是char,对齐数是1,所以取1,第三个是int,对齐数是4,取4放在偏移量为4的地方,现在占了8个字节的空间。整个结构体最大对齐数是4,所以整个空间应该是4的倍数,现在是8,刚好,所以就占用8个字节的空间。
在这里插入图片描述

结构体嵌套问题

struct S3
{
	double d;
	char c;
	int i;
};
printf("%d\n", sizeof(struct S3));
struct S4
{
	char c1;
	struct S3 s3;
	char d;
};
printf("%d\n", sizeof(struct S4));

分析上面代码,第一个结构体S3,double类型直接存储,存8个字节,然后接着存一个char,此时占用了9个字节的内存,然后是int,要在偏移量是4的倍数的地方存,所以是12开始时存,然后存到15,此时整个空间占用16个字节,为8的整数倍。所以整个就占16个字节。

第二个结构体S4,第一个存char,直接存储,第二个按照第四条规则,S3最大对齐数是8,就在偏移量是8的地方存储,中间七个空间浪费。然后占16个字节,第三个char,占1个字节,对齐数取1,所以存在偏移量为24的位置。此时总共占了25个字节。最后占用内存大小是整体最大对齐数的整数倍,整体最大对齐数包含嵌套类型里面的,所以取8,最后总大小就是32。

为了验证上面的结论,可以使用offsetof这个宏来看结构成员相对于结构开头的字节偏移量
在这里插入图片描述

修改默认对齐数

可以通过#pragma这个预处理指令修改默认对齐数
#pragma pack(1)就是设置默认对齐数为1
#pragma pack()取消设置的默认对齐数,还原为默认

#include <stdio.h>
#pragma pack(1)//设置默认对齐数为1
struct S
{
	char c1;
	int i;
	char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
struct S2
{
	char c1;
	int i;
	char c2;
};

int main()
{
	printf("%d\n", sizeof(struct S));//输出6
	printf("%d\n", sizeof(struct S2));//输出12
}

#pragma pack(1)下面的结构体就是按照1的默认对齐数对齐的,#pragma pack()取消了默认对齐上述,就再次还原为默认值,此时S2就是12个字节的大小

结构体在对齐方式不合适的时候,我们可以自己更改默认对齐数。但是我们尽量改为2的n次方,而不要改为1,3,5,7之类的非2的n次方的数这样乱改只会适得其反。

为什么会存在内存对齐

平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行次内存访问,因为对象可能被分放在两个8字节内存块中。
总的来说就是在拿空间换时间

但是也不是说我们就一定要浪费空间,我们可以通过自己的代码编写就可以在一定程度上既节省空间又提升效率。比如上面的这两个结构体

struct Node1{
	char c1;
	int a;
	char c2;
};
struct Node2{
	char c1;
	char c2;
	int a;
};

结构体成员都一样,但是第二个的空间就比第一个的空间要少。

结构体实现位段

什么是位段

1.位段的成员必须是int、unsigned int 或signed int ,在C99中位段成员的类型也可以选择其他类型。
2.位段的成员名后边有一个冒号和一个数字。

C语言标准并没有规定位域的具体存储方式,不同的编译器有不同的实现,但它们都尽量压缩存储空间。
位段的规则:(仅作相同类型讨论,不同类型穿插,以及位段和非位段穿插不在探究)

当相邻成员的类型相同时,如果它们的位宽之和(前面所有的)小于类型的 sizeof 大小,那么后面的成员紧邻前一个成员存储,直到不能容纳为止;如果它们的位宽之和大于类型的 sizeof 大小,那么后面的成员将从新的存储单元开始,其偏移量为类型大小的整数倍。(不是对齐数)
简而言之是按照类型开辟的,一次开辟一个int类型或者一个char,如果能一起放下就放,不能就重新再开辟一个int类型的大小

看下面代码

struct A {
	int a : 2;
	int b : 7;
	int c : 10;
	int d : 30;
};
int main()
{
	struct A a = { 0 };
	a.a = 1;
	a.b = 2;
	a.c = 3;
	a.d = 4;
	printf("%zd\n", sizeof(struct A));
	return 0;
}

当我们跑起来的时候内存是这样的:
在这里插入图片描述
我们给a开辟了2个比特位,存入内存,首地址存入的是00000001,b占7个比特位,a + b不大于32,7个比特位存2是0000010所以接着在后面存储,将第一个地址占满,第一个地址存入的是000010 01,所以第一个显示09,然后还要一个0,存在第二个地址的第一个比特位。c占10个比特位,2 + 10 + 7 不大于32,所以接着存,从第二个地址的第二个比特位开始。10为存3是0000000011。所以第二个地址存入的是0000011 0所以显示06,第四个是30位,2 + 7 + 30 + 10大于32,所以在其偏移量为类型大小的整数倍再开辟一个int类型的大小。就在第五个地址存。

使用位段注意事项

位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
位段的跨平台问题:

int 位段被当成有符号数还是无符号数是不确定的。
位段中最大位的数目不能确定。16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
在一个字节内部上看,位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。

位段的几成员共有同一个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配一个地址,一个字节内部的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;
}

总结:

跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。如果真的需要节省空间而且可以容忍跨平台的问题,那么就可以使用位段。

位段的应用

网络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要几个bit位就能描述,这里
使用位段,能够实现想要的效果,也节省了空间,这样网络传输的数据报大小也会较小⼀些,对网络的畅通是有帮助的。
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值