C语言自定义类型变量——结构体(二)

前言:上一篇内容中我们概述了结构体的基本内容,包括结构体的定义,声明,创建和初始化,内存对齐等问题,本篇将进一步深入结构体相关内容,包括为什么会存在内存对齐,结构体如何传参,结构体实现位段等一系列内容。

目录:

一.结构体为什么存在内存对齐?

二.修改默认对齐数

三.结构体传参

四.结构体实现位段

  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语言自定义类型之一——结构体的全部介绍了,望屏幕前的你能有所收获。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值