目录
结构体
结构体是什么?
结构是一些值的集合 , 这些值称为成员变量。结构的每个成员可以是不同类型的变量。
数组也是一些值的集合,但这些值的类型是相同的。
结构体的定义
struct 结构体名
{
成员表列;
}变量表列;
定义结构体的例子:
struct Stu
{
int age;
char name[20];
}s1,s2;
定义了一种结构体叫 Stu ,它有两个成员:age 和 name ,类型分别是 int 和 char [20],随后定义了两个结构体变量 s1 和 s2。
如果结构体定义在 main 函数之前,那么 s1 和 s2 就是全局变量。结构体也可以在 main 函数内部定义。
定义结构体变量可以在定义结构体时就定义,如 s1 和 s2,也可以在 mian 函数内定义:
struct Stu
{
int age;
char name[20];
}s1,s2;
int main()
{
struct Stu s3, s4;
return 0;
}
typedef
如果每次定义结构体变量的时候不想写 struct ,可以在定义结构体变量时写上一个 typedef,如:
typedef struct student
{...}Stu;
下次定义结构体变量时,就只需要写 Stu s1;而不需要写 struct student s1;
typedef 也可以在定义结构体之后再使用:
struct student
{...};
typedef struct student stu;
分析以下代码:
typedef struct Node
{
int data;
Node* next;
}Node;
代码的原意是将结构体 struct Node 命名为 Node ,在定义结构体时就直接用 Node 来定义 next 指针,但这种做法是不合法的。
匿名结构体
匿名结构体就是在定义结构体时,不指定结构体的名字:
struct
{
int age;
char name[20];
}s1,s2;
这种结构体只有在定义结构体后就定义结构体变量,而不能在其他地方定义结构体变量。
分析以下代码:
struct
{
int age;
char name[20];
}s1;
struct
{
int age;
char name[20];
}*p;
int main()
{
p = &s1;
return 0;
}
s1 和 p 的结构是一样的,但 p = &s1 ;是不合法的。
结构体的自引用
在结构中包含一个类型为该结构本身的成员是否可以呢?
struct Node
{
int data;
struct Node next;
};
这是不行的,因为这种结构体类型的大小是无穷大的,但是我们可以把 struct Node next 改为
struct Node * next;这样这种结构体类型的大小就是 8 或 12 了,并且这种结构体就叫作链表。
链表
struct Node
{
int data;
struct Node *next;
};
结构体变量的初始化
示例:
struct Stu
{
int age;
char name[20];
}s1 = { 21,"zhangsan" };
struct Stu s2 = { 12,"wangwu" };
int main()
{
struct Stu s3 = { 18,"lisi" };
return 0;
}
即按照定义结构体时成员变量的顺序,将要初始化的内容用大括号括起来,并且用逗号隔开。
如果结构体内定义有结构体,则初始化这种结构体时对结构体内的成员结构体初始化要加大括号:
struct A
{
int a;
char arr1[20];
};
struct B
{
int b;
struct A a1;
char arr2[20];
};
struct B b1 = { 12,{30,"hehe"},"haha" };
可以不严格地按照定义结构体时成员的顺序来初始化,用成员运算符即可:
struct B b1 = { .a1.a = 30,.a1.arr1 = "hehe",.b = 12,.arr2 = "haha" };
访问结构体成员的方式
使用成员运算符 ‘ . ' 访问:
用上面的结构体变量 b1 为例,用 printf 输出 b1 的内容:
printf("%d %d %s %s\n", b1.b, b1.a1.a, b1.a1.arr1, b1.arr2);
即:结构体变量名 . 结构体成员名,如果要访问结构体内的结构体变量,则需要使用多个成员运算符。
结构体的内存对齐
先来看一个例子:
struct A
{
char b;
int a;
char c;
};
struct B
{
char b;
char c;
int a;
};
int main()
{
printf("%d\n",sizeof(struct A));
printf("%d\n",sizeof(struct B));
return 0;
}
最后打印的结果是:12 和 8。
产生了两个疑问:
1、为什么结构体 A 和 B 的成员类型都是一样的,只是顺序不同,但打印的结果不同。
2、为什么打印的结果不是 6 而是 12 和 8 ?
结构体的对齐规则:
1. 第一个成员在与结构体变量偏移量为 0 的地址处。
2. 其他成员变量要对齐到某个数字 ( 对齐数 ) 的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数与该成员大小的较小值(如果该成员是数组类型,就将数组元素的类型与编译器默认的一个对齐数比较)。
ㅇVS中默认对齐数为 83. 结构体总大小为每个成员的对齐数的最大值 (包括第一个成员 ) 与默认对齐数取小后的数的整数倍。
4. 如果嵌套了结构体的情况 , 嵌套的结构体对齐到自己的每个成员的对齐数的最大值的整数倍处 , 结构体的整体大小就是所有成员的对齐数的最大值 ( 含嵌套结构体的对齐数, 嵌套结构体的对齐数就是自己的每个成员的对齐数的最大值) 的整数倍。
易错:第一个成员的偏移量是 0 !
具体解释:
1. 第一个成员在与结构体变量偏移量为 0 的地址处。
所谓的偏移量,就是指某个内存单元与结构体变量第一个成员的存储单元的距离。
拿上面的 struct A 举例:
第一个成员的类型占多少字节就占多少格子,如果 struct A 的第一个成员是 int 类型的,就占 4 个格子。
2. 其他成员变量要对齐到某个数字 ( 对齐数 ) 的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。
ㅇVS中默认对齐数为 8
也就是从第二个成员开始 , 每个成员都要对齐到 ( 一个对齐数 ) 的整数倍处
对齐数 : 结构体成员自身大小和默认对齐数的较小值
VS : 默认对齐数是 8
Linux gcc : 没有默认对齐数 , 对齐数就是结构体成员的自身大小
struct A 的第二个成员,也就是 int a,a 的大小是 4 个字节,现在使用 VS ,默认对齐数是 8,取较小值 4,也就是说 a 的存储空间的偏移量只要是 4 的最小倍数就行了,char c 同理。
3. 结构体总大小为每个成员的对齐数的最大值 (包括第一个成员 ) 的整数倍。
这就非常好理解了,struct A 中 b 的对齐数是 1,a 的对齐数是 4,c 的对齐数也是 1,所以最大对齐数是 4, struct A 的总大小应该为 4 的整数倍,而现在 struct A 的总大小是 9,因此 struct A 应该再占 3 个字节,总大小变为 12 才对。
这就是为什么 struct A 的总大小是 12 而不是 6 的原因,同样,为什么结构体 A 和 B 的成员类型都是一样的,只是顺序不同,但大小不同也能解释了。
使用 offsetof 验证
offsetof 是一个宏,使用它需要包含头文件:#include <stddef.h>
它是用来计算结构体成员的偏移量的,它有两个参数,第一个参数是结构体名,第二个参数是结构体成员名,函数返回值就是偏移量,且是整形。以 struct A 为例:
为什么要内存对齐?
大部分的参考资料都是这样说的:
1. 平台原因 ( 移植原因 ) :
不是所有的硬件平台都能访问任意地址上的任意数据的 ; 某些硬件平台只能在某些地址处取某些特定类型的数据 , 否则抛出硬件异常。
2. 性能原因 :
数据结构 ( 尤其是栈 ) 应该尽可能地在自然边界上对齐。原因在于 :为了访问未对齐的内存 , 处理器需要作两次内存访问 ; 而对齐的内存访问仅需要一次访问。
总体来说 :
结构体的内存对齐是拿空间来换取时间的做法。
如何设计结构体
那在设计结构体的时候 , 我们既要满足对齐 , 又要节省空间 , 如何做到?
让占用空间小的成员尽量集中在一起。
例如上述的 struct A 和 struct B,它们的成员一模一样,只是顺序不同,但两者的大小有所区别,struct A 占 12 个字节,而 struct B 只占 8 个字节。
修改默认对齐数
可以使用 #pragma pack( )修改默认对齐数:
#pragma pack(1)//修改默认对齐数为 1
struct B
{
char b;
char c;
int a;
};
#pragma pack()//恢复默认对齐数为 8
结构在对齐方式不合适的时候,可以更改默认对齐数。
结构体传参
结构体传参建议将函数参数设置为结构体指针变量
原因:函数传参的时候,参数是需要压栈的。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
位段
位段的定义
位段的定义和结构体是类似的 , 但有两个不同:
1.位段的成员类型必须是 int、unsigned int 、char、unsigned char 。
2.位段的成员名后边有一个冒号和一个数字(数字称为位,位的单位是比特,位不能超过该类型的大小)
例子:
struct A
{
int _a : 1;
int _b : 5;
int _c : 10;
int _d : 30;
};
A 就是一个位段 ,_a 是命名习惯。
位段的作用(优点)
有时候一个结构体中的 int 类型的数据不需要 32 个 bit 位来存储数据,而只需要少于 32 个 bit 位就可以完整的存储数据,此时就可以使用位段来节省空间,即指定一个 int 类型的变量的实际大小。上述 A 位段中,a 变量的实际大小就是 1 bit。
位段的空间分配
以上述位段 A 为例:先开辟一个 int 类型,即 32 个 bit 位,_a 使用这 32 个 bit 位的 1 位,而到底是从左开始使用还是从右开始使用取决于编译器(VS 从右开始使用),_b 使用 5 位,_c 使用 10 位,还剩 16 位,而 _d 要使用 30 个 bit 位,明显不够,再开辟一个 int 类型,即 32 位,而 _d 到底是使用剩余的 16 位和新开辟的空间的 14 位,还是直接全部使用新开辟的空间(VS 使用这种方式)取决于编译器。
位段的缺点
1、位段中 int 类型被当成是无符号还是有符号是不确定的。
2、在 16 位机器中 int 类型大小是 16 bit,32 位和 64 位机器中 int 类型的大小是 32 bit,假如指定 位为 27,在 16 位机器上会出现问题。
3、对于新开辟的空间,到底是从左开始使用还是从右开始使用取决于编译器。
4、当空间不够用时,到底是使用剩余的位和新开辟的空间的位,还是直接全部使用新开辟的空间的位取决于编译器。
枚举
枚举类型的定义
enum A
{
a,b,c
};
枚举类型的初始化
A 是枚举类型名,a,b,c 是 A 枚举类型的可能取值,默认为 0,1,2,3...... ,也可以人为指定它们的数值,如 a = 1,b = 2,c = 3。
如果只初始化 a 的值为 -5 ,则 a 和 b 和 c 会依次递增 1 ,即 b = -4,c = -3。
如果初始化 b 的值为 5,则 a 为 0,c 为 6。
枚举类型的成员的数值一但确定就无法在其他地方更改,比如在 main 函数中令 a = 2 是不合法的。
枚举的优点
我们可以使用 #define 定义常量,为什么非要使用枚举?
1. 增加代码的可读性和可维护性(switch 语句的 case)
2. 和 #define 定义的标识符比较,枚举有类型检查,更加严谨。
3. 防止了命名污染 ( 封装 )
4. 便于调试(#define 定义的标识符在运行时已经被替换)
5. 使用方便,一次可以定义多个常量
共用体
共用体的定义
定义的基本形式与结构体相似:
union A
{
int a;
char b;
};
可以看到成员 c 和 i 共用同一段内存。
共用体大小的计算
共用体也存在内存对齐
1、共用体的大小至少是最大成员的大小。
2、当最大成员大小不是最大对齐数的整数倍的时候 , 就要对齐到最大对齐数的整数倍。