【C语言】自定义类型(结构体/枚举体/共用体)
目录结构体结构体的定义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 s1typedef 也可以在定义结构体之后再使用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 aa 的大小是 4 个字节现在使用 VS 默认对齐数是 8取较小值 4也就是说 a 的存储空间的偏移量只要是 4 的最小倍数就行了char c 同理。3. 结构体总大小为每个成员的对齐数的最大值 (包括第一个成员 ) 的整数倍。这就非常好理解了struct A 中 b 的对齐数是 1a 的对齐数是 4c 的对齐数也是 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 bit32 位和 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、当最大成员大小不是最大对齐数的整数倍的时候 , 就要对齐到最大对齐数的整数倍。