C语言 结构体 · 位段

一、结构体

1. 结构体类型的声明

结构的基础知识

结构是一些值的集合,这些值称为成员变量。
结构的每个成员可以是不同类型的变量。

结构的声明

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 {
	int a;
	char b;
	float c;
};

struct {
	int a;
	char b;
	float c;
}a[2], * p;

匿名结构体类型,只能使用一次,省略了结构体标签(tag)。

还可能引发问题,编译器会误判。

struct {
	int a;
	char b;
	float c;
}a[2], * p;

int main(){
	p = &s; //err
	return 0;
}

2. 结构的自引用

在结构体中,能否包含一个类型为该结构本身的成员?

答案:可以

//正确的结构自引用
struct Node {
	int data;
	struct Node* next;
};
//typedef - 类型重定义,重新定义一个新类型名称
//一般应用于数据结构中
typedef struct Node{
	int data;
	struct Node* next;
}Node;

3. 结构体变量的定义和初始化

有了结构体类型,那么任何定义变量,其实很简单。

举例一

struct Point {
	int x;
	int y;
}p1;	//声明结构体类型的时候,定义变量p1
int main() {
	struct Point p2; //定义结构体变量p2
	struct Point p3 = { 1,2 }; //定义变量的同时给变量赋值
	return 0;
}

举例二

struct Stu {
	char name[20]; //姓名
	int age; //年龄
};
int main() {
	struct Stu s = { "zs",22 }; //初始化
	return 0;
}

举例三

struct Point {
	int x;
	int y;
}p1;	//声明结构体同时定义变量p1
struct Node {
	int data;
	struct Point p;
	struct Node* next;
}n1 = { 10,{1,2},NULL };	//结构体嵌套初始化

int main() {
	struct Node n2 = { 20,{3,4},NULL };	//结构体嵌套初始化
	return 0;
}

举例四

struct S {
	char c;
	int i;
}s1, s2;

struct B {
	double d;
	struct S s;
	char c;
};

int main() {
	struct B sb = { 3.14,{'w',1000},'ccc' };
	printf("%lf %c %d %c", sb.d, sb.s.c, sb.s.i, sb.c);
	return 0;
}

4. 结构体内存对齐

我们已经掌握了结构体的基本使用。

现在我们深入讨论一下:如何计算结构体的大小
这就涉及到特别热门的知识点:结构体内存对齐

试问,这四种结构体计算内存大小,将输出多少?

struct S1 {
	char c1;
	int i;
	char c2;
};
struct S2 {
	char c1;
	char c2;
	int i;
};
struct S3 {
	double d;
	char c;
	int i;
};
struct S4 {
	char c1;
	struct S3 s3;
	double d;
};

int main() {
	printf("%d\n", sizeof(struct S1));
	printf("%d\n", sizeof(struct S2));
	printf("%d\n", sizeof(struct S3));
	printf("%d\n", sizeof(struct S4));

	return 0;
}

结果

为什么结构体成员一样,仅仅是前后顺序不同就导致内存空间差异?

这我们得先了解结构体的对齐规则

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

对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS编译器默认为 8.
Linux中的默认值为 4.

个人理解

结构体的这个成员变量,在内存地址中从几开始?放几个字节?
从几开始,成员变量大小和编译器对齐数对比,取小值的倍数,内存就从几开始。
例如:
struct S1{
	char c1;
	int i;
}
char c1;c1是1,vs编译器的对齐数是8,取小值1;1的倍数还是1,所以char c1变量在内存中的存储位置从1开始。
int i;i是4,vs编译器对齐数是8,取小值4;4的倍数是4、8,前面有char c1存储的1,i存储下去,整个内存变成5,不对齐了,所以要浪费3位字节;int i此时要在第5字节位开始存储4个字节。
最后总结构体的内存为 1+3+4=8。

为什么存在内存对齐?

大部分参考资料都是这么说的:

  1. 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  2. 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

总的来说
结构体的内存对齐是拿空间来换取时间的做法。

所以在设计结构体的时候,我们要满足对齐,又要节省空间,如何做到?
答案:让占用空间小的成员集中在一起。

struct S1 {		//12
	char c1;
	int i;
	char c2;
};

struct S2 {		//8
	char c1;
	char c2;
	int i;
};

S1和S2类型的成员一样,但是S1占用的空间比S2大。

修改默认对齐数 pragme

之前我们见过 #pragme这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数。

//改变前
struct S1 {	//12
	char c1;
	int i;
	char c2;
};

//改变后
#pragma pack(2) //设置默认对齐数为2
struct S1 {	//8
	char c1;
	int i;
	char c2;
};
#pragma pack() //取消设置默认对齐数
#pragma pack(8) //设置默认对齐数为8
struct S1 {	//1 +3(要对齐)+4 +1 +3(要对齐) = 12
	char c1;
	int i;
	char c2;
};
#pragma pack() //取消设置默认对齐数

#pragma pack(1) //设置默认对齐数为1
struct S2 {	//1 +1 +4 =6
	char c1;
	char c2;
	int i;
};
#pragma pack() //取消设置默认对齐数

int main() {
	printf("%d\n", sizeof(struct S1));	//12
	printf("%d\n", sizeof(struct S2));	//6
	return 0;
}

结果在对齐方式不适合的时候,我们可以自己改变默认对齐数。

计算结构体成员的偏移量 offsetof

我们可以通过这个函数来计算,结构体成员在内存中相对于首地址的偏移量。

#include<stdio.h>
#include<stdlib.h>
struct s2
{
	char c1;
	int i;
	char c2;
};
int main()
{
	printf("%d\n", offsetof(struct s2, c1));	//0
	printf("%d\n", offsetof(struct s2, i));		//4
	printf("%d\n", offsetof(struct s2, c2));	//8
	return 0;
}

5. 结构体传参

上代码:

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);	//传结构体
	print2(&s);	//传结构体地址
	return 0;
}

结构体传参有两种方式

  1. 传递结构体对象(传值),对应就是print1函数的方式。
  2. 传递结构体地址(传址),对应就是print2函数的方式。

思考:print1和print2函数哪个好些?

答案:print2函数传递地址好些
函数传参的时候,参数需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象过大,导致压栈系统开销变大,最终造成性能下降。


二、位段

位段的声明

位段的声明和结构是类似的,有两个不同:

  1. 位段的成员必须是 int、unsigned int、signed int。
  2. 位段的成员名后边有个冒号和一个数字。

例如:

struct A{
	int _a:2;
	int _b:5;
	int _c:10;
	int _d:30;
};

A就是一个位段类型。
那位段A的大小是多少?

printf("%d\n", sizeof( struct A ));	//8

8 是怎么得来的?
答:

  1. 首先位段的类是是int,int类型大小是4个字节。
  2. 开辟4个字节空间,其中_a:2,表示_a占用2个bit位,那么_b占用5个bit位,_c占用10个bit位;这时候已经使用17个bit位,4字节=32个bit,还剩下15个bit位不够_d的30个bit存储。
  3. 所以要再开辟4个字节,给_d占用30个bit。
  4. 最终位段A大小为4+4=8个字节。

位段的内存分配

  1. 位段的成员可以是 int、unsigned int、signed int 或者 char(属于整形家族)类型。
  2. 位段的空间上是按照需要以 4 个字节(int)或者 1 个字节(char)的方式来开辟的。
  3. 位段涉及到很多不确定因素,位段是不能跨平台的,注意可移植的程序要避免使用位段。
  4. 不确定因素体现在:①空间是否被浪费;②空间是从左向右还是从右向左使用。

位段的跨平台问题

  1. int位段被当成有符号数还是无符号数不确定。
  2. 位段中最大位的数目不确定(16位机器最大16,32位机器最大32),写成27bit位在16位机器会出现问题。
  3. 位段中的成员在内存中从右向左分配,还是从左向右分配标准未定义。
  4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳第一个位段剩余的位时;是舍弃剩余的位还是利用,这不确定。

总结
和结构体相比,位段可以达到同样的效果。相比之下的优点是:可以很好的节省空间;缺点是位段有跨平台的问题存在。

位段的应用场景

网络传输协议包:
例如在微信上发送消息,数据需要承载其他验证消息才能发送,这时候就适合使用位段来操作。使用结构体会很复杂并且浪费空间。
在这里插入图片描述

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值