• C语言笔记第12篇:自定义类型(struct结构体)


    1、结构体类型的声明

    为什么要有自定义的结构类型呢?

    这是因为稍微复杂的类型,直接使用内置类型是不行的!比如:描述一个人或 一本书的价格、版号等信息。

    1.1 结构的创建

    结构体是一些值的集合,这些值称为成员变量,结构的每个成员可以是不同类型的变量。

    1.1.1 结构的声明
    1. struct tag
    2. {
    3. member-list;//成员列表,可以有多个成员
    4. }variable-list;//变量列表,可以使用该类型创建多个变量

    例如描述一个学生:

    1. struct Stu
    2. {
    3. char name[20];//名字
    4. int age;//年龄
    5. char sex[5];//性别
    6. char id[20];//学号
    7. };

    因为struct student是一个结构体类型声明,并不是函数,所以旁边不用加()。但是结构体后的分号是不可省略的,因为不管是函数声明还是自定义类型声明结尾都是必须有分号的。

    1.1.2 结构体类型的变量

    结构体类型变量有两种创建方式:

    方法1:

    1. struct Stu
    2. {
    3. char name[20];//名字
    4. int age;//年龄
    5. char sex[5];//性别
    6. char id[20];//学号
    7. };
    8. int main()
    9. {
    10. struct student s1, s2, s3;
    11. return 0;
    12. }

    结构体声明好后,直接使用该自定义类型创建变量

    方法2:

    1. struct Stu
    2. {
    3. char name[20];//名字
    4. int age;//年龄
    5. char sex[5];//性别
    6. char id[20];//学号
    7. }s3,s4,s5;

    声明结构体的同时创建变量

    注:结构体声明就像绘制建筑图纸,当建筑的图纸绘制好后。我可以通过这个图纸建造n个建筑(变量)。自定义类型的声明可以比作建筑图纸,而使用这个类型创建变量就可以看作照着建筑图纸搭建一个建筑。

    1.1.3 结构的初始化

    我们通过函数的声明创建好变量后可以给变量初始化,那如何给结构初始化呢?

    1. struct Stu
    2. {
    3. char name[20];//名字
    4. int age;//年龄
    5. char sex[5];//性别
    6. char id[20];//学号
    7. };
    8. int main()
    9. {
    10. struct Stu s1 = { "zhangsan",20,"nan","12345" };
    11. struct Stu s2 = {"lisi",21,"nan","54321"};
    12. return 0;
    13. }

    结构体变量的初始化是按照顺序来初始化的,你在声明结构类型时里面的成员是什么顺序的创建好变量后初始化就必须是什么顺序的,不能不按顺序乱初始化。

    但是有没有什么办法可以不按照顺序初始化呢?答案是有的:

    1. struct Stu
    2. {
    3. char name[20];//名字
    4. int age;//年龄
    5. char sex[5];//性别
    6. char id[20];//学号
    7. };
    8. int main()
    9. {
    10. struct Stu s1 = { "zhangsan",20,"nan","12345" };
    11. struct Stu s2 = {"lisi",21,"nan","54321"};
    12. struct Stu s3 = { .sex = "nan",.age = 18,.name = "wangwu",.id = "13579" };
    13. return 0;
    14. }

    这种方式相当于在初始化过程中访问该变量s3的每个成员并赋值,赋值可以不按照顺序,因为是在s3内部通过 ' . ' 来访问的,所以默认为s3.age访问。

    1. 以下两种方法是等价的:
    2. struct Stu s3 = { .sex = "nan",.age = 18,.name = "wangwu",.id = "13579" };
    3. struct Stu s3 = {0};
    4. s3.sex = "nan";
    5. s3.age = 18;
    6. s3.name = "wangwu";
    7. s3.id = "13579";

    这里需要了解到 ' . '是结构体的访问操作符,比如我想访问变量s3里的成员age,我就可以使用 ' . '

    s3.age,既然能访问也就可以通过这种方式来给成员赋值,s3.age = 30。 

    那我们如何打印结构体类型呢?比如:

    1. #include
    2. struct Stu
    3. {
    4. char name[20];//名字
    5. int age;//年龄
    6. char sex[5];//性别
    7. char id[20];//学号
    8. };
    9. int main()
    10. {
    11. struct Stu s1 = { "zhangsan",20,"nan","12345" };
    12. printf("%s %d %s %s", s1.name, s1.age, s1.sex, s1.id);
    13. return 0;
    14. }

    看上面代码就是通过 ' . '操作符访问该结构的每个成员并打印,这就是结构体的打印方式。

    1.2 结构的特殊声明

    在声明结构的时候,可以不完全的声明。

    比如:

    1. //匿名结构体类型
    2. struct
    3. {
    4. int a;
    5. char b;
    6. float c;
    7. }x;
    8. struct
    9. {
    10. int a;
    11. char b;
    12. float c;
    13. }a[20],*p;

    匿名结构体是不能声明好结构类型后再创建变量,这样会报错,比如:

    1. struct
    2. {
    3. int a;
    4. char b;
    5. float c;
    6. };
    7. int main()
    8. {
    9. struct x = {0};//会报错
    10. return 0;
    11. }

    匿名结构体的变量应该在声明的时候就创建,然后就可以直接使用该变量:

    1. #include
    2. struct
    3. {
    4. int a;
    5. char b;
    6. float c;
    7. }x;
    8. int main()
    9. {
    10. x.a = 10;
    11. x.b = 'a';
    12. x.c = 3.14f;
    13. printf("%d %c %f\n",x.a,x.b,x.c);
    14. return 0;
    15. }

    注:匿名结构体只能使用一次,就是在声明的时候创建变量,声明好后就不能创建变量了。

    那匿名结构体可以这样使用吗?

    1. struct
    2. {
    3. int a;
    4. char b;
    5. float c;
    6. }s = {0};
    7. struct
    8. {
    9. int a;
    10. char b;
    11. float c;
    12. }* ps;
    13. int main()
    14. {
    15. ps = &s;//err
    16. return 0;
    17. }

    警告:

    答案是不能,因为运行时会报错:指针ps和&s的类型不兼容。别看两个匿名结构类型的成员一模一样,但是编译器依然认为它们是两个不同的指针类型,所以不能相互赋值。

    解决方法:定义结果体不要使用匿名结构体

    1.3 结构的自引用

    在结构中包含一个类型为该结构本身的成员是否可以呢?

    比如,定义一个链表的节点:

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

    上述代码正确吗?如果正确,那sizeof(struct Node)是多少?

    仔细分析,其实是不行的,因为一个结构体中再包含一个同类型的结构体变量,这样结构体变量的大小会无穷大,是不合理的。

    正确的自引用方式:

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

    因为指针就是用来存储地址的,地址的大小是4/8个字节,所以大小可以固定。

    1.3.1 typedef类型重命名

    typedef是C语言的关键字,作用是类型重命名

    比如:如果觉得struct Node太长太麻烦,就使用类型重命名:

    1. typedef struct Node
    2. {
    3. int data;
    4. Node* next;//将struct Node改名为Node可以在内部使用吗?
    5. }Node;//类型声明时使用typedef改名时是在这个位置,这里是要更改的名字,不是变量

    将struct Node改名为Node后可以在内部使用吗?

    答案是不能,因为是先声明后改名,在声明阶段还未改名,就用上Node来表示自引用类型,编译器不认识就会报错,typedef改名后是在后来想创建该类型变量时可以使用改名后的Node来创建,在之前是不能使用的,所以还是应该这样使用:

    1. typedef struct Node
    2. {
    3. int data;
    4. struct Node* next;
    5. }Node;
    6. int main()
    7. {
    8. Node* n1 = NULL;等价于 struct Node* n1 = NULL;
    9. return 0;
    10. }

    2、结构体内存对齐

    我们已经掌握了结构体的基本使用了。

    现在我们深入讨论一个问题:计算结构体的大小

    这也是一个特别热门的考点:结构体内存对齐

    注:结构体类型的大小是由结构体内存对齐来决定的。

    看下面代码:

    1. #include
    2. struct S1
    3. {
    4. char c1;
    5. char c2;
    6. int a;
    7. };
    8. struct S2
    9. {
    10. char c1;
    11. int a;
    12. char c2;
    13. };
    14. int main()
    15. {
    16. int ret1 = sizeof(struct S1);
    17. int ret2 = sizeof(struct S2);
    18. printf("%d\n%d\n", ret1, ret2);
    19. return 0;
    20. }

    运行结果:

    结果不一样,为什么?虽然是不同的结构类型,但是每个结构类型的成员都是一模一样的,不同点就是顺序有所差异,为什么最后类型的大小不一样?

    这就要谈到结构体的对齐规则了,如果结构体的成员顺序有所差异,也会导致对齐规则开辟的空间大小不相同。

    2.1 对齐规则

    首先得掌握结构体的对齐规则:

    1. 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处

    2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处

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

    • VS中默认的值为8
    • Linux中gcc没有默认对齐数,对齐数就是成员自身的大小

    3. 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍

    4. 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍

    知道了上面的规则了,那我们就可以通过例子来更加清晰的认识到对齐规则。

    例1:

    我们先来看结构体struct S1是如何在内存中对齐的:

    1. struct S1
    2. {
    3. char c1;
    4. char c2;
    5. int c3;
    6. };

    首先就是第一条规则:结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处

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

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

    • VS中默认的值为8
    • Linux中没有默认对齐数,对齐数就是成员自身的大小

    c2是char类型,大小是1字节,和默认对齐数8对比1最小,1的整数倍是任何数

    还是第二规则,c3是int类型,大小4字节,默认对齐数是8字节,所以要对齐到4的整数倍的偏移量的位置

    最后就是第三规则:结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍。

    注意:这里的整数倍不是看偏移量,是看已占内存空间大小是不是整数倍

    例2:

    然后再来看struct S2的对齐过程:

    1. struct S2
    2. {
    3. char c1;
    4. int a;
    5. char c2;
    6. };

    还是第一条规则:让第一个成员对齐到偏移量为0的地址处

    然后第二条规则:对比默认对齐数,选出最小对齐数对齐到该对齐数的整数倍

    还是第二条规则,对齐到char类型对齐数的整数倍

    最后就是第三条规则:结构体最终大小是最大对齐数的整数倍,该结构体最大对齐数是4

    那可能有人问了,中间不是还空着内存空间吗?为什么不用呢?这样放在一起不是更节省空间吗?为什么要浪费呢?

    虽然在内存开辟那么大的空间,但是对齐后中间可能会有开辟的空间但未使用,这是因为对齐规则就是这样,是以空间换取效率的开辟方式,也是为了平台的移植性。

    如果使用结构体时想要知道某个成员的偏移量,难道我们要自己算出来吗?当然不是,我们可以使用C语言里的一种宏,叫offsetof,offsetof需要两个参数:

    1. offsetof(type,member);
    2. 类型 成员

    使用offsetof只需要传一个结构体类型,再将结构体类型成员传过去,他会返回size_t类型的一个值,这个值就是它计算出的偏移量。

    如果想要使用必须包含头文件#include

    1. #include
    2. #include
    3. struct S1
    4. {
    5. char c1;
    6. char c2;
    7. int a;
    8. };
    9. int main()
    10. {
    11. printf("%zd\n",offsetof(struct S1,c1));
    12. printf("%zd\n",offsetof(struct S1,c2));
    13. printf("%zd\n",offsetof(struct S1,a));
    14. return 0;
    15. }
    2.2 为什么存在内存对齐

    大部分的参考资料都是这样说的:

    1. 平台原因(移植原因)

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

    2. 性能原因

    数据结构(尤其是栈)应该尽可能地在自然边界上对齐,原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存仅需一次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。

    总体来说:结构体的内存对齐是拿空间来换取时间的做法。

    那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:

    让占用空间小的成员尽量集中在一起:

    1. //例如
    2. struct S1
    3. {
    4. char c1;
    5. int a;
    6. char c2;
    7. };//占用了12个字节
    8. struct S2
    9. {
    10. char c1;
    11. char c2;
    12. int a;
    13. };//占用了8个字节

    S1和S2类型的成员一模一样,但是S1和S2所占空间大小有一定的区别

    2.3 修改默认对齐数

    #pragma 这个预处理指令,可以改变编译器的默认对齐数。

    1. #include
    2. #pragma pack(1)//设置默认对齐数为1
    3. struct S
    4. {
    5. char c1;
    6. int a;
    7. char c2;
    8. };
    9. #pragma pack()//取消设置的默认对齐数,还原默认对齐数
    10. int main()
    11. {
    12. printf("%d\n", sizeof(struct S));
    13. return 0;
    14. }

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

    3、结构体传参

    函数调用时,结构体传参尽量传地址过去,因为结构体可能是一个非常大的空间,在传参时是需要压栈来存储传过来的实参的,所以我们将地址传参过去,可以提高程序效率。

    两种结构体传参方式:

    方法1:传值调用

    1. #include
    2. struct S
    3. {
    4. int data[10];
    5. int num;
    6. };
    7. void print1(struct S s)
    8. {
    9. int sz = sizeof(s.data) / sizeof(s.data[0]);
    10. int i = 0;
    11. for (i = 0; i < sz; i++)
    12. {
    13. printf("%d ", s.data[i]);
    14. }
    15. printf("\n%d\n", s.num);
    16. }
    17. int main()
    18. {
    19. struct S s = { {1,2,3,4,5,6,7,8,9,10},20 };
    20. print1(s);//传递结构体变量
    21. return 0;
    22. }

    方法2:传址调用

    1. #include
    2. struct S
    3. {
    4. int data[10];
    5. int num;
    6. };
    7. void print2(const struct S* s)//不希望指针修改该空间就修饰const
    8. {
    9. int sz = sizeof(s->data) / sizeof(s->data[0]);
    10. int i = 0;
    11. for (i = 0; i < sz; i++)
    12. {
    13. printf("%d ", s->data[i]);
    14. }
    15. printf("\n%d\n", s->num);
    16. }
    17. int main()
    18. {
    19. struct S s = { {1,2,3,4,5,6,7,8,9,10},20 };
    20. print2(&s);//传递结构体变量的地址
    21. return 0;
    22. }

    '->'是结构体指针解引用操作符,正常结构体使用 ' . ' 来访问成员,而结构体指针可以直接使用 '->'来访问成员

    stu->num ==等价于== *(stu).num

    上面两种传参方式哪种更好?

    答案是:首选传址调用。

    原因:

    1. 函数传参的时候,参数是需要压栈的,会有时间和空间上的系统开销。

    2. 如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能下。

    结论:结构体传参的时候,要传结构体的地址。

    4、位段

    结构体讲完就得讲讲结构体实现  位段  的能力。

    注:位段是基于结构体,位段的出现是为了节省空间

    4.1 什么是位段

    位段的声明和结构是类似的,有两个不同:

    1. 位段的成员必须是 int、unsigned int、或 signed int,在C99 中位段成员的类型也可以选择其他类型。

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

    比如:

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

    那冒号 ' : ' 后面的数字是什么意思呢?其实冒号后面的数字是给该成员分配的空间大小,单位是二进制位,比如成员_a后面是:2意思是我给该成员分配2个二进制位来存放数据,1个二进制位是1bit,所以可以简单理解为后面的数字的单位就是bit。

    所以成员变量_a:2就是2个bit位,_b:5就是5个bit位,_c:10就是10个bit位,_d:30就是30个bit位。

    注:结构体位段不会内存对齐

    知道了位段信息,我们就可以根据该信息算出上面的结构体A的大小,最后算出一共是47个bit位,大概是6个字节。如果不使用位段4个整型的变量也是16个字节。但是结果真的是6个字节吗?我们可以使用sizeof运算一下。

    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("%d\n",sizeof(struct A));
    12. return 0;
    13. }

    运算结果:

    我们算出的位段总共加起来差不多6个字节,那为什么结果是8个字节呢?

    这就要看位段的内存分配方式了,经过第一个成员位段在开辟空间时首先不管成员位段后面的空间,而是看成员的类型,是int类型就先开辟一个4个字节,32个二进制位的空间。开辟好后就开始看第一个成员变量_a位段的数字,首先是2bit,可以存放。接下来是看_b和_c后面的数字,还是可以在所开辟的空间范围之内申请空间。此时已经占用了17个bit位了,但是_d是30,剩下的空间不够申请30个bit了,所以又要开辟一块空间,怎么开辟呢?就是要看_d的类型,是整型所以又开辟了32个bit,又开辟了4个字节,最后分配给_d30个bit。所以最后结果是8个字节。可以看到位段可能会浪费一些空间,但是相对结构体位段的空间节省较好一些。

    注意:位段后面分配的位数大小是不能超出自身类型的大小的,比如char类型的变量不能分配9个bit,int类型不能分配33个bit。

    4.2 位段的内存分配

    1. 位段成员可以是int、unsigned int、signed int 或是 char等类型。

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

    3. 位段涉及很多不确定因素,位段是不跨平台的,注意可移植的程序应该避免使用位段。

    为了大家能够更深刻的理解位段,特举了下面代码例子:

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

    该位段大小为3个字节,为什么是三个字节呢?那这些值在内存中如何存储的呢?可以根据下图来分析。

    4.3 位段的跨平台问题

    1. int 位段被当成有符号数还是无符号数是不确定的。

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

    3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。

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

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

    4.5 位段使用的注意事项

    位段的几个成员共有同一个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的,内存中每个字节分配一个地址。一个字节内部的bit位是没有地址的。

    所以不能对位段的成员使用&操作符,这样不能使用scanf直接给位段的成员输入值,只能是先输入放在一个变量中,然后赋值给位段成员。

    本篇博客到了这里也就结束了,再见

  • 相关阅读:
    Load-balanced-online-OJ-system 负载均衡的OJ系统项目
    力扣 272. 最接近的二叉搜索树值 II 递归
    用于高等教育的 DocuWare 文档管理软件
    vscode各种配置的方法
    PY32F003F18之定时器中断
    react异常 Each child in a list should have a unique “key” prop
    服务器开发不常见操作
    无人机测试用例
    【在英伟达nvidia的jetson-orin-nx和PC电脑ubuntu20.04上-装配ESP32开发调试环境-基础测试】
    LeetCode 每日一题——1413. 逐步求和得到正数的最小值
  • 原文地址:https://blog.csdn.net/2302_78977491/article/details/139413361