文章目录
结构体
结构体的基本知识
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
结构的声明
struct tag
{
member-list;
}variable-list;//注意分号别丢了
第一行中 struct不能删也不能改,tag是结构体标签是根据自己实际需求来定义
大括号里面的member-list是成员列表,里面可以有一个或者多个不同类型的成员变量
最后一样是variable-list是变量列表 特别注意最后的分号
结构的自引用
结构体的自引用在数据结构里面会被不断使用 如单链表
struct SListNode
{
int data;
struct SListNode* next;
};
int main()
{
struct SListNode newnode;//newnode是struct SListNode类型的变量
return 0;
}
上面中结构体成员变量struct SListNode* next就是结构体的自引用
有个印象就可以 学数据结构时会广泛使用
结构体类型的重命名
在上面这个结构体声明时struct SListNode作为结构体类型会显得非常的长不太方便之后的使用这时候便可以用typedef 来重命名
typedef struct SListNode
{
int data;
struct SListNode* next;
}Node;
int main()
{
Node newnode;//把struct SListNode
return 0;
}
Node便是struct SListNode的重命名
当然typedef不仅只可以重命名结构体
typedef int SLTDateType;//把int重命名为SLTDateType
typedef struct SListNode
{
SLTDateType data;
struct SListNode* next;
}Node;
int main()
{
Node newnode;//把struct SListNode
return 0;
}
这样写的好处可以是方便日后好改结构体成员的变量的类型更好的维护 类似于 #define MAX 100;
之后要用到有关最大值100的地方我都可以改成MAX而当最大值相要改变时只需要把#define 中100改成其他
类推当要把存储的值改为double只需要把typedef int SLTDateType中int改为double
结构体变量的定义和初识化
定义结构体变量有两种方法
1、 创建类型的同时创建变量,变量是全局变量
struct student
{
char name[10];
char sex[10];
char id[20];
int age;
}stu1,stu2;
//stu1,stu2就是struct student类型的结构体变量
//同时是全局变量
2 、在函数主体内定义,变量则是局部变量
struct student
{
char name[10];
char sex[10];
char id[20];
int age;
}stu1,stu2;
//stu1,stu2就是struct student类型的结构体变量
//同时是全局变量
int main()
{
struct student stu3;
struct student stu4;
//stu3,stu4就是struct student类型的结构体变量
//同时是 局部变量
return 0;
}
初始化结构体变量也有两种方法
1、 创建类型的同时 创建变量 并定义变量的初始化如stu1
struct student
{
char name[10];
char sex[10];
char id[20];
int age;
}stu1 = {"张三","男","522022",20};//要注意 字符类型要加引号
2、和创建类型时分开 如stu3
struct student
{
char name[10];
char sex[10];
char id[20];
int age;
}stu1 = {"张三","男","522022",20};
int main()
{
struct student stu3= { "张红","女","522012",21};
return 0;
}
那么结构体中嵌套一个结构体该怎么初始化?
struct parent
{
char name[10];
char telenumb[15];
};
struct student
{
char name[10];
char sex[10];
char id[20];
int age;
struct parent mon;
};stu1 = { "张三","男","522022",20, {"张丰","987654321"} };
int main()
{
struct student stu3 = { "张红","女","522012",21,{"张大红","123456789"} };
return 0;
}
上面讲的都是按照 结构体中成员变量的顺序进行初识化
那么不按照顺序进行初始化该怎么做?
struct student
{
char name[10];
char sex[10];
char id[20];
int age;
};
int main()
{
struct student stu3 = { "张红","女","522012",21};
struct student stu4 = { .sex = "女",.age = 21,.name = "张红",.id = "522012" };
printf("%s %s %s %d\n", stu3.name, stu3.sex, stu3.id, stu3.age);
printf("%s %s %s %d\n", stu4.name, stu4.sex, stu4.id, stu4.age);
return 0;
}
可以看到打印的结果是一样的。
结构体内存对齐
结构体内存对齐是一个热门的考点
struct s1
{
int i;
char c1;
char c2;
};
struct s2
{
char c1;
int i;
char c2;
};
int main()
{
printf("s1的大小:%d \n", sizeof(struct s1));
printf("s2的大小:%d \n", sizeof(struct s2));
return 0;
}
思考:
为什么两个结构体大小不是 int占4个两个char共占2个最后的总大小是6呢?
为什么s1和s2在成员变量个数和类型都一样的情况下为什么大小却不同呢?
通过offsetof宏我们可以探究s1和s2在内存中的布局(offsetof宏是用来计算结构体成员相对于起始位置的偏移量)
#include <stddef.h>
struct s2
{
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n", offsetof(struct s2, c1));
printf("%d\n", offsetof(struct s2, i));
printf("%d\n", offsetof(struct s2, c2));
return 0;
}
这是结构体s2的结构体成员的偏移量
解释:char类型的c1的在内存中偏移量是0; int类型的 i的偏移量是4;char类型的c2的偏移量是8
结合图像进一步理解内存中的布局
那么s1的成员的偏移量布局又是怎么样?
#include <stddef.h>
struct s1
{
int i;
char c1;
char c2;
};
int main()
{
printf("%d\n", offsetof(struct s1, i));
printf("%d\n", offsetof(struct s1, c1));
printf("%d\n", offsetof(struct s1, c2));
return 0;
}
同样结合图像
思考 :为什么布局相差这么大? 为什么会有内存浪费不用?
看到这里你肯定已经明白结构体成员不是单单的按照顺序存放进去而是按照规则进行存放
那么规则是什么呢?
结构体内存对齐规则:
- 第一个成员在与结构体变量偏移量为0的地址处。
- 其他成员变量要对齐到某个对齐数的整数倍的偏移处。对齐数 = 编译器默认的一个齐数 与 该结构体成员自身大小的较小值。VS中默认对齐数的值为8
- 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
第一条规则 第一个成员在与结构体变量偏移量为0的地址处。
什么意思?
回顾一下s2的代码和各各成员之间的偏移量
#include <stddef.h>
struct s2
{
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n", offsetof(struct s2, c1));
printf("%d\n", offsetof(struct s2, i));
printf("%d\n", offsetof(struct s2, c2));
return 0;
}
举s2为例子应用第一条规则c1存放在偏移处为0的内存中 结合图像
第二条规则 : 其他成员变量要对齐到某个对齐数的整数倍的偏移处。对齐数 = 编译器默认的一个齐数 与
该结构体成员自身大小的较小值。VS中默认对齐数的值为8
是什么意思?
对齐数 = 编译器默认的一个齐数 与 该结构体成员自身大小的较小值。VS中默认对齐数的值为8
例如 int类型的结构体成员的自身大小是4 而VS中默认对齐数的值为8那么取其中的较小值那么最后的对齐数就是4
chart类型的结构体成员的自身大小是1 VS为8,那对齐数就是1;以此类推double对齐数就是4。
理解规则二的这一句 (其他成员变量要对齐到某个对齐数的整数倍的偏移处)
结合下方图像 int类型的成员的自身大小是4 ,VS的默认对齐数是8取其中的较小值 最终的对齐数就是4。 0偏移处已经占有c1, 4的对齐数的整数倍的偏移处有 4,8,12,16…所以 i 从偏移数为4为起点开始存储
那么char类型的c2怎么存储?
c2的自身大小是1 VS默认为8 取较小值 对齐数便是1。而1又是所有数的整数倍
因此c2便直接存储在 i的后面
第三条规则 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
c1的对齐数是1 i的对齐数是4 c2的对齐数1,所以他们中的最大对齐数就是4 ,那么规则三就很好理解4 8 12 16…分别是最大对齐数的整数倍,而c1,i,c2已经使用了9个字节但是结构体的总大小要是4的整数倍所以结构体总大小自然是12后面再浪费了3个字节
思考:s1的内存布局时怎么一步一步形成的?为什么是8,结合规则一二三
第四条规则:
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
struct s2
{
double c1;
int i;
char c2;
};
struct s1
{
int i;
char c1;
struct s2 s2;
};
int main()
{
printf("%d ", sizeof(struct s1));
return 0;
}
解释: 嵌套的结构体对齐到自己的最大对齐数的整数倍处
我们把s2嵌套再s1中,s2是嵌套的结构体,分析s2中各各成员的对齐数再求出s2的最大对齐数
double类型的c1 自身大小是8字节,VS默认为8字节再取最小值,c1的对齐数就是8
以此类推 i 的对齐数就是4 ,c2的对齐数就是1
所以说嵌套结构体的最大对齐数就是8 对齐到8的整数倍的偏移处有 8 16 24 32…
结合图像
解释:结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
从上面可以看到最后又浪费了三个字节,这就是和之前的原理一样,不过也要算上嵌套进结构体的对齐数,所以结构体的最大对齐数依旧是8 而8的整数倍有 8 16 24 32… 把c2存入内存中结构体的整体大小为21所以得再浪费3个字节凑到24
截至四条规则都已经讲完,但为什么要这样按照规则存放呢为什么要存在结构体内存对齐呢对齐的意义又是什么呢?
大部分的参考资料都是如是说的:
1 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
.2 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说
结构体的内存对齐是拿空间换取时间的做法
最后再看一遍之前的代码
struct s1
{
int i;
char c1;
char c2;
};
struct s2
{
char c1;
int i;
char c2;
};
int main()
{
printf("s1的大小:%d \n", sizeof(struct s1));
printf("s2的大小:%d \n", sizeof(struct s2));
return 0;
}
两个结构体存储的成员都是一样的但是最后的大小却不同
所以当我们构建结构体时要去运用结构体内存对齐的规则 来定义出存储同样成员但更加节省空间的结构体
让占用空间小的成员尽量集中在一起这是关键
修改默认对齐数
#pragma pack(2)//设置默认对齐数为2
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数为1
struct S2
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
//输出的结果是什么?
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
}
其实最后的结果很好分析只要你理解之前的结构体内存对齐的规则,无非就是把VS的默认对齐数8字节改成了另外的字节
位段
什么是位段
位段的声明和结构是类似的,有两个不同:
1.位段的成员必须是 int、unsigned int 或signed int 或char。
2.位段的成员名后边有一个冒号和一个数字。
比如
struct s1
{
int a;
int b;
int c;
};//结构体
struct s2
{
int a : 2;
int b : 15;
int c : 20;
};//位段
int main()
{
printf("%d\n", sizeof(struct s1));
printf("%d\n", sizeof(struct s2));
return 0;
}
s2就是位段注意和结构体区分
那么位段s2的大小是多少呢是否和结构体大小不同?
调试可以看到位段s2的大小是8字节。为什么会这样?
先开始从位段名字开始说起 位段的位实际上是的意思所指的是比特位而一个比特位便是一个二进制位(一个字节等于八个二进制位)
int a : 2;意思则是a变量只申请了两个比特位;int b : 15;则是b变量申请了十五个比特位;int c :20;则是c变量申请了20个比特位。
可是 2+15+20=37 只需要5个字节就足够了,为什么s2的大小却是8个字节?
位段的内存分配
- 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
- 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
重点理解第二条中 以4个字节( int )或者1个字节( char )的方式来开辟的。且一般不会再一个位段中既开辟int又开辟char,所以要么就是成员都是int类型一次开辟4个字节,要么成员都是char类型一次开辟1个字节
回到s2代码中
struct s2
{
int a : 2;
int b : 15;
int c : 20;
};//位段
位段里都是int类型的成员 一次开辟4个字节也就是32个比特位
成员变量 a 和 b加起来17存进还剩下15个比特位,而成员变量c需要20个比特位,内存不够所以得再向系统申请字节 而一次开辟也就是申请4个字节 所以结构体s2的总大小为8个字节
位段的不确定问题
- int 位段被当成有符号数还是无符号数是不确定的。
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机 器会出问题。
- 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是 舍弃剩余的位还是利用,这是不确定的。
因为c语言对于位段没有明确的规定所以位段会存在跨平台的问题.
如在拿结构体s2举例子
struct s2
{
int a : 2;
int b : 15;
int c : 20;
};//位段
c语言没有定义在一个字节中成员是从左向右还是从右向左开始存储,不同的平台是不确定的
第二点 当开辟了4个字节 而成员a和成员b使用的剩下的空间不够接下来的成员存储时也是成员c,c比较大,到底时是 舍弃剩余的位还是利用,也是不确定的。
总结:
跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
枚举
枚举的定义
以上定义的 enum Day , enum Sex , enum Color 都是枚举类型。
{}中的内容是枚举类型的可能取值,也叫 枚举常量 。
这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。
枚举的使用
上面便是运用枚举和switch语句进行的配合,因为枚举中枚举常量默认是从0开始所以exit的值便是0以此类推modify的值就是6
枚举的优点
上面的代码中如果不使用枚举类型自然也可以直接在case语句后面加数字,但是相比下这样便不方便代码的阅读
枚举类型的五大优点
- 增加代码的可读性和可维护性
- 和#define定义的标识符比较枚举有类型检查,更加严谨。
- 防止了命名污染(封装)
- 便于调试
- 使用方便,一次可以定义多个常量
联合(共用体)
联合的使用情形是和结构体使用时相反的,结构体中的成员变量都是存在内存中的甚至为了减少读取数据的时间存储数据采取内存对齐的规则达到空间换时间的效果,而联合其中的成员都是存储在同一块内存中所以说它的使用情形则是成员中只会使用一个,如18岁前我是未成年而一旦过了18岁我便是成年,未成年和成年两者只能取其一,这种情形就适合使用联合,而未成年和成年便是联合中的两个成员。
联合类型的定义
//联合类型的声明
union Un
{
char a;
int b;
};
int main()
{
//联合变量的定义
union Un un;
//计算联合变量的大小
printf("%d\n", sizeof(un));
return 0;
}
联合其中的成员都是存储在同一块内存中
下面两个代码输出的例子也能够体现出存储在同一块空间的特性
例子一
例子二
例子二的内存图像
联合体的大小计算
因为联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。
联合体大小计算的两个规则
1 联合的大小至少是最大成员的大小。
2 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
例子
union Un1
{
char c[5];
int i;
};
union Un2
{
short c[7];
int i;
};
int main()
{
//下面输出的结果是什么?
printf("%d\n", sizeof(union Un1));
printf("%d\n", sizeof(union Un2));
return 0;
}
如果看过我上面结构体的内存对齐的规则那么这个就规则二就非常好理解
Un1内存图像
Un1中的最大对齐数为4字节而蓝色中使用的内存只有5字节而5不是4的整数倍所以只能往后再浪费三个字节,详细可以看结构体内存对齐中的知识