【C学习】自定义类型篇Ⅰ:详细介绍 - 结构体的内存对齐,结构体、位段


零 关于自定义类型

对于一些基本数据类型,就像 int 可以存放整数数据,例如表示我有一百块钱,可以定义作 int money = 100;

数组是相同类型的集合,例如一个班级的学生姓名,可以定义做 char stuName[] = {“鲁鲁”, “花花”,“福宝”};

如果我们想要储存每个学生的信息,包括学生的姓名、年龄、电话、身高体重…这些不同的数据类型,数组就无法满足我们的要求了,不光是数组,已有的数据类型都没法很好的储存和管理这样的信息。我们不妨自己定义一个,满足我们要求的类型~😏


一、结构体

结构体 属于用户 自定义的数据类型,是一些值的 集合,这些值称作 成员变量,其中每个成员可以是不同类型的变量

已知的内置类型能表达的内容有限 、单一,复杂对象就可以用结构体表示


1.1 结构体的声明

结构体、包括所有自定义类型的声明 不占用栈里的储存空间

struct tag			//结构体类型(tag 为结构体标签)
{
	member - list;	//成员列表
}variable - list;	//变量列表(可有可无)

没有变量列表时,结构体大括号 {} 后面也是需要加分号 ; 的,写作:

struct tag			//结构体标签
{
	member - list;	//成员列表
};

此时一个结构体类型就声明成功,而他的类型就是 struct tag,定义变量的时候和 int 一样,把类型名写在变量名前面即可~

🙋‍🌰举栗子 – 声明一个学生:

// 一个学生信息可以放入一个结构体变量中储存
// 这里定义学生信息包括:姓名、年龄、学号
// 分别用 char 类型数组、整型、char 类型数组 进行储存
struct Student
{
	char name[20];
	int age;
	char id[20];
};	// 分号不能忘记

// 这个结构体的类型名就为:struct Student

特殊的声明:匿名结构体

匿名结构体:省略了结构体标签 tag,只能在声明时定义结构体变量,好处是可以防止别人调用

struct
{
	char name[20];
	int age;
	char id[20];
}Stuff1;	// 结构体变量

struct
{
	char name[20];
	int age;
	char id[20];
}s1, *ps, arr[5];	// 结构体变量指针, 结构体变量数组

int main()
{
	// 正确访问
	Stuff1.age = 33; // 可以直接赋值的

	// 错误访问
	*ps = Stuff1;	// 报错
	Stuff1 = s1;	// 报错
	
	return 0;
}

注意: 即使两个匿名结构体成员变量一样,编译器也认为他们是两个类型,不能进行相互间的赋值。


1.2 结构体的自引用

所谓自引用就是 结构体中包含一个类型为该结构本身的成员。
注意:需要以 结构体指针 的方式写,不然无法计算结构体的大小!

(参考数据结构里的 链表)

// 一个结构体里放 指向下一个结构体的指针 和 该结构体中需要存放的数据
struct ListNode
{
	int data;
	struct ListNode* next;
};

结构体的 typedef:

在定义结构体变量时,我们需要写出 struct ListNode n1 这样的代码,难免会感到结构体类型名很长、很复杂,这时的 typedef 仍旧管用,写法如下:

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

//这里将 struct ListNode --> Node 

值得注意的是:即使 typedef 了,在自引用结构里,还是得用原来的结构类型

int main()
{
	Node n1;	
	return 0;
}
有观点认为:不建议使用 typedef,因为这个写法确实简洁了但是看不出原来的类型是结构体还是枚举,或是别的什么。

1.3 结构体的 定义&初始化

/**********************************  结构体声明  **********************************/
// 【书】:书名 + 作者 + 定价 + 书号
struct Book
{
	char book_name[20];
	char author[20];
	int price;
	char id[20];
}b4 = {"同桌冤家","yhy",36,"TS10001"}, b5;

struct Book b5;
// b3,b4,b5 是都类型为 struct Book 的结构体变量 (并且是全局变量)

// 【学生】:名字 + 年龄 + 学号 + 喜欢的书
struct Stu
{
	char name[20];
	int age;
	char id[20];
	struct Book favorite_book;
};

结构体的定义方式:

  1. 直接定义,不初始化
/**********************************  主函数部分  **********************************/
	struct Book b1;
  1. 定义时 初始化 :将结构体里的成员顺序写在 {} 中,成员间以 , 隔开
    如果每个成员在之前加上 .成员名 = 初始化内容 ,就可以乱序书写了。
	struct Book b2 = {"小大人丁文涛", "yhy", 35, "TS38492"};		// 顺序
	struct Book b3 = {.author = "yhy",.book_name = "丁克舅舅"}; 	// 乱序

在结构体声明中定义的结构体变量,也同样可以如此初始化!(见声明处)

结构体内的结构体类型成员,也同理初始化如下

 	struct Stu s1 = { "Kevin",24,"N170409113",{"小时代","gjm",88,"TS123"}};

1.4 结构体传参

结论:结构体传参时,推荐 传址传参(要传结构体的地址!!!)

struct SS
{
	int data[100];
	int num;
};

void print1(struct SS s)		// 不推荐
{
	printf("%d %d %d %d\n", s.data[0], s.data[1], s.data[2], s.num);
}

void print2(struct SS* ps)		// 推荐
{
	// 下面两种访问均可,看个人喜好
	printf("%d %d %d %d\n", ps->data[0], ps->data[1], ps->data[2], ps->num);
	printf("%d %d %d %d\n", (*ps).data[0], (*ps).data[1], (*ps).data[2], (*ps).num);
}

int main()
{
	struct SS s = { {1,2,3},999 };
	print1(s);
	print2(&s);

	return 0;
}

函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。


二、🚩结构体的内存对齐

研究 内存对齐 讨论的是 结构体大小如何计算 的问题,总体来说,这种规则是一种 拿空间换取时间 的做法。

(注意:这里的所有实验都是基于 Windows 系统下的 VS 环境中讨论的 ~)


2.1 引子

结构体在内存中到底是如何存放的呢?
先看一组结构体,他们成员类型一致,让我们来输出他们的类型大小

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

struct S2	
{
	char c1;
	char c2;
	int i;	
};
	printf("%d\n", sizeof(struct S1));
	printf("%d\n", sizeof(struct S2));

------------
输出结果
12
8

两个结果竟截然不同。

为解决如此诡异的现象,我们不由得想到去分析一下,每个成员分别是如何储存的,这里我们需要引入一个 C 库宏:


2.2 成员偏移 - offsetof

  • offsetof(type, member)

作用:返回一个 结构体某个成员 相对于 结构体开头 的偏移量,单位为 字节
type:一个 struct 类型。
member:type 类型中的某个成员。
头文件:<stddef.h>
返回值类型: size_t(可理解为 int)

使用如下:

struct S1		// 类型大小:12
{
	char c1;	// 0		1 2 3 空间浪费了
	int i;		// 4(4~7)
	char c2;	// 8		9 10 11 空间浪费了
};

struct S2		// 类型大小:8
{
	char c1;	// 0
	char c2;	// 1		2 3 空间浪费了
	int i;		// 4(4~7)
};

int main()
{
	printf("%d ", offsetof(struct S1, c1));	// 结果见结构体定义处的注释
	printf("%d ", offsetof(struct S1, i));
	printf("%d\n", offsetof(struct S1, c2));
	
	printf("%d ", offsetof(struct S2, c1));
	printf("%d ", offsetof(struct S2, c2));
	printf("%d\n", offsetof(struct S2, i));

	return 0;
}
------------
输出结果:
0 4 8
0 1 4

好像更懵了,内存对齐规则🤡究竟是什么规则!!!


2.3 🚩内存对齐规则

  1. 🎯结构体的 第一个成员 直接对齐到相对于结构体变量 起始位置为 0 的偏移处

  2. 🎯从第二个成员开始,要对齐到某个 对齐数整数倍 的偏移处

    对齐数:结构体 成员类型自身大小默认对齐数 的较小值 (VS 环境下默认对齐数为 8;Linux 环境默认不设对齐数,则对齐数就是结构体成员类型自身大小)
    助记:min(sizeof(int),8)

  3. 🎯整个 结构体的大小最大对齐数整数倍

    每个结构体成员都有一个对齐数,其中最大的对齐数就是最大对齐数
    助记:max(每个成员的min)

  4. 如果结构体中嵌套了结构体类型为成员:
    嵌套的结构体 对齐到 自己的最大对齐数整数倍 处(即该结构体类型大小的整数倍处)
    结构体的整体大小仍是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。


2.4 🚩实操一下

(详细分析一个 Struct S1:

struct S1		// 结构体类型大小:max(1,4,1)--> 4 --> 12
{
	char c1;	// 0
	int i;		// min(4,8)--> 4 --> 4-7
	char c2;	// min(1,8)--> 1 --> 8
};
第一步:按照第一条规则,安置第一个成员
👣 第一个成员为 char 类型,放于偏移量 0 处,占 1 个字节,存在 {0}
第二步:按照第二条规则,从第二个开始,依次安置后面所有成员
👣 第二个成员为 int 类型,类型大小 4,默认对齐数 8,对齐数取两者最小 4。char c1 已经放在 {0} 位置,往后找对齐数 4 的整数倍的位置即 {4},int 按大小占 4 个字节,就可以存在 {4,5,6,7} 四个位置上。
👣 最后一个成员为 char 类型,类型大小 1,默认对齐数 8,对齐数取两者最小 1。int i 已经放到了位置 {7},往后找对齐数 1 的整数倍的位置即 {8},char 按大小占 1 个字节,就可以存在 {8} 这个位置上。
最后一步:按照第三条规则,计算结构体类型整体大小
👣这里三个成员的对齐数分别是 1、4、1,取最大 4,又根据前两步得知,各个成员存放于 {0} ~ {8} 共计 9 字节。所以,我们从 9 往后找 4 的倍数,最近的就是 12 啦。12 也自然为结构体 S1 的类型大小 ~

(简化版分析写在代码注释啦!熟练使用后就可以用眼睛看出简单结构体大小了哦 😉
(接下来的结构体,其分析步骤都大致相同,此处不赘述

struct S2		// 结构体类型大小:max(1,1,4)--> 4 --> 8
{
	char c1;	// 0
	char c2;	// min(1,8)--> 1 --> 1
	int i;		// min(4,8)--> 4 --> 4-7
};

struct S3		// 结构体类型大小:max(8,1,4)--> 8 --> 16
{
	double d;	// 0-7
	char c;		// min(1,8)--> 1 --> 8
	int i;		// min(4,8)--> 4 --> 12-15
};

struct S4			// 结构体类型大小:max(1,8,8)--> 8 --> 32
{
	char c1;		// 0
	struct S3 s3;	// max(8,1,4)--> 8 --> 8-23
	double d;		// min(8,8)--> 8 --> 24-31
};

ps:
一道百度笔试题:写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明
考察:offsetof 宏的实现

#define MY_OFFSET_OF(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
// 这样可能更好理解
#define MY_OFFSET_OF(TYPE, MEMBER) ((size_t)&((TYPE*)NULL)->MEMBER)

2.5 内存对齐的意义

至于…为啥会有内存对齐呢?比较官方的解释可以了解一下:

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

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


2.6 省空间的小技巧

在设计结构体时,尽可能把占用空间小的成员放在一起

通过观察上述案例 struct S1 和 struct S2 成员相同只是位置不同,结构体所占的空间也不相同。struct S1 有更多的浪费空间,而我们想让空间被利用,就可以选择把占空间的小的类型放在一块进行声明。


2.7 修改 默认对齐数

#pragma pack(对齐数); 声明进行设置,一般将对齐数设为 2n 大小,使用完成后 #pragma pack(); 可以恢复默认对齐数

(欢迎手动计算验证哦:

#pragma pack(1)	// 设置默认对齐数

struct S1_Re	// 结构体总大小:max(1,1,1)--> 1 --> 6
{
	char c1;	// 0
	int i;		// min(4,1)--> 1 --> 1-4
	char c2;	// min(1,1)--> 1 --> 5
};

#pragma pack()	// 恢复默认对齐数

int main1_4()
{
	printf("%d\n", sizeof(struct S1_Re));	// 6

	return 0;
}

三、位段

跟结构相比,位段可以达到同样的效果,并且可以很好的 节省空间,但是有跨平台的问题存在。
位段中的 指的是 二进制位

位段的声明和结构是类似的,有两个不同:
  1. 位段的成员类型必须是 int、unsigned int、signed int 或 char。
  1. 位段的成员名后边有一个冒号和一个数字。
理解补充:位段没有对齐规则,位段存在本身是为了节省空间,而对齐是牺牲空间换时间。

3.1 声明

(如注释所见,是一些错误的思考和自言自语:

struct A
{					// int 类型 4byte - 32bit(32 个比特位,这些成员按照声明占位量挨个放?)
	int _a : 1;		// 32-1=31
	int _b : 5;		// 31-5=26
	int _c : 10;	// 26-10=16
	int _d : 20;	// 16-20 不够了,再申请 4 byte,16+32-20
};	

// 数字加起来一共是 36 bit
// 1 byte = 8 bit
// 算起来 5个 byte 便可装下 A,可是为什么 A 的大小是 8 咧?

int main()
{
	printf("%d\n", sizeof(struct A));	// 8
	return 0;
}
------------
输出结果:
8

3.2 位段的内存分配

  1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
  2. 位段的空间上是按照需要以 4 个字节(int)或者 1 个字节(char)的方式来开辟的
  3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段
struct S
{
	char a : 3;		// 申请 1 字节
	char b : 4;
	char c : 5;		// 申请 1 字节
	char d : 4;		// 申请 1 字节
};
// 从高地址往低地址放

int main()
{
	printf("%d\n", sizeof(struct S));	// 3
	struct S s = { 0 };
	s.a = 10;
	s.b = 12;
	s.c = 3;
	s.d = 4;
	
	return 0;
}
----------
结果可验证:
&s 内存中放的值为 62 03 04

在这里插入图片描述

位段动图演示

  • 位段结构体类型大小
    在这里插入图片描述

  • 位段储存方式
    在这里插入图片描述

  • 最后呈现结果的计算过程
    在这里插入图片描述


3.2 位段的跨平台问题

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

🔗接下篇 Ⅱ:枚举、联合(共用体),大小端相关知识点和应用

👉🔗【C学习】自定义类型篇Ⅱ:枚举、联合(共用体)、机器大小端,你确定你搞懂了吗?

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值