本章重点
C语言中除了有char,short,int,long,long long,double,foat这些内置类型,还有结构体,枚举,联合等这些自定义类型。
结构体:结构体类型的声明 结构的自引用 结构体变量的定义和初始化 结构体内存对齐 结构体传参 结构体实现位段(位段的填充&可移植性)
枚举:枚举类型的定义 枚举的优点 枚举的使用
联合:联合类型的定义 联合的特点 联合大小的计算
一 结构体
1.1 结构的基础知识
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
1.2 结构的声明
1. #include <stdio.h> 2. 3. struct stu//struct是结构体关键字,stu是结构体标签 4. { 5. //这里面放成员列表 6. char name[10]; 7. char sex[5]; 8. int age; 9. int height; 10. }s2,s3,s4;//这里的s2,s3,s4也是结构体变量,是一个全局变量 11. 12. struct stu s5;//这里的s5是结构体变量,是一个全局变量 13. 14. int main() 15. { 16. struct stu s1 = { "xianming", "nan", 18, 43 };//这里的s1就是结构体变量,是一个局部变量 17. return 0; 18. }
1.3 特殊的声明
匿名结构体类型
1. #include <stdio.h> 2. 3. struct 4. { 5. char c; 6. int a; 7. double d; 8. }e;//结构体变量必须在这里命名,而且只能使用一次 9. 10. struct 11. { 12. char c; 13. int a; 14. double d; 15. }* ps; 16. 17. int main() 18. { 19. ps = &e;//在这里是不能这样用的,会出现错误,即使结构体成员一模一样也不可以 20. //编译器认为等号两边是不同的结构体类型,所以这种写法是错误的 21. return 0; 22. }
编译器会把上面的两个结构体声明当成完全不同的两个类型。所以是非法的
省略结构体标签,编译器也会认为这两个结构体是不同的。(虽然省略结构体标签后,这两个结构体成员是一样的)
1.4 结构的自引用
下面为正确的自引用的方式:
1. struct Node 2. { 3. int m; 4. struct Node* next; 5. };
1. #include <stdio.h> 2. 3. typedef struct Node//重命名这个结构体为Node 4. { 5. int data; 6. struct Node* next; 7. }Node; 8. //这种方法尽量不要使用,不建议 9. int main() 10. { 11. struct Node s2 = { 0 };//这种写法是可以的 12. Node s1 = { 0 };//这种写法也是可以的 13. return 0; 14. }
1.5 结构体变量的的定义和初始化
定义:
1. struct stu 2. { 3. char name[20]; 4. int age; 5. float score; 6. }; 7. int main() 8. { 9. struct stu s; 10. return 0; 11. }
1. struct stu 2. { 3. char name[20]; 4. int age; 5. float score; 6. }s1, s2;//这里的s1 s2 和s一样,也是结构体变量,(全局的) 7. struct stu s3;//定义一个初始化变量 全局变量 8. int main() 9. { 10. struct stu s;//s就是结构体变量,struct stu 就是和int char float 一样的类型,但是却是局部的 11. return 0; 12. }
初始化:
1. struct stu 2. { 3. char name[20]; 4. int age; 5. float score; 6. }; 7. 8. struct stu s2 = { "xiaohong", 20, 97.5f }; 9. 10. int main() 11. { 12. struct stu s = { "xiaoming", 20, 97.5f };//97.5 后面加f说明是float类型,不加的话,会默认为为double类型 13. printf("%s %d %f", s.name, s.age, s.score); 14. return 0; 15. }
1. 结构体嵌套初始化 2. struct Node 3. { 4. int data; 5. struct Point p; 6. struct Node* next; 7. }n1 = {10, {4,5}, NULL}; 8. 9. struct Node n2 = {10, {4,5}, NULL};
1.6 结构体内存对齐
这个知识点比较重要
计算结构体的大小,这也是一个特别热门的考点: 结构体内存对齐
代码1展示:
1. #include <stdio.h> 2. struct S1 3. { 4. char c1; 5. int i; 6. char c2; 7. }; 8. int main() 9. { 10. printf("%d\n", sizeof(struct S1)); 11. return 0; 12. }
运行的结果是:12
1. #include <stdio.h> 2. #include <stddef.h> 3. 4. struct S1 5. { 6. char c1; 7. int i; 8. char c2; 9. }; 10. //offsetof(a,b)计算的是返回的是size_t,a代表结构体类型名,b代表结构体成员名,头文件是<stddef.h> 11. //偏移量,第一个位置的偏移量是0. 12. int main() 13. { 14. printf("%d\n", sizeof(struct S1)); 15. printf("%d\n", offsetof(struct S1, c1)); 16. printf("%d\n", offsetof(struct S1, i)); 17. printf("%d\n", offsetof(struct S1, c2)); 18. return 0; 19. }
打印结果:12 0 4 8
c1是第一个成员,所以是0,第二个成员的对齐数是4,所以从4开始,因为int是4个字节,所以占了4个位置,然后c2的对齐数是1,然后8是1的倍数,所以是8.此时的大小是9(因为还有一个0处位置的大小),因为最终大小是最大对齐数的整数倍,所以是12个字节
对齐规则
1. 第一个成员在与结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。(比如该成员是int 4,与8相比,选择4,然后因为) VS中默认对齐数的值为8,linux没有默认对齐数,所以对齐数就是成员自身本身的大小
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,然后根据嵌套结构体的大小,占据相应的字节数, 结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
注意: 为了可以既要对齐,又要省空间 。可以让占用空间小的成员尽量集中在一起,以防浪费空间
(每一个成员都要确认一个对齐数,从0开始,总大小(注意加上0的大小)为最大对齐数的倍数)
为什么存在内存对齐?
大部分的参考资料都是如是说的:
1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特
定类型的数据,否则抛出硬件异常。
2. 性能原因:(32位平台,4个字节4个字节的读)
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访
问。
总体来说:
结构体的内存对齐是拿空间来换取时间的做法。
1.7 修改默认对齐数
#pragma 这个预处理指令,可以改变我们的默认对齐数
1. #include <stdio.h> 2. #pragma pack(2) //设置默认对齐数为2 3. struct S1 4. { 5. char c1; 6. int i; 7. char c2; 8. }; 9. #pragma pack() //取消设置的默认对齐数,还原为默认 10. 11. int main() 12. { 13. printf("%d\n", sizeof(struct S1)); 14. return 0; 15. }
打印结果:8
在不改变的情况下为,12.
结构在对齐方式不合适的时候,我么可以自己更改默认对齐数
1.8 结构体传参
结构体传参的时候,要传结构体的地址
1. #include <stdio.h> 2. struct S 3. { 4. int data[1000]; 5. int num; 6. }; 7. struct S s = { {1,2,3,4}, 1000 }; 8. void print2(struct S* ps) 9. { 10. printf("%d\n", ps->num); 11. } 12. int main() 13. { 14. print2(&s); //传地址 15. return 0; 16. }
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的
下降。