进阶结构体目录
☝️ 前言 ☝️
前面我们系统得学习了一下结构体,讲解了结构体的定义和使用,下面我们会着重下面的重点来讲解自定义类型:
💢 结构体 💢
❓ 结构体类型的声明
❓ 结构的自引用
❓ 结构体变量的定义和初始化
❓ 结构体内存对齐
❓ 结构体传参
❓ 结构体实现位段(位段的填充&可移植性)
其中
❓结构体类型的声明
❓结构的自引用
❓结构体变量的定义和初始化
❓ 结构体传参
这几个知识点在初阶结构体里面讲过了,这里将不再进行讲解。
如果对这几个知识点有什么不了解的地方可以在这里查阅。
⚡️ 初阶结构体讲解
💥 枚举 💥
❓ 枚举类型的定义
❓ 枚举类型的优点
❓ 枚举的使用
✨ 联合(共用体) ✨
❓ 联合类型的定义
❓ 联合的特点
❓ 联合的大小计算
💢 结构体 💢
❓❗️ 结构体的内存对齐
📚 结构体的内存对齐规则
在使用结构体类型时,只要是类型就难免会涉及到该类型的大小问题,就像是使用整型和浮点型的时候,就会使用到sizeof来计算类型的大小,结构体也是一样可以使用sizeof来计算。可以先来猜测一下:
求test1的内存大小
struct Test
{
char c1;
int i;
char c2;
} test1;
printf("%d\n", sizeof(test1));
我们可以来猜测一下,如果是按照常规的思路来说的话,test1 的内存大小就是1 + 4 + 1 = 6
。但当我们运行代码的时候我们会发现其实并不是6。
为什么不是6而是12呢?这是因为在结构体在存储的时候存在内存对齐。那我们要怎么来计算结构体的大小呢?首先我们需要掌握结构体的对齐规则:
>1. 第一个成员在与结构体变量偏移量为0的地址处。
>2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
> 对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
> - VS中默认的值为8
> 3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
> 4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍
> 处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的
> 整数倍。
根据结构的对齐规则,test1在内存中是这样存放。占据着偏移量0~11的空间,也就是 12 个字节。
📚 试题讲解
📚为什么存在内存对齐
既然内存对齐这么浪费空间,那为什么还要使用内存对齐来存储结构体呢?这是因为下面这几个原因:
总的来说:
所以我们在涉及结构体的时候需要尽量将占用空间较少的成员放在一起,我们可以来测试一下:
struct S1
{
char c1;
int i;
char c2;
} s1;
struct S2
{
char c1;
char c2;
int i;
} s2;
printf("%d\n", sizeof(s1));
printf("%d\n", sizeof(s2));
虽然这两个结构体的成员都是一样的,但是其所占空间的大小是不一样的。
📚修改默认对齐数
每个编译器都有一个默认对齐数,当结构在对齐方式不合适的时候,我们可以更改默认对齐数。这时我们需要使用到#praga
这个预处理指令:
#pragma pack(8)//设置默认对齐数为8
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
❓❗️ 结构体实现位段(位段的填充&可移植性)
📚什么是位段
位段的声明和结构是类似的,有两个不同:
就比如:
struct A {
int _a:2;
int _b:5;
int _c:10;
int _d:30;
} a;
printf("%d\n", sizeof(struct A)):
就像上面的代码所示,a就是一个位段类型的变量。这时候就回到了上面的问题,只要是类型就涉及到内存大小的问题。我们可以运行代码尝试一下:
这时跟结构体的内存对齐完全对不上了啊,明明都是结构体,为什么加了一个冒号差距就这么大呢?这就涉及到了位段的内存分配。
📚位段的内存分配
在了解位段的内存分配之前,我们需要先了解一下使用位段的注意事项:
位段中的位代表的就是比特位,后面的数字表示用几个比特位切割而成的一个变量。我们可以来看下面一个例子:
struct S {
char a:3;
char b:4;
char c:5;
char d:4;
};
struct S s = {0};
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
大家都知道char类型占据着一个字节,一个字节表示8个比特位。所以s在内存中的内存布局是这样的:
当数据进行存储的时候先将数据转换为二进制,然后截取对应的位数放入位段中,如果二进制位多于位段数则截取低位的位段数,如果二进制位多于位段数则在高位补0进行填充:
如果对这个结果不相信的话,可以对代码进行调试,在内存窗口中查看该位段的内存布局:
📚位段的跨平台问题
总结:
💥 枚举 💥
❓❗️ 枚举的定义
在生活中有许多例子可以完全列举出来,比如:性别、星期几、颜色等。这时候我们就可以使用枚举类型来将这些列举出来,定义形式如下:
enum Day//星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
enum Color//颜色
{
RED,
GREEN,
BLUE
};
上面的类型都是枚举类型,{ } 里面的都是枚举类型的可能取值,这些可能取值都是有默认值的,值从0开始依次递增 1 ,也可以在定义的时候赋初值。比如:
enum Color//颜色
{
RED=1,
GREEN=2,
BLUE=4
};
但是需要注意的是,只能在定义的时候给他们赋初值,不能在后面更改他们的值。
❓❗️ 枚举的优点
我们可以使用 #define 定义常量,为什么非要使用枚举?我们需要了解一下枚举的优点:
所以定义常量的时候,可以适当的考虑使用枚举类型,来提高代码的质量。
❓❗️ 枚举的使用
枚举类型的使用只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
enum Color//颜色
{
RED=1,
GREEN=2,
BLUE=4
};
enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
✨ 联合(共用体) ✨
❓❗️ 联合的定义
联合也是一种特殊的自定义类型,这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。联合的定义和结构体的定义一样,但是关键字不同,联合的定义使用的是union关键字
比如:
//联合类型的声明
union Un
{
char c;
int i;
};
//联合变量的定义
union Un un;
//计算这个变量的大小
printf("%d\n", sizeof(un));
运行程序后我们可以发现这个un的空间只有4字节。
为啥会这样呢,明明这个类型的空间最小都是4+1个字节。这就涉及到联合的特点了。
❓❗️ 联合的特点
我们可以测试一下:
union Un
{
int i;
char c;
};
union Un un;
// 下面输出的结果是一样的吗?
printf("%p\n", &(un.i));
printf("%p\n", &(un.c));
程序运行起来的时候我们可以看到这两个地址是一样的:
所以联合里的变量都是使用同一块空间的,所以改变了 c 就是改变了 i ,改变了 i 就是改变了 c 。
un.i = 0x11223344;
un.c = 0x55;
printf("%x\n", un.i);
当程序运行起来时,我们可以发现 i 的值被改变了:
这时候我们就可以运用联合的特性来验证当前计算机的大小端字节序。
❓❗️ 联合大小的计算
联合大小的计算有一下特点:
我们可以来尝试一下计算联合大小的计算:
union Un1
{
char c[5];
int i;
};
union Un2
{
short c[7];
int i;
};
printf("%d\n", sizeof(union Un1));
printf("%d\n", sizeof(union Un2));
当程序运行起来的的时候可以看到:
😎 总结 😎
想要学号自定义类型一定要将结构的内存对齐规则掌握好,要合理利用各个类型的特点,将代码的质量提高: