当前位置:   article > 正文

C语言学习记录——结构体(声明、初始化、自引用、内存对齐、结构体设计、修改默认对齐数、结构体传参)_c语言的结构体自引用

c语言的结构体自引用

简单介绍

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

例如描述书是比较复杂的,包括书名、作者、出版社、定价、书号等。

我们可以创建一个书的类型,用来描述书,存储书的各项数据。将这若干项数据集合起来就是一个结构体

声明与定义

声明后定义

注:在声明结构体类型时,最后的分号不能漏掉

声明时定义

特殊的声明

不完全声明


对匿名结构体类型进行探讨:

  1. struct//匿名结构体类型
  2. {
  3. char c;
  4. int i;
  5. char ch;
  6. double d;
  7. }n;
  8. struct
  9. {
  10. char c;
  11. int i;
  12. char ch;
  13. double d;
  14. }*p;
  15. int main()
  16. {
  17. p = &n; //error
  18. return 0;
  19. }

以上两个匿名结构体类型虽然里面的成员是完全相同的,但是编译器会把上面的两个声明当成完全不同的两个类型。所以是非法的。


注:匿名结构体只能使用一次,不能再用于创建结构体变量。

结构自引用

创建一个情景:我们创建一个存储图书馆各项数据的结构体类型,其中包括图书、内饰等等。而图书中又存储着有关图书的各项数据,这时就可以用到结构的自引用了。例如:

  1. struct book //图书
  2. {
  3. char type[10];
  4. char name[20];
  5. int price;
  6. char number[30];
  7. };
  8. struct upholstery //内饰
  9. {
  10. char WallColor[10];
  11. char CeilingMolding[30];
  12. int LampSwitch;
  13. };
  14. struct library
  15. {
  16. struct book b;
  17. struct upholstery u;
  18. double d;
  19. };


对结构自引用进行探讨

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

当我们用struct Node来创建变量时,会发现这个变量的大小是无法计算的,进入了一个死递归的状态。

因此,这种写法是非法的。

正确的写法

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

不可以包含同类型的变量,可以包含同类型的指针变量。

与链表有关,在数据结构中会学到。链表的结点包罗两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。

另一种错误写法,将匿名结构体与自引用结合起来

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

很显然,这种写法是非法的。那么此时想尝试用typedef修改一下,可不可行呢?

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

来将这段代码捋一捋:将匿名结构体自定义为Node,那么先要让匿名结构体定义。而匿名结构体定义时要建立指针变量Node* next之后才算完成;但此时自定义还没有把struct定义为Node,故而以Node为类型的next变量不能成功建立。从而我们得知这段代码也是错误的,正确写法应该先自定义完成再定义结构体:

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

结构体的初始化

  1. struct A
  2. {
  3. int i;
  4. char c;
  5. }a;
  6. struct B
  7. {
  8. int n;
  9. char e;
  10. struct A a;
  11. }b;
  12. int main()
  13. {
  14. struct A a = { 32,'a' };//直接初始化
  15. printf("%d\n%c\n", a.i, a.c);
  16. b.n = 64;
  17. b.e = 'b';
  18. //利用操作符“.”初始化
  19. b.a.i = 128;
  20. b.a.c = 'c';
  21. //结构自引用(结构嵌套)的初始化
  22. printf("%d %c %d %c", b.n, b.e, b.a.i,b.a.c);
  23. return 0;
  24. }

利用操作符“->”进行初始化

  1. struct book
  2. {
  3. int price;
  4. char name[20];
  5. }b1,*p;
  6. int main()
  7. {
  8. p = &b1;
  9. p->price = 64;
  10. printf("%d\n", p->price);
  11. return 0;
  12. }

结构体内存对齐

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

  1. struct S1
  2. {
  3. char c; //1字节
  4. int i; //4字节
  5. char c2;//1字节
  6. };
  7. int main()
  8. {
  9. struct S1 s1 = { 0 };
  10. printf("%d\n", sizeof(s1));
  11. return 0;
  12. }

结果显示为12,这涉及到了结构体内存对齐的知识点。我们先了解结构体内存对齐的规则

对齐规则

1.第一个成员在与结构体变量偏移量为0的地址处。
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
3.结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

要更好地理解这个对齐规则,我们要先简单了解一些概念。

偏移量

对齐数

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

VS中默认的对齐数的值为8,;而Linux没有默认对齐数的概念。

这个时候我们拿起刚刚那道题,用对齐规则来解决。

练习

  1. struct S2
  2. {
  3. char c;
  4. int i;
  5. double d;
  6. };
  7. struct S3
  8. {
  9. double d;
  10. char c;
  11. int i;
  12. };
  13. int main()
  14. {
  15. struct S2 s2 = {0};
  16. struct S3 s3 = {0};
  17. }

试着算算s2和s3的大小,答案在后文公布。



嵌套结构体大小计算

  1. struct S4
  2. {
  3. char c1;
  4. struct S3 s3;
  5. double d;
  6. };
  7. int main()
  8. {
  9. struct S4 s4 = { 0 };
  10. printf("s4 = %d\n", sizeof(s4));
  11. return 0;
  12. }

练习答案


嵌套结构体计算大小:

存在内存对齐的原因

1.平台原因(移植原因)
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次访问;而对齐的内存访问仅需要一次访问。

对“访问未对齐的内存,处理器需要作两次访问;而对齐的内存访问仅需要一次访问。”做解释:

总的来说

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

结构体的设计

在设计结构体的时候,我们既要满足对齐,又要节省空间。

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

例如:

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

修改默认对齐数

使用#pragma这个预处理指令,可以改变默认对齐数。

具体为:

  1. #include <stdio.h>
  2. #pragma pack(2) //修改默认对齐数为2
  3. struct S1
  4. {
  5. char c1;
  6. int n;
  7. char c2;
  8. }s1;
  9. #pragma pack() //取消设置的默认对齐数,还原为8
  10. struct S2
  11. {
  12. char c1;
  13. int n;
  14. char c2;
  15. }s2;
  16. int main()
  17. {
  18. printf("s1 = %u\n", sizeof(s1));//默认对齐数为2时的大小
  19. printf("s1 = %u\n", sizeof(s1));//默认对齐数为8时的大小
  20. return 0;
  21. }

变量

占用大小

默认对齐数

对齐数

c1

1

8

1

n

4

8

4

c2

1

8

1

变量

占用大小

默认对齐数

对齐数

c1

1

2

1

n

4

2

2

c2

1

2

1

结论

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

结构体传参

试着对比下面两种传参方式

  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. //使用结构体传参
  9. void print1(struct S s)
  10. {
  11. printf("%d\n", s.num);
  12. }
  13. //使用结构体地址传参
  14. void print2(struct S* ps)
  15. {
  16. printf("%d\n", ps->num);
  17. }
  18. int main()
  19. {
  20. print1(s);//传结构体
  21. print2(&s);//传地址
  22. return 0;
  23. }

我们应该首选print2函数。

原因:

函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降。

而且,直接传结构体会有一些限制。例如,并不能改变结构体内部成员的值,而传地址既节省系统开销,提高性能,又不会有太多限制。

  1. #include <stdio.h>
  2. struct S
  3. {
  4. int data[1000];
  5. int num;
  6. };
  7. struct S s1 = { {1,2,3,4},1000 };
  8. struct S s2 = { {1,2,3,4},1000 };
  9. //结构体传参 修改成员变量的值
  10. void change1(struct S s1)
  11. {
  12. s1.num = 2000;
  13. }
  14. //结构体地址传参 修改成员变量的值
  15. void change2(struct S* ps)
  16. {
  17. ps->num = 2000;
  18. }
  19. int main()
  20. {
  21. change1(s1);
  22. change2(&s2);
  23. printf("s1.num = %d\n", s1.num);
  24. printf("s2.num = %d\n", s2.num);
  25. return 0;
  26. }

而当你使用结构体地址传参且不需要改变其成员变量的值时,可以加上const修饰。


学习自:比特鹏哥C语言课程

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/码创造者/article/detail/1000988
推荐阅读
相关标签
  

闽ICP备14008679号