• 【进阶C语言】自定义类型


    本节内容大致目录如下:

    1.结构体

    2.位段

    3.枚举

    4.联合(共用体

    以上都是C语言中的自定义类型,可以根据我们的需要去定义。

    一、结构体

    一些基础知识在初阶C语言的时候已经介绍过,在这里粗略概括;重点介绍前面没有提到过的。

    1.结构体的声明

    声明其实就是需要自己创造一个结构体(类型)。后面再拿这个结构体(类型)去创造变量。

    (1)简单声明

    (2*)特殊的声明

    1. struct
    2. {
    3. int a;
    4. char b;
    5. float c;
    6. }x;//x为结构体创造出来的名字
    7. struct
    8. {
    9. int a;
    10. char b;
    11. float c;
    12. }a[20], * p;

    1.这种在结构体关键字前省略了名字,这种声明方式的结构体称为:匿名结构体类型。

    2.因为省略了名字,后续没法再进行变量的创造,所以只能使用一次

    3.两个相同的匿名结构体,属于两种不同的类型

    (3*)结构体的自引用

    1. struct Node
    2. {
    3. int data;
    4. struct Node* next;
    5. };

    1.在结构体内部,可以用自身结构体类型来创建的指针,称为结构体的自引用。

    2.一般用来数据结构的链表中,因为需要类型相同。

    2.结构体变量的创建和初始化


    在前面的时候,我们有介绍过在声明的时候创造的全局变量,接下来都一起介绍了。

    第一种创造方式:

    1. #include
    2. struct Stu
    3. {
    4.     char c;
    5.     int arr[10];
    6. };
    7. int main()
    8. {
    9.     struct Stu A;//结构体变量A
    10.     struct Stu B;//结构体变量B
    11.     return 0;
    12. }


    这里创造的变量A和B都是局部变量。

    第二种创造方式:

    1. #include
    2. typedef struct Stu
    3. {
    4.     char c;
    5.     int arr[10];
    6. }Stu;//对结构体重命名
    7. int main()
    8. {
    9.     //struct Stu A;//结构体变量A
    10.     //struct Stu B;//结构体变量B
    11.     Stu C;//结构体变量C
    12.     return 0;
    13. }


    第三种方式:上面提到过的创造全局变量

    1. #include
    2. struct Stu
    3. {
    4.     char c;
    5.     int arr[10];
    6. }D;//全局变量D
    7. int main()
    8. {
    9.     //struct Stu A;//结构体变量A
    10.     //struct Stu B;//结构体变量B
    11.     //Stu C;//结构体变量C
    12.     return 0;
    13. }


    在创建后变量后,就该对变量进行初始化了

    初始化:

    1. #include
    2. struct Stu
    3. {
    4.     char name[20];
    5.     int age;
    6.     double height;
    7. };
    8. int main()
    9. {
    10.     struct Stu s1 = {"zhangsan",20,182.8};//顺序初始化
    11.     struct Stu s2 = {.age=18,.height=188.5};//指定成员初始化
    12.     return 0;
    13. }


    在创建变量的时候就初始化:

    1. struct Stu
    2. {
    3.     char name[20];
    4.     int age;
    5.     double height;
    6. }s3 = {"lisi",19,150.6};//创造的全局变量并初始化

    3.结构体的内存对齐(*)

    这是本节的重点,所谓的内存对齐,就是要知道结构体类型的内存大小专门来的,我们该怎么计算。

    (1)引例

    我们先观察两个大体相同的结构体,为什么相同的变量不同的顺序,内存大小却不一样。

    1. struct s1
    2. {
    3. char c1;
    4. char c2;
    5. int i;
    6. };
    7. struct s2
    8. {
    9. char c1;
    10. int i;
    11. char c2;
    12. };
    13. #include
    14. int main()
    15. {
    16. printf("%zd\n", sizeof(struct s1));
    17. printf("%zd\n",sizeof(struct s2));
    18. return 0;
    19. }

    运行结果:

    第一个结构体类型s1的内存是8字节

    第二个结构体类型s2的内存是12字节

    造成这种原因是:在结构体中,存在内存对齐这一规则。

    (2)结构体内存对齐规则

     1)第一条规则:第一个成员在,与结构体变量偏移量为0的地址处

    什么是偏移量:

    把存储单元的实际地址与其所在段的段地址之间的距离称为段内偏移,也称为“有效地址或偏移量”。

    图解:

    第一个成员就从0位置开始存放,内存多大就占几个格子(字节)。

    2)第二条规则:其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处(偏移量处)

     什么是对齐数:

    对齐数=编译器默认的一个对齐数与该成员的内存大小的较小值

    (vs的默认值为8)

    图解:

     上述成员在内存中一共占据了8个字节的空间

    第二个结构体:

     这些成员在内存中所占的字节大小就是结构体的最终内存大小了吗?还没完,还需要根据第三条规则来计算。

    3)第三条规则:结构体的总大小为最大对齐数(每个成员变量都有一个对齐数) 的整数倍

    如:char的大小为1,1就是对齐数;像int大小为4,4就是对齐数;还有一个编译器默认对齐数,需要变量与其对比得出。

    对齐数图解:

    现在我们来计算结构体的大小

    第一个:

    第二个:

     该结构体在内存中占9个字节,不是4的整数倍,需要增大(增大到离9最近的数字且是4的整数倍),所以该结构体的内存大小为12字节。

    如果结构体嵌套又如何计算,我们看第四条规则

    4)第四条规则:如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍

    存放嵌套的结构体时,它也有自己的最大对齐数和内存大小,所以只需要把它存放到是它自己的最大对齐数的整数倍处即可。

     图解:

    变量C在右图所占的内存大小为16个字节,该结构体的最大对齐数为4,所以16为最终的内存大小。

    1. struct s1
    2. {
    3. char c1;
    4. char c2;
    5. int i;
    6. };
    7. struct s2
    8. {
    9. char c1;
    10. int i;
    11. char c2;
    12. };
    13. struct s3
    14. {
    15. char c1;
    16. struct s2 c2;
    17. };
    18. #include
    19. int main()
    20. {
    21. printf("%zd\n", sizeof(struct s1));
    22. printf("%zd\n", sizeof(struct s2));
    23. printf("%zd\n",sizeof(struct s3));
    24. return 0;
    25. }

     

    (3)内存对齐的原因

    1)平台原因(移植原因):不是所有的硬件平台都能访问任意地址处上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常

    2)性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐;原因在于,为了访问未对齐的内存,处理器需要做两次内存访问;而对齐的内存访问仅需要一次访问(拿空间换时间的一种做法)

    3)做法:既可以节省空间又能节约时间

    在设计结构体的时候,我们既要满足对齐,又要节省空间,就要让占用空间小的成员尽量集中在一起。

    例如:

    1. struct s1//已集中
    2. {
    3. char c1;
    4. char c2;
    5. int i;
    6. };
    7. struct s2//未集中
    8. {
    9. char c1;
    10. int i;
    11. char c2;
    12. };

    像struct s1的结构体就做到了上述的要求,内存只占8,而struct s2却占了12

    (4)修改默认对齐数

    我们可以通过修改默认对齐数,使结构体有更好的对齐方式,这里用#pragma这个预处理指令来修改。

    1. #pragma pack(1)//设置默认对齐数为1
    2. struct s1
    3. {
    4. char c1;
    5. char c2;
    6. int i;
    7. };
    8. #pragma pack()//恢复默认对齐数
    9. struct s2
    10. {
    11. char c1;
    12. char c2;
    13. int i;
    14. };
    15. #include
    16. int main()
    17. {
    18. printf("%zd\n", sizeof(struct s1));
    19. printf("%zd\n", sizeof(struct s2));
    20. return 0;
    21. }

    1.一样的数据类型和排列方式,所占内存却是不一样

    2.结构在对齐方式不合适的时候,我们可以自己更改默认对齐数

    3.默认对齐数,修改的结果一般要求为2^n,n>=1;不可以为符合或奇数(1除外)。

    二、位段

    位段是基于结构体的基础上的,位段是一种特殊的结构体--也是为了节省空间

    1.位段的定义

    位段的声明和结构是类似的

    (1)位段的成员必须是:int,unsigned int、char或signed int(c99之后也可以有其他的类型)

    (2)位段的成员名后边有一个冒号和数字

    (3)举例:

    1. struct A
    2. {
    3. int _a : 2;
    4. int _b : 5;
    5. int _c : 10;
    6. int _d : 30;
    7. };

    1. _a、_b这些只是为了更好知道这是位段才加的,也可以选择不加。

    2.后面的冒号和数字才是位段的语法要求。

    3.位段的“位”表示二进制位的意思,冒号后面的数字就是代表有多少二进制位。

    4.数字表明该成员变量最大的二进制位,如_a:2,_a只有两个二进制位,能表示的二进制数字只有:00,01,10和11,范围就是0-3。

    (4)位段的作用

     我们根据预知的数据内存,可以设置合理的二进制位,就可以达到节约空间的目的

    2.位段的内存分配

    (1)内存的计算

    1.位段也是一种结构体,所以位段也遵守内存对齐的方式。

    2.位段是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。

    如:

    下面的内存大小是多少呢?

    1. #include
    2. struct A
    3. {
    4. int _a : 2;
    5. int _b : 5;
    6. int _c : 10;
    7. int _d : 30;
    8. };
    9. int main()
    10. {
    11. printf("%zd\n",sizeof(struct A));
    12. return 0;
    13. }

    我们通过代码的运行结果可知:该位段的内存大小为8个字节

     我们接上面第二点:如:该位段都是int,一上来会先分配一个字节的空间(一字节==32bit),我们前面的2+5+10刚好存放在第一个字节的空间里面,而_d需要30bit,所以只能再开辟一个字节的空间,32bit拿出30比特刚好存放_d这个数据。

    (2)位段的内存分配规则

    1)位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
    2)位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
    3)位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

     比如上述第一个字节中的bit没有被用完,后续是否还用这是不确定的

    (3)内存分配实例

    看一段代码:

    1. #include
    2. struct S
    3. {
    4. char a : 3;
    5. char b : 4;
    6. char c : 5;
    7. char d : 4;
    8. };
    9. int main()
    10. {
    11. struct S s = {0};//初始化
    12. printf("%zd\n", sizeof(struct S));
    13. return 0;
    14. }

    先简单看这个位段会消耗多少字节?

    该结构体共消耗3字节。他们所占的二进制有3+4+5+4=16位,不应该是只占2字节吗?

    图解:

    现在我们已经知道了内存的分配,接下来了解数据是怎么存入的。

    代码:

    1. #include
    2. struct S
    3. {
    4. char a : 3;
    5. char b : 4;
    6. char c : 5;
    7. char d : 4;
    8. };
    9. int main()
    10. {
    11. struct S s = {0};//初始化
    12. printf("%zd\n", sizeof(struct S));
    13. //赋值
    14. s.a = 10;
    15. s.b = 12;
    16. s.c = 3;
    17. s.d = 4;
    18. return 0;
    19. }

     内存分配图解:

    这就是这些数据存入内存中的二进制形式,然后转化成16进制就是在调试窗口的展示形式,让我们看看是不是这样子呢?

    可以清楚看到三个字节中数据的存储方式,因为只能存储有限位的bit,所以需要控制数据的大小范围,否则会造成数据的丢失。

    3.位段的跨平台问题

    (1)int被当成有符号数还是无符号数是不确定的

    如上述代码的结果:

    (2)位段中最大位的数目不能确定。(16位机器最大位16,32位机器最大32位,写成27,在16位平台的机器上会出现问题)

    (3)位段中的成员在内存中是从左向右分配,还是从右向左分配标准尚未定义(大小端存储字节序)

    (4)当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的

    跟结构相比,位段可以达到同样的效果,可以更好的节省空间,但是存在跨平台问题。

    三、枚举

    所谓枚举,就是一一列举

    1.枚举的定义

    枚举,全名又称枚举常量,属于常量中的一种。定义枚举常量的时候需要用到enum的关键字。

    (1)定义格式

    1. enum Sex//性别常量
    2. {
    3. Man,//男
    4. Woman,//女
    5. Sercet,//保密
    6. };

    1.enum Sex称为枚举类型,Man、Woman和Sercet称为枚举常量

    2.格式与结构体相似,但是每个枚举常量后面是逗号。

    3.枚举常量的常量值默认从0开始。类似#define定义的常量一样,在后续的使用中他就是一个数字。

    4.枚举常量的名字一般首字母大写或者全部大写,便于识别。

    (2)打印枚举常量

    1. #include
    2. enum Sex//性别常量
    3. {
    4. Man,//男
    5. Woman,//女
    6. Sercet,//保密
    7. };
    8. int main()
    9. {
    10. printf("%d\n", Man);
    11. printf("%d\n", Woman);
    12. printf("%d\n",Sercet);
    13. return 0;
    14. }

    结果:

    (3)修改默认值

    第一种:修改起始值,后续的值会依次递增

    1. #include
    2. enum Sex//性别常量
    3. {
    4. Man=5,
    5. Woman,
    6. Sercet,
    7. };
    8. int main()
    9. {
    10. printf("%d\n", Man);
    11. printf("%d\n", Woman);
    12. printf("%d\n",Sercet);
    13. return 0;
    14. }

    第二种:任意赋值

    1. #include
    2. enum Sex//性别常量
    3. {
    4. Man=5,
    5. Woman=3,
    6. Sercet=100,
    7. };
    8. int main()
    9. {
    10. printf("%d\n", Man);
    11. printf("%d\n", Woman);
    12. printf("%d\n",Sercet);
    13. return 0;
    14. }

    注意:只能在定义的时候赋值,但是不能在后续的步骤中修改

    2.枚举的的优点

    (1) 增加代码的可读性和可维护性


    (2)和#define定义的标识符比较枚举有类型检查,更加严谨。

    枚举常量是有类型的(枚举类型),而#define定义的却没有
    (3)防止了命名污染(封装)


    (4)便于调试


    (5)使用方便,一次可以定义多个常量

    四、联合(共用体)

    1.联合体的定义

    (1)联合体是一种特殊的自定义类型,特征是这些变量公用一块空间。定义需要用到联合关键字union。

    (2)代码格式

    1. union Un
    2. {
    3. char c;
    4. int i;
    5. };
    与结构体格式相同,但是关键字不一样。

    联合变量的创建:

    1. #include
    2. union Un
    3. {
    4. char c;
    5. int i;
    6. };
    7. int main()
    8. {
    9. union Un u1;//联合变量
    10. printf("%zd",sizeof(u1));//计算联合变量的大小
    11. return 0;
    12. }

    2.联合体的特点

    (1)特点

    联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联
    合至少得有能力保存最大的那个成员)。

    1. #include
    2. union Un
    3. {
    4. char c;
    5. int i;
    6. };
    7. int main()
    8. {
    9. union Un u1;
    10. printf("%p\n", &u1);
    11. printf("%p\n", &(u1.c));
    12. printf("%p\n",&(u1.i));
    13. return 0;
    14. }

    运行结果:

    这三个的起始地址都一样,那在内存中是什么样的呢?

    他们是公用一块内存的,如果修改其中一个,另一个是很有可能就被修改了。

    (2)利用其特点解决的问题

    利用联合体判断当前机器是小端还是大端字节序?

    1. #include
    2. int check_sys()
    3. {
    4. union Un
    5. {
    6. char c;
    7. int i;
    8. }u;
    9. u.i = 1;
    10. return u.c;
    11. }
    12. int main()
    13. {
    14. int ret = check_sys();
    15. if (ret == 1)
    16. printf("小端\n");
    17. else
    18. printf("大端\n");
    19. return 0;
    20. }

    原理:

    返回的u.c,拿到的是i的第一个字节;如果小端存储。第一个字节(低字节)就会存储在低地址处,也就是起始位置,反之一样。

     3.联合大小的计算

    (1)联合的大小至少是最大成员的大小。(不一定就是最大的)
    (2)当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。(蹲守结构体的内存对齐规则)

    1. #include
    2. union Un
    3. {
    4. char a[5];
    5. int i;
    6. };
    7. int main()
    8. {
    9. printf("%zd\n",sizeof(union Un));
    10. return 0;
    11. }

    联合体一般用于有公用部分的时候,如果商场的购物系统,很多商品都有共同的属性,如:价格等。


    本章完 

  • 相关阅读:
    vue中的响应式数据和副作用函数
    Imu_PreIntegrate_07 Vecility bias update 零偏更新后速度预积分量对零偏的偏导
    Navicat 连接Docker的MySQL报错2003,10060 “Unknown error“
    环保行业智能供应链系统加快企业数字化转型,增强企业核心竞争力
    2024.3.28成都AI与机器视觉技术工业应用研讨会
    Python的简单使用与应用
    mysql主从复制与读写分离
    日常开发中,提升技术的13个建议
    MySql创建分区
    给一个数组赋1到10的初始值(指针)
  • 原文地址:https://blog.csdn.net/2301_77053417/article/details/133209869