一、结构体
一些基础知识在初阶C语言的时候已经介绍过,在这里粗略概括;重点介绍前面没有提到过的。
1.结构体的声明
声明其实就是需要自己创造一个结构体(类型)。后面再拿这个结构体(类型)去创造变量。
(1)简单声明
(2*)特殊的声明
struct { int a; char b; float c; }x;//x为结构体创造出来的名字 struct { int a; char b; float c; }a[20], * p;
1.这种在结构体关键字前省略了名字,这种声明方式的结构体称为:匿名结构体类型。
2.因为省略了名字,后续没法再进行变量的创造,所以只能使用一次
3.两个相同的匿名结构体,属于两种不同的类型
(3*)结构体的自引用
struct Node { int data; struct Node* next; };
1.在结构体内部,可以用自身结构体类型来创建的指针,称为结构体的自引用。
2.一般用来数据结构的链表中,因为需要类型相同。
2.结构体变量的创建和初始化
在前面的时候,我们有介绍过在声明的时候创造的全局变量,接下来都一起介绍了。
第一种创造方式:
#include<stdio.h> struct Stu { char c; int arr[10]; }; int main() { struct Stu A;//结构体变量A struct Stu B;//结构体变量B return 0; }
这里创造的变量A和B都是局部变量。
第二种创造方式:
#include<stdio.h> typedef struct Stu { char c; int arr[10]; }Stu;//对结构体重命名 int main() { //struct Stu A;//结构体变量A //struct Stu B;//结构体变量B Stu C;//结构体变量C return 0; }
第三种方式:上面提到过的创造全局变量
#include<stdio.h> struct Stu { char c; int arr[10]; }D;//全局变量D int main() { //struct Stu A;//结构体变量A //struct Stu B;//结构体变量B //Stu C;//结构体变量C return 0; }
在创建后变量后,就该对变量进行初始化了
初始化:
#include<stdio.h> struct Stu { char name[20]; int age; double height; }; int main() { struct Stu s1 = {"zhangsan",20,182.8};//顺序初始化 struct Stu s2 = {.age=18,.height=188.5};//指定成员初始化 return 0; }
在创建变量的时候就初始化:
struct Stu { char name[20]; int age; double height; }s3 = {"lisi",19,150.6};//创造的全局变量并初始化
3.结构体的内存对齐(*)
这是本节的重点,所谓的内存对齐,就是要知道结构体类型的内存大小专门来的,我们该怎么计算。
(1)引例
我们先观察两个大体相同的结构体,为什么相同的变量不同的顺序,内存大小却不一样。
struct s1 { char c1; char c2; int i; }; struct s2 { char c1; int i; char c2; }; #include<stdio.h> int main() { printf("%zd\n", sizeof(struct s1)); printf("%zd\n",sizeof(struct s2)); return 0; }
运行结果:
第一个结构体类型s1的内存是8字节
第二个结构体类型s2的内存是12字节
造成这种原因是:在结构体中,存在内存对齐这一规则。
(2)结构体内存对齐规则
1)第一条规则:第一个成员在,与结构体变量偏移量为0的地址处
什么是偏移量:
把存储单元的实际地址与其所在段的段地址之间的距离称为段内偏移,也称为“有效地址或偏移量”。
图解:
第一个成员就从0位置开始存放,内存多大就占几个格子(字节)。
2)第二条规则:其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处(偏移量处)
什么是对齐数:
对齐数=编译器默认的一个对齐数与该成员的内存大小的较小值
(vs的默认值为8)
图解:
上述成员在内存中一共占据了8个字节的空间
第二个结构体:
这些成员在内存中所占的字节大小就是结构体的最终内存大小了吗?还没完,还需要根据第三条规则来计算。
3)第三条规则:结构体的总大小为最大对齐数(每个成员变量都有一个对齐数) 的整数倍
如:char的大小为1,1就是对齐数;像int大小为4,4就是对齐数;还有一个编译器默认对齐数,需要变量与其对比得出。
对齐数图解:
现在我们来计算结构体的大小
第一个:
第二个:
该结构体在内存中占9个字节,不是4的整数倍,需要增大(增大到离9最近的数字且是4的整数倍),所以该结构体的内存大小为12字节。
如果结构体嵌套又如何计算,我们看第四条规则
4)第四条规则:如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
存放嵌套的结构体时,它也有自己的最大对齐数和内存大小,所以只需要把它存放到是它自己的最大对齐数的整数倍处即可。
图解:
变量C在右图所占的内存大小为16个字节,该结构体的最大对齐数为4,所以16为最终的内存大小。
struct s1 { char c1; char c2; int i; }; struct s2 { char c1; int i; char c2; }; struct s3 { char c1; struct s2 c2; }; #include<stdio.h> int main() { printf("%zd\n", sizeof(struct s1)); printf("%zd\n", sizeof(struct s2)); printf("%zd\n",sizeof(struct s3)); return 0; }
(3)内存对齐的原因
1)平台原因(移植原因):不是所有的硬件平台都能访问任意地址处上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
2)性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐;原因在于,为了访问未对齐的内存,处理器需要做两次内存访问;而对齐的内存访问仅需要一次访问(拿空间换时间的一种做法)
3)做法:既可以节省空间又能节约时间
在设计结构体的时候,我们既要满足对齐,又要节省空间,就要让占用空间小的成员尽量集中在一起。
例如:
struct s1//已集中 { char c1; char c2; int i; }; struct s2//未集中 { char c1; int i; char c2; };
像struct s1的结构体就做到了上述的要求,内存只占8,而struct s2却占了12
(4)修改默认对齐数
我们可以通过修改默认对齐数,使结构体有更好的对齐方式,这里用#pragma这个预处理指令来修改。
#pragma pack(1)//设置默认对齐数为1 struct s1 { char c1; char c2; int i; }; #pragma pack()//恢复默认对齐数 struct s2 { char c1; char c2; int i; }; #include<stdio.h> int main() { printf("%zd\n", sizeof(struct s1)); printf("%zd\n", sizeof(struct s2)); return 0; }
1.一样的数据类型和排列方式,所占内存却是不一样
2.结构在对齐方式不合适的时候,我们可以自己更改默认对齐数
3.默认对齐数,修改的结果一般要求为2^n,n>=1;不可以为符合或奇数(1除外)。
二、位段
位段是基于结构体的基础上的,位段是一种特殊的结构体--也是为了节省空间
1.位段的定义
位段的声明和结构是类似的
(1)位段的成员必须是:int,unsigned int、char或signed int(c99之后也可以有其他的类型)
(2)位段的成员名后边有一个冒号和数字
(3)举例:
struct A { int _a : 2; int _b : 5; int _c : 10; int _d : 30; };
1. _a、_b这些只是为了更好知道这是位段才加的,也可以选择不加。
2.后面的冒号和数字才是位段的语法要求。
3.位段的“位”表示二进制位的意思,冒号后面的数字就是代表有多少二进制位。
4.数字表明该成员变量最大的二进制位,如_a:2,_a只有两个二进制位,能表示的二进制数字只有:00,01,10和11,范围就是0-3。
(4)位段的作用
我们根据预知的数据内存,可以设置合理的二进制位,就可以达到节约空间的目的
2.位段的内存分配
(1)内存的计算
1.位段也是一种结构体,所以位段也遵守内存对齐的方式。
2.位段是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
如:
下面的内存大小是多少呢?
#include<stdio.h> struct A { int _a : 2; int _b : 5; int _c : 10; int _d : 30; }; int main() { printf("%zd\n",sizeof(struct A)); return 0; }
我们通过代码的运行结果可知:该位段的内存大小为8个字节
我们接上面第二点:如:该位段都是int,一上来会先分配一个字节的空间(一字节==32bit),我们前面的2+5+10刚好存放在第一个字节的空间里面,而_d需要30bit,所以只能再开辟一个字节的空间,32bit拿出30比特刚好存放_d这个数据。
(2)位段的内存分配规则
1)位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
2)位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
3)位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
比如上述第一个字节中的bit没有被用完,后续是否还用这是不确定的
(3)内存分配实例
看一段代码:
#include<stdio.h> struct S { char a : 3; char b : 4; char c : 5; char d : 4; }; int main() { struct S s = {0};//初始化 printf("%zd\n", sizeof(struct S)); return 0; }
先简单看这个位段会消耗多少字节?
该结构体共消耗3字节。他们所占的二进制有3+4+5+4=16位,不应该是只占2字节吗?
图解:
现在我们已经知道了内存的分配,接下来了解数据是怎么存入的。
代码:
#include<stdio.h> struct S { char a : 3; char b : 4; char c : 5; char d : 4; }; int main() { struct S s = {0};//初始化 printf("%zd\n", sizeof(struct S)); //赋值 s.a = 10; s.b = 12; s.c = 3; s.d = 4; return 0; }
内存分配图解:
这就是这些数据存入内存中的二进制形式,然后转化成16进制就是在调试窗口的展示形式,让我们看看是不是这样子呢?
可以清楚看到三个字节中数据的存储方式,因为只能存储有限位的bit,所以需要控制数据的大小范围,否则会造成数据的丢失。
3.位段的跨平台问题
(1)int被当成有符号数还是无符号数是不确定的
如上述代码的结果:
(2)位段中最大位的数目不能确定。(16位机器最大位16,32位机器最大32位,写成27,在16位平台的机器上会出现问题)
(3)位段中的成员在内存中是从左向右分配,还是从右向左分配标准尚未定义(大小端存储字节序)
(4)当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的
跟结构相比,位段可以达到同样的效果,可以更好的节省空间,但是存在跨平台问题。