目录
结构体属于自定义类型,用于描述复杂对象
例如最典型的描述一个学生对象:有名字、学号、性别、年龄、年级等多个属性,使用结构体定义非常方便。这也是封装思想的典型应用。
一、结构体的声明与创建:
结构体是一些值的集合,这些值称为成员变量,每一个成员变量可以是不同的类型
定义格式:
(struct关键字不可省略)
struct 自定义的结构体类型名字{
member-list;
成员变量;
}变量列表(称为结构体变量,可有可无);//只有创建全局结构体变量才使用
结构体变量有三种定义以及创建方式:
1、定义方式一:
而后直接在声明结构体花括号后面直接创建,//这种方式创建的结构体属于全局变量
这个时候就相当于直接创建了两个该类型结构的全局变量stu1和stu2
2、定义方式二
用struct + 结构体类型名 + 结构体变量名
例如:struct student s;
示例:
3、用typedef重定义类型:
此时的Student是一个该结构体的类型,可以理解为跟int、double一样,用于声明变量
(而事实上类型的声明本质上是决定访问地址的多少的问题)
对于结构体的初始化使用{}大括号
创建结构体变量,有两种方式:
一种是正常的顺序定义,即必须和定义结构体类型的顺序相同
还有一种是乱序定义,可以使用.引用符号进行定义
4、匿名结构体:
结构体还有一种特殊的声明方式:即没有结构体类型名,所以称为匿名结构体
对于这种方式来说,没有了类型名,就只能在定义结构体时顺带创建结构体变量,因为没有结构体类型名,那么之后就不能再创建新的该类型的结构体(注意:依然可以在花括号后面定义多个结构体)
其使用场景是:通常用于只使用一次的结构体,即不希望之后再有人定义该类型的结构体
注意:对于这种特殊声明的结构体,如果两个结构体内容一模一样,都没有名字,形式上一模一样的,就有可能会出问题。但是编译器可能会报错也可能不会报错,但是不报错并不意味着没有Bug)
二、结构体自引用
结构体自引用 (用于链表创建节点,在数据结构中非常重要与常见,具体可以参考我的关于数据结构的另外的文章)
链表中一个节点的定义方式:
三、结构体体内存对齐(计算结构体内存的大小)
即结构体的大小并非是类型的大小,例如:
同时,你会发现拥有相同成员的两个结构体它们的内存大小居然不一样!例如:
所以,我们到底要确定一个结构体的内存大小呢?有什么规则吗?
是的,有规则。
这是因为有一个内存对齐的机制,本质上是为了提高访问效率,以为空间换时间。
1、什么叫结构体对齐?
第一个成员变量存储在与结构体初始地址偏移量为o的地方
其他成员要存储在某个数字(对齐数)的整数倍的地址//这个对齐数是该成员自身的对齐数
结构体的内存必须是最大对齐数的整数倍,在这个例子中,结构体内存大小本应该是5,但不是4的整数倍,于是就变成了8.
什么是对齐数?(每一个成员的类型占据的字节大小就是该成员的对齐数,所以每一个成员的对齐数就是数据类型大小,而后再将所有成员的对齐数进行比较,取较大的为对齐数。还要和默认的对齐数进行比较,取较小值)
对齐数:编译器默认的对齐数 与 该成员大小的较小值,而VS的默认对齐数是8
(注意:linux(gcc)编译器没有默认对齐数,其对齐数就是成员自身的大小)
对于嵌套结构体的对齐数:
如果是嵌套结构体呢?就是结构体内部还有其他结构体,此时对齐数式多少呢?
结构体的总大小为最大对齐数(每一个成员都有一个对齐数,从中取出最大的对齐数)的整数倍数
对于内部嵌套了结构体的情况来说,嵌套结构体成员对齐到自己最大对齐数的整数倍处(而不是该结构体整体作为一个成员来计算对齐数),而对于结构体内存的整体大小来说就是所有对齐数的最大对齐数的整数倍(包括嵌套结构体成员内部的对齐数,而不是将该嵌套结构体整体大小视为一个对齐数,即结构体内部最大对齐数)
2、为什么要对齐?
空间换时间
由于机器的环境不同,或者要访问某一个数据时,本质上是访问某个地址,其每一次访问的字节数是固定的。
比如一个结构体的第一个数据是字符,而第二个数据是一个整型类型。假设我们要访问第二整型数据,
如果在结构体存储时不对齐,那么该结构体的地址是5个字节,那么我们需要访问两次,因为第一次只能访问四个字节,
而第二个数据是整型是第二位到第五,还要再访问一次4字节才能将第二个整型数据全部拿到。
而如果在结构体存储初始就进行了对齐,那么我们从该地址的第四位开始访问四个字节即可。只需要一次
因此,相较于不对齐存储,事实上是用空间换取时间,节省了时间,提高了效率
因为有结构体对齐结构的存在,
因此我们在设计结构体时,不是胡乱设计,而是将占用空间较小的成员尽量集中在一起
以此减小内存空间的浪费
我们一起来计算下面两个结构体的内存:
3、自定义对齐数
可以自己修改默认的对齐数:
#pragam pack(设置的默认对齐数大小)//当设置为1时,相当于没有对齐,因为每一个位置都是1的倍数
该句在代码中的书写意义是,从写下该句开始之后改变默认对齐数
如果要取消设置,返还系统默认设置,则再写一次
#pragam pack()
offsetof (type,member);//是一个宏,可以直接使用,作用是计算结构体成员相对于起始位置的偏移量,返回值即偏移量
四、结构体传参
两种传参方式:
1、传值调用:这种方式,形参会再申请一个空间,有一个实参的临时拷贝,不会改变结构体的实际值,费时间费空间,访问的时候用.操作符
2、传址调用:这种方式,直接将结构体的地址传过去,直接进行操作,省时省地
对于传址来说,形参是一个结构体指针类型,访问到时候用->
即结构体指针要访问其成员变量用的是->符号
结构体传参尽量使用传址,
因为结构体传参需要压栈,如果结构体过大,会耗时耗空间,而对于传址来说,只有压一个地址,4/8个字节。
而形参是放在栈区的,对于数据在内存的开辟位置,可以参考我另外的文章,有详解。
五、位段
位段:该概念是基于结构体来说的,作用为了结构体节省空间
位段的类型必须是:int、unsigned int 、signed int 、char
位段成员的后面必须有一个冒号加一个数字
位段:二进制的位
示例: int a:2;//表示a占据两个比特位的空间
位段的应用场景:
对于一个变量来说,有没有可能只会取某些固定的值,对于这些值来说,其大小就只有那么多,
那么就没有必要分配太多空间,因为对于多余的空间来说,根本用不到,纯粹就是浪费
例如:
一个变量a,只有0、1、2、3四种可能的值
那么只需要两个比特位,即00、01、10、11就可以
然而一个int类型占据4个字节,也就是32个比特位,浪费了30个比特位
但是对于位段的使用需谨慎。
因为位段是不跨平台的,可移植的程序应该避免使用位段
同时,位段的存储是不确定的,即不知道是从左边开始存储还是从右边开始不知道
以及,当第一个位段比较小,而第二个位段比较大时,我们不确定剩余空间是否使用
注意:位段的大小不能超过机器的位数,例如32位机器不能安置超过32个比特位