前言:上一篇内容中我们概述了结构体的基本内容,包括结构体的定义,声明,创建和初始化,内存对齐等问题,本篇将进一步深入结构体相关内容,包括为什么会存在内存对齐,结构体如何传参,结构体实现位段等一系列内容。
目录:
一.结构体为什么存在内存对齐?
二.修改默认对齐数
三.结构体传参
四.结构体实现位段
4.1什么是位段?
4.2 位段的内存分配
4.3位段的跨平台问题
4.4位段的应用
4.5位段使用的注意事项
一.结构体为什么存在内存对齐?
1.平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定 类型的数据,否则抛出硬件异常。
2.性能原因:这点也是比较核心的一点原因,数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要 作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地 址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以 ⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两 个8字节内存块中。例如:以x64平台为例,一次访问8个字节,对齐与不对齐,会造成不同的访问次数:
当结构体根据内存对齐规则对齐之后,每次访问出的数据都是完整的,而不对齐的话数据将会被割裂开,会导致同一个数据可能会被分成两次访问 。
一句话,结构体的内存对齐就是一种妥协,是拿空间来换取时间的做法。
那我们在设计结构体的时候,我们如何做到既满足对齐,又节省空间?
让占用空间小的成员尽量集中在一起!(集中不一定代表空间小的成员一定在前面,只要群聚在一起即可)
例如:
#include<stdio.h>
struct S1
{
char c1;
char c2;
int a;
};
struct S2
{
char c1;
int a;
char c2;
};
int main()
{
printf("%zd\n", sizeof(struct S1));//8
printf("%zd\n", sizeof(struct S2));//12
return 0;
}
从中我们发现,S1,S2类型的成员变量完全一样,但是所占空间大小却不一样,说明让占空间小的成员变量聚集在一起可以节省空间。
二.修改默认对齐数
C语言存在一种预处理指令#pragma,可以修改编译器的默认对齐数。
例:
#pragma pack(1)//设置默认对⻬数为1
struct S
{
char c1;//1
int i;//4
char c2;//1
};
#pragma pack()//取消设置的对⻬数,还原为默认
int main()
{
//输出的结果是什么?
printf("%zd\n", sizeof(struct S));
return 0;
}
结构体在对⻬⽅式不合适的时候,我们可以⾃⼰更改默认对⻬数。
三.结构体传参
结构体本质上也是一种数据类型,所以它是可以作为参数传递的,那它是如何作为参数传递的呢?
struct S
{
int data[1000];
int num;
};
struct S s = { {1,2,3,4},1000 };//结构体的全局声明和初始化
//结构体传参
void print1(struct S s)
{
printf("%d\n", s.num);
}
void print2(struct S* ps)
{
printf("%d\n", ps->num);
}
int main()
{
print1(s);//1000
print2(&s);//1000
return 0;
}
上面提供了结构体传参的两种方式:1.传值调用(print1)。2.传址调用(print2)
那么两种方式哪种好呢?答案:首选print2函数——传址调用。
原因:函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。 如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,所以会导致性能的下降。
四.结构体实现位段
4.1什么是位段?
结构体讲完就得讲讲结构体实现位段的能力,位段的声明和结构与结构体是类似的,但有两处不同:
1.位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以 选择其他类型。
2. 位段的成员名后边有⼀个冒号和⼀个数字。
struct W
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
W就是一个位段类型。那么W所占内存大小是多少?
printf("%zd\n", sizeof(struct W);//8
4.2 位段的内存分配
1. 位段的成员可以是 int unsigned int signed int 或者是 char 等类型;
2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的⽅式来开辟的。
3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使⽤位段。
//⼀个例⼦
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 = 3;
s.d = 4;
//空间是如何开辟的?
return 0;
}
具体空间是如何开辟的呢?
位段根据成员变量类型(char)连续开辟了四个一个字节的空间,并将变量转为二进制从低地址的低字节处存入内存,当所余空间足够存入下一个数据则继续使用,如果不行那就存进连续的下一个字节空间,以此类推。全部存进去后,将他们转化为十六进制,以这题为例,在内存中我们观测到了预料中的结果。
所以从中我们不难解释上文提及位段涉及的不确定性因素:
1.申请到的一块内存当中,从左向右使用,还是从右向左使用,是不确定的。
2.剩余的空间,不足下一个成员使用的时候,是浪费呢?还是继续使用呢?
这些都是造成位段不确定性因素的原因。
4.3位段的跨平台问题
如上文提及的:
1. int位段被当成有符号数还是⽆符号数是不确定的
2. 位段中最⼤位的数⽬不能确定,(16位机器最⼤16,32位机器最⼤32,写成27,在16位机器会出问题。
//32位和64位机器上int的长度是4个字节
//在16位机器上int的长度是2个字节
3. 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
4. 当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃 剩余的位还是利⽤,这是不确定的。
总结: 跟结构相⽐,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
4.4位段的应用
下图是⽹络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要⼏个bit位就能描述,这⾥ 使⽤位段,能够实现想要的效果,也节省了空间,这样⽹络传输的数据报⼤⼩也会较⼩⼀些,对⽹络 的畅通是有帮助的。
4.5位段使用的注意事项
位段的⼏个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的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;//间接将值赋给_b
return 0;
}
以上,便是C语言自定义类型之一——结构体的全部介绍了,望屏幕前的你能有所收获。