本篇将对C语言自定义类型进行讲解
1.结构体(struct)
前面简单讲过结构体,这里将会把前面结构体还没讲完的知识继续补充。复习链接:
1.1 结构的基础知识
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
1.2 结构的声明
struct tag //结构体关键字+标签 合起来是结构体类型 { member - list; //成员变量列表 }variable - list; //变量列表
例如描述一个学生:
struct Stu { char name[20];//名字 int age;//年龄 char sex[5];//性别 char id[20];//学号 }; //分号不能丢
1.3 匿名结构体
//匿名结构体类型(一次性使用)
struct { int a; char b; float c; double d; } s; struct { int a; char b; float c; double d; } *ps;
对于上面的代码如果进行如下操作,是非法的
int main() { ps = &s; // error return 0; }
1.4 结构的自引用
介绍:结构体中包含一个类型为该结构体本身的成员,包含同类型的结构体指针(不是包含同类型的结构体变量)
struct A { int i; char c; }; struct B { char c; struct A sa; double d; };
注意事项1:结构体不能自己包含自己,不能包含同类型的结构体变量
struct N { int d; struct N n; //结构体里不能存在结构体自己类型的成员 };
为了加深理解,先引入一下数据结构的一些知识:
注意事项2:结构体自引用时,不要用匿名结构体:
struct // 如果省略结构体名字 { int data; struct Node* next; // 这里的 struct Node* 是哪里来的? };
即使使用 typedef 重新取名为 Node,也是不行的。因为要产生 Node 必须先有结构体类型之后才能重命名 Node,即先 Node* next 定义完成员之后才 typedef 才能对这个类型重命名为 Node。
所以这种方式仍然是不行的:
typedef struct { int data; Node* next; // 先有鸡还是先有蛋??? } Node;
正确方法:
typedef struct Node { int data; struct Node* next; } Node;
1.5 结构体变量的定义和初始化
有了结构体类型,那如何定义变量,其实很简单。
struct S { char c; int i; } s1, s2; // 声明类型的同时创建变量 int main() { struct S s3, s4; return 0; }
创建变量的同时赋值(初始化)
struct S { char c; int i; } s1, s2; int main() { struct S s3 = {'x', 20}; // c i return 0; }
结构体包含结构体的初始化方法:
struct S { char c; int i; } s1, s2; struct B { double d; struct S s; char c; }; int main() { struct B sb = {3.14, {'w', 100}, 'q'}; printf("%lf %c %d %c\n", sb.d, sb.s.c, sb.s.i, sb.c); return 0; }
1.6 结构体内存对齐
现在已经掌握了结构体的基本使用了。
深入讨论一个问题:计算结构体的大小。
这也是一个特别热门的考点: 结构体内存对齐
首先得掌握结构体的对齐规则:
1. 第一个成员在与结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
(对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。VS中默认的值为8)
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
//练习1 #include <stdio.h> struct S { char c1; int i; char c2; }; int main() { struct S s = {0}; printf("%d\n", sizeof(s));//12 return 0; }
//练习2 struct S2 { char c1; char c2; int i; }; printf("%d\n", sizeof(struct S2));//8 //练习3 struct S3 { double d;//8 char c;//1 int i;//4 }; printf("%d\n", sizeof(struct S3));//16
//练习4-结构体嵌套问题 #include <stdio.h> struct S4 { double d; char c; int i; }; struct S5 { char c1; struct S4 s4; double d; }; int main() { struct S5 s5 = {0}; printf("%d\n", sizeof(s5)); return 0; } //上面第4点: 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处, //结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。 //此题嵌套的结构体对齐到自己的最大对齐数就是8(所以浪费了下面的7个字节)且32是8的倍数
为什么存在内存对齐? 大部分的书籍都是这样说的:
1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;
某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;
而对齐的内存访问仅需要一次访问。
总体来说:
结构体的内存对齐是拿空间来换取时间的做法。
那在设计结构体的时候,既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起。
//例如: struct S1 { char c1; int i; char c2; }; struct S2 { char c1; char c2; int i; }; //S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。 //S1大小是12 而S2大小是8
1.7 修改默认对齐数
之前我们见过了 #pragma 这个预处理指令,这里再次使用,可以改变默认对齐数。
#include <stdio.h> // 默认对齐数是8 #pragma pack(2) // 把默认对齐数改为2(一般改为2的几次方) struct S { char c1; //1 int i; // 4 char c2; // 1 }; #pragma pack() // 取消 int main() { printf("%d\n", sizeof(struct S)); //修改默认对齐数后12变为8 return 0; }
所以在结构在对齐方式不合适的时候,可以自己更改默认对齐数。
百度笔试题:
写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明
注:这里还没学习宏,可以放在宏讲解完后再自己实现。
考察: offsetof 宏的实现
该宏用于求结构体中一个成员在该结构体中的偏移量。
头文件: stddef.h
使用方法演示:
#include <stdio.h> #include <stddef.h> struct S { char c1; //1 int i; // 4 char c2; // 1 }; int main() { printf("%d\n", offsetof(struct S, c1));//0 printf("%d\n", offsetof(struct S, i));//4 printf("%d\n", offsetof(struct S, c2));//8 return 0; }
C语言进阶⑮(自定义类型)(结构体+枚举+联合体)(结构体实现位段)(中):https://developer.aliyun.com/article/1513097