目录
前言
结构体属于自定义类型,那么我们为什么需要自定义这个类型?
现实生活中的对象往往不是一个单一元素,如描述一个人,可以有身高,年龄,性别,体重等等,
对于C语言任意一种自带类型,如int,double等等都是不能描述的,因此便有了诸如结构体这种自定义的类型包括了多种类型的结构
1.结构体的声明与创建
与变量一样,结构体也需要声明,初始化等等操作
struct Stu
{
numebr_list//成员列表(包括各种类型)
};//不能省略最后的分号!!!!本质上这是一个语句,是用struct声明结构体的操作,是需要断开的
//像函数的声明一样最后需要分号
用struct这个关键字,声明一个结构体,此时这种结构体的类型是struct Stu。
上述的是正常情况下的声明,C语言还有一种特殊的声明方式——不完全声明
struct //没有Stu这个名字
{
numebr_list//成员列表(包括各种类型)
}s1;
这种方式有几个问题需要注意:
1.这种声明只能在一开始就创建结构体也就是s1,因为缺少名字,也就不能再后面创建
2.这种声明方式在编译器眼里是独立类型
上述代码用不完全声明的方式创建了结构体s1,结构体指针s2,然后让s2取s1的地址,只要二者的结构体类型一样,就不会有任何问题,可是这里VS2019报了警告说类型不兼容,由此可以看出用这种方式创建的结构体在编译器眼里是独立的类型,哪怕它们内部的成员都一样。
结构体的创建
有两种方式,一种是s1在定义的时候创建,而是单独用结构体的类型创建
#include<stdio.h>
struct Stu
{
int n;
}s1;
int main()
{
struct Stu s2;
return 0;
}
注意s1创建在main函数外面,是全局变量,不初始化默认0,s2是局部变量
2.结构体的自引用
指的是结构体内部的一个特殊成员
例如链表
struct Stu
{
int date;
struct Stu* next;
};
两个成员一个是存放数据,一个存放下一个存放数据的结构体的地址,这个就是结构体的自引用
或许会有人好奇为什么是存放地址,而不是直接存放结构体,也就是
struct Stu
{
int date;
struct Stu next;
};
这种写法会导致这个结构体类型的大小无法计算,可以理解成一直套娃,无穷大
写成指针不仅节约空间,而且操作方便,可以计算大小
3.结构体的初始化
#include<stdio.h>
struct Stu
{
int date;
char name[10];
}s1={20,"zhang"};
int main()
{
struct Stu s2={10,"wang"};
return 0;
}
可以声明的时候直接初始化,也可以单独初始化
特殊情况:嵌套初始化,结构体内部有结构体
如
struct next
{
int n;
char name[10];
};
struct Stu
{
int date;
struct next date;
}s1={20,{10,"wang"}};
一个结构体内部成员需要用 { }括起来。
不完全初始化
#include<stdio.h>
struct Stu
{
int date;
char name[10];
};
int main()
{
struct Stu b={.name="wang"};
return 0;
}
初始化是用 . 加成员名并赋初值,其他的编译器会自动根据类型赋0,字符就是'\0',指针是NULL,其他int,double等等都是0
4.内存对齐
首先:结构体的大小如何计算,这可不是单纯的直接结算a4字节+b,c两字节等于6字节,它们在内存上的存放是有一定偏移的,也就是我们所说的内存对齐
struct Stu
{
int a;
char b;
char c;
};
下面是内存对齐的原则
什么是偏移量?
我们可以用一个offsetof函数来观察结构体中成员的偏移情况(也就是想较于起始地址的偏移字节数)
根据例子解读
如果是涉及到嵌套了结构体的结构体计算呢?
也是一样的计算方式,计算出结构体占用的内存,以及最大的对齐数,然后在新的结构体计算中与编译器默认的对齐数相比较,取小值
这种内存会带来什么影响?
struct Stu
{
char c1;
char c2;
int i;
};
上面的排列算出来是8,下面的算出来是12
struct Stu
{
char c1;
int i;
char c2;
};
有没有发现明明成员一样但是s1消耗的内存空间大一点,这就涉及到我们结构体应该怎么写更好的问题了
5.为什么会存在内存对齐
1、平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
解读:有些硬件不能随意访问内存,只能访问特定类型的数据,比如int型在4的倍数处获取,因此为了便于移植统一放在对齐后的位置
2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,对于访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
设定 char c; int i;
解读:我们计算机假如是32位机器,也就是一次传输4字节内容(32位),如果不对齐,单是传递i需要传递两次,第一次访问c与c后面的三个字节,也就是i的前三个字节,第二次访问i的第一个字节及后面未知的三个字节,然后组合
对齐后可以直接一次传输到 i 的前面,第二次传输 i 的4字节,也就是i的传递只用了一次
内存对齐本质就是利用空间换取效率的方式,占用空间固然大了,但是效率也高了
6.如何设计结构体
通过上面对内存对齐的阐述,其实浪费的空间就是一个小的类型分散了,造成了大类型不断开创新的空白内存而不放东西,改进措施就是把小的类型集中到一起(不是单纯的把大类型放前面或后面),多填补大类型造成的空白内存,而不是开创新的空白内存
7.修改默认对齐数
利用宏修改
#pragrma pack()
这里把Stu结构体内部的对齐数修改成4,最后记得把它返回原本的默认值,没有返回那这个程序的默认对齐数就一直是4
一般两者是配套使用的,用来修改你想要修改的部分
8.结构体和位段
在存在内存对齐后,结构体就会占用很大的内存空间来提高效率,一个int型的1,(00 00 00 01)就占了4字节,这时候完全可以认为控制他只占一个字节就足以存储数据了,位段这个概念便是如此,位指的是二进制位。
struct Stu
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
意思是结构体Stu中的a变量只占用两位(而不是4字节),b 5位,c 10位。
解读一下代码是如何开辟空间的
首先a变量是int,以int的形式开辟,也就是32位(4字节),然后a取2,取5,c取10,此时还剩下15位,但是15位不够d用,再开辟一次,多出32位,注意此时d是取前面剩下的15位以及新开的15位还是直接取新开的30位这是不确定的,不同编译器是不同的
注意事项:
1.位段成员只能是int,signd int ,unsignd int ,char(整型家族),来开辟1字节或者是4字节
2.位段的最大位数是4字节也就是32位16位机器的int是2字节,也就是16位
3.书写形式是变量名+冒号+数字,来控制所占用的位数
位段的出现可以大大降低结构体所占用内存的大小
位段虽好,但是有几个问题需要注意
移植性差,
因为位段内部成员被当成无符号型还是有符号型是取决于编译器的,
最大位数是不能确定的,
位段的成员在内存中分配是未知的,(不知道是高地址到低还是低地址到高)
位段成员是否填充空出内存区域是未知的,这一切都取决于编译器,编译器不同可能方式不同
(在上述红色字体有提到)
所以需要移植或者通用的代码不要用位段写