个人主页:@Weraphael
✍作者简介:目前是C语言学习者
✈️专栏:C语言航路
希望大家多多支持,咱一起进步!
如果文章对你有帮助的话
欢迎 评论 点赞 收藏 加关注
在《C语言初阶》中(初阶结构体传送门),我们浅浅学习了结构体的声明、结构体成员的访问、结构体嵌套和结构体传参。今天这篇博客将带领大家深入学习有关结构体的知识。闲言少叙,开快车
- 结构是一些值的集合,这些值可以称为成员变量,结构的每个成员可以是不同类型的变量。
- 类比数组。数组也是一些值的集合,但类型是相同的。
//结构体的声明
struct tag
{
member_list; //成员变量
}variable_list;//variable_list - 变量列表
//注意:后面的分号不可缺
举个例子,假设要描述一名学生
可以描述一个学生的名字、性别、学号、成绩、年龄等等。
【第一种声明】
//描述一位学生
struct Stu //Stu - 标签
{
char name[10]; //名字
int age; //年龄
char sex[5]; //性别
}s1; //s1 - 结构体变量(全局变量)
//分号不可缺
int main()
{
struct Stu s2;//结构体变量(局部变量)
}
【第二种声明typedef】
//描述一位学生
typedef struct Stu //Stu - 标签
{
char name[10]; //名字
int age; //年龄
char sex[5]; //性别
}Stu; //Stu - 将结构体类型struct Stu重新命名为Stu
//分号不可缺
int main()
{
Stu s1; //创建结构体变量(全局)
}
在声明结构的时候,也可以不完全的声明。
【匿名结构体类型(缺少标签)】
注意:
首先先想一个问题:在结构中包含一个类型为该结构本身的成员是否可以?
【举个例子】
假设要存储1、2、3。可以用链表来存储。一个节点中存储一个数,并且当前的节点能够找到下一个节点。所以我们可以把节点定义成一个结构体
【错误代码】
struct Node
{
int data;
struct Node next;
};
但其实上面的代码是错误的,假如说要计算结构体的大小,data是4个字节,next包含date和下一个节点,下个next又包含date和下一个节点…,这样下来会发现它的大小其实是very very大的。
【正确代码】
我们可以放一个指向下一个节点的指针,也就是结构体指针
struct Node
{
int data; //4个字节
struct Node* next; //4/8个字节
};
那么接下来我把代码改成这样是否也是正确的?
【错误代码】
typedef struct Node
{
int data;
Node* next;
}Node;
这其实是错误的!因为代码编译是从上到下的,当走到
Node* next
时,struct Node
还未被typedef
重命名
【正确代码】
typedef struct Node
{
int data;
struct Node* next;
}Node;
详情见《初阶结构体》-> 传送门
在了解结构体内存对齐之前,我们先想想如何计算结构体的大小
为什么
Stu1
和Stu2
的成员变量一样,就仅仅交换了位置,结构体大小却不一样,这是为什么呢?这就要涉及到结构体内存对齐
结构体内存对齐的规则
- 第一个成员在与结构体变量偏移量为0的地址处
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
- 对齐数 = 编译器默认的一个对齐数与该成员类型大小的较小值。(VS中默认的值为8,Linux环境下无对齐数)
- 结构体总大小为结构体成员最大对齐数的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
现在解释开头的代码
以下解析默认是在visual studio环境下
【Stu1】
【Stu2】
如果大家不相信第一个成员在与结构体变量偏移量为0的地址处,我们可以来验证。
C语言有一个宏叫offsetof
,它的功能是计算一个结构体成员相较起始位置的偏移量(头文件:#include
)
在《结构体内存对齐》中提到了默认对齐数,那么默认对齐数能否修改呢?答案是当然可以!
可以用#pragma
这个预处理指令
那么接下来问题来了,怎么恢复呢?
位段的声明和结构其实是类似的,但有两个不同:
- 位段的成员必须是
int
、unsigned int
或signed int
,但也可以是char
类型,因为char
是属于整型家族的- 位段的成员名后边有一个冒号和一个数字
- 位段里的成员一般都是同类型=的
- 位段的位其实表示二进制位
【举个例子】
#include
struct segment
{
int a : 2; //表示a只占内存的2个二进制位
int b : 5;//表示b只占内存的5个二进制位
int c : 10;//表示c只占内存的10个二进制位
int d : 30;//表示d只占内存的30个二进制位
};
segment
就是一个位段类型。
结构体的大小计算和位段的计算是一样的吗?答案其实是不一样的
位段的计算过程其实是这样的
int
是4个字节,有32个比特位,a
占内存2个二进制位,还剩下30个比特位b
占内存5个二进制位,还剩下25个比特位c
占内存10个二进制位,还剩下15个比特位d
占内存30个比特位,上一步还剩下15个比特位,假设把这15个比特位舍弃,继续向内存申请32个比特位给d
用,所以还剩余2个比特位
从上过程中,类型在创建的时候向内存一共申请了32+32
个比特位,也就8
字节。也能看出位段其实比结构体更加节省空间,有多少用多少。
- 位段的成员可以是
int
、unsigned int
、signed int
或者是char
类型- 位段的空间上是按照需要以4和字节(
int
)或者1个字节(char
)的方式来开辟的- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段
【举个例子】
我们一起看看下面这一串代码在内存中是如何分配的
#include
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;
}
首先先定制一些规则:
- 假设位段中的成员在内存中是从低位向高位分配
- 当第二个位段成员占内存二进制位比较大时,无法容纳前一个剩余位段时,直接把前一个剩余的舍弃
- 在visual studio测试
分配过程如下:结构体初始化为000000000
a
被赋值成10,转化为二进制:00001010,而a
只占内存3个二进制位,也就是010(最高位的1被截断),分配:00000010- b被赋值成12,转化为二进制:00001100,b占内存4个二进制位,恰好能容纳上一个剩余位段。分配01100010
- c被赋值成3,转化为二进制:00000011,c只占内存5个二进制位,也就是:00011,然后这次的位段成员无法容纳上一个剩余位段,我们就舍弃,继续向内存申请:00000000,分配:00000011
- d被赋值成4,转化为二进制:00000100,d只占内存4个二进制位,然而上一个剩余位段还是无法容纳这次的成员位段,继续像内存申请:00000000,分配:00000100
- 所以现在内存里为:0110010 00000011 00000100
转化为十六进制也就是 62 03 04,我们F10调试,在内存中看看
这结果恰好就是我们分析出来的结果!
int
位段被当成有符号数还是无符号数是不确定的- 位段中最大位的数目不能确定。16位机器最大16,32位机器最大32,如果
int
位段成员占内存27个二进制位,那么在16位机器会出现问题- 位段中的成员咋子内存中从左向右分配,还是从右向左分配标准还未确定。
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余位时,是舍弃剩余的位还是利用,这也是未确定的。
总结:
跟结构体相比,位段可以达到同样的效果,但是位段可以很好的节省空间,但是有跨平台的问题存在
枚举顾名思义就是一一列举
比如在我们生活中:
- 星期一到星期天是有限的7个,可以一一列举
- 性别,男和女,也可以一一列举
- 一年有12个月,也可以一一列举
枚举和结构体其实是非常类似的,它的关键字:
enum
【举个例子】
//枚举星期一道星期天
enum Week
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
//分号不能丢
//成员以逗号结尾
int main()
{
enum Week w = Mon;//初始化
return 0;
}
- 增加代码的可读性和可维护性
- 和#define定义的标识符比较,枚举有类型检查,更加严谨
- 防止了命名污染(封装)
- 便于调试
- 使用方便,一次可以定义多个常量
枚举的大小是4个字节
联合也是一种特殊的自定义类型,其关键字是:
union
,这种类型定义的变量也包含一系列的成员,特征是这些成员共用同一块空间,所以联合也叫共用体
//定义一个联合类型
union UN
{
char c;
int a;
};
int main()
{
union UN x; //初始化
return 0;
}
- 联合的大小至少是最大成员的大小。
- 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍
- 最小对齐数 = 编译器默认的一个对齐数与该成员类型大小的较小值。(VS中默认的值为8,Linux环境下无对齐数)
【举个例子】
char c[5]
自身大小是5,对齐数(成员类型大小):1,默认对齐数是(vs环境):8,其最小对齐数为1int a
自身大小是4,对齐数(成员类型大小):4,默认对齐数是(vs环境):8,最小对齐数为4- 通过比较它们两个的最小对齐数,发现4为最大对齐数,而最大成员大小是5,5不是4的整数倍,因此要浪费空间到,对齐到8,所以它们大小是8
联合的成员是共用同一块内存空间,这样一个联合变量的大小,至少是最大成员的大小。
【证明】