目录
结构体
结构体的定义
typedef
匿名结构体
结构体的自引用
结构体变量的初始化
访问结构体成员的方式
结构体的内存对齐
如何设计结构体
位段
枚举体
1. 基本语法
2. 定义和使用示例
2.1 简单枚举
2.2 指定枚举值
3. 枚举变量定义方式
枚举 vs 宏定义
共用体
共用体的定义
共用体大小的计算
结构体
结构体是什么?
结构是一些值的集合 , 这些值称为成员变量。结构的每个成员可以是不同类型的变量。
数组也是一些值的集合,但这些值的类型是相同的。
结构体的定义
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);即:结构体变量名 . 结构体成员名,如果要访问结构体内的结构体变量,则需要使用多个成员运算符。
通过结构体指针访问结构体成员:
使用 "->" 运算符访问:
用上面的结构体变量 b1 为例:
string B* pb = &b1; printf("%d",pb->b1);即:结构体指针名 -> 结构体成员名,-> 的作用相当于(*结构体指针名) . 结构体成员名
结构体的内存对齐
先来看一个例子:
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中默认对齐数为 8ㅇLinux gcc : 没有默认对齐数 , 对齐数就是结构体成员的自身大小
3. 结构体总大小为每个成员的对齐数的最大值 (包括第一个成员 ) 与默认对齐数取小后的数的整数倍。
4. 如果嵌套了结构体的情况 , 嵌套的结构体对齐到自己的每个成员的对齐数的最大值的整数倍处 , 结构体的整体大小就是所有成员的对齐数的最大值 ( 含嵌套结构体的对齐数, 嵌套结构体的对齐数就是自己的每个成员的对齐数的最大值) 的整数倍。
易错:第一个成员的偏移量是 0 !
具体解释:
1. 第一个成员在与结构体变量偏移量为 0 的地址处。
所谓的偏移量,就是指某个内存单元与结构体变量第一个成员的存储单元的距离。
拿上面的 struct A 举例:
第一个成员的类型占多少字节就占多少格子,如果 struct A 的第一个成员是 int 类型的,就占 4 个格子。
2. 其他成员变量要对齐到某个数字 ( 对齐数 ) 的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。
ㅇVS中默认对齐数为 8
也就是从第二个成员开始 , 每个成员都要对齐到 ( 一个对齐数 ) 的整数倍处
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、当空间不够用时,到底是使用剩余的位和新开辟的空间的位,还是直接全部使用新开辟的空间的位取决于编译器。
枚举体
1. 基本语法
enum 枚举名 { 标识符1, 标识符2, ... 标识符n };2. 定义和使用示例
2.1 简单枚举
#include <stdio.h> // 定义星期枚举 enum Weekday { MON, // 默认值为0 TUE, // 1 WED, // 2 THU, // 3 FRI, // 4 SAT, // 5 SUN // 6 }; int main() { enum Weekday today = WED; // enum Weekday today = 2; error:不能用 int 初始化 // 只能用定义的标识符 printf("today = %d\n", today); // 输出: 2 // 枚举比较 if (today == WED) { printf("今天是星期三\n"); } return 0; }2.2 指定枚举值
#include <stdio.h> enum Color { RED = 1, GREEN = 3, BLUE = 5, YELLOW, // 自动为6 BLACK = 10, WHITE // 自动为11 };3. 枚举变量定义方式
3.1 先定义类型,再定义变量
enum Season {SPRING, SUMMER, AUTUMN, WINTER}; enum Season s1, s2;3.2 定义类型同时定义变量
enum Season {SPRING, SUMMER, AUTUMN, WINTER} s1, s2;3.3 匿名枚举
enum {SPRING, SUMMER, AUTUMN, WINTER} s1, s2;3.4 typedef定义枚举别名
typedef enum { SPRING, SUMMER, AUTUMN, WINTER } Season; Season s1 = SPRING;枚举 vs 宏定义
// 使用宏定义 #define MON 0 #define TUE 1 #define WED 2 // 使用枚举 enum Weekday {MON, TUE, WED}; // 枚举的优势: // 1. 自动递增 // 2. 调试时可显示符号名称 // 3. 更好的类型检查 // 4. 作用域控制共用体
共用体的定义
定义的基本形式与结构体相似:
union A { int a; char b; };可以看到成员 c 和 i 共用同一段内存。
共用体大小的计算
共用体也存在内存对齐
1、共用体的大小至少是最大成员的大小。
2、当最大成员大小不是最大对齐数的整数倍的时候 , 就要对齐到最大对齐数的整数倍。