当前位置:   article > 正文

自定义类型的解析——结构体详解_匿名结构体

匿名结构体
  1. 匿名结构体类型

结构体的完全声明

  1. struct tag
  2. {
  3. member_list;
  4. }variable_list;
  5. //结构体的标签:tag
  6. //结构体的类型:struct tag
  7. //结构的成员列表:member_list
  8. //结构体变量列表:variable_list

当不完全声明时,比如说省略结构体的标签,这时,这种结构体就被称为匿名结构体

匿名结构体也称为未命名结构体,由于没有名称,因此不会创建它们的直接对象(或变量),通常我们在嵌套结构或联合中使用它们。

值得注意的是,匿名结构体类型可以说是“一次性用品”,用了一次以后再也用不了了。

  1. struct
  2. {
  3. char c;
  4. int a;
  5. double d;
  6. }s1;
  7. struct
  8. {
  9. char c;
  10. int a;
  11. double d;
  12. }* p1;//一个看上去指向和s1相同类型的指针
  13. int main()
  14. {
  15. p1 = &s1;//这样写是不可以的,编译器会认为它是两个不同的类型
  16. return 0;
  17. }

如上代码是错误的,虽然两个匿名结构体,看上去它们的成员都是一样的,表面上是同一类型,但实际上他们是两种不同的类型。因此,因为在定义的时候没有写结构体类型名称,编译器会把它们当做两种不同的类型,然后报错。

匿名结构体的优点:

嵌套在结构体中的结构体为匿名结构时,可以直接访问其成员。

  1. #include <stdio.h>
  2. struct Stu
  3. {
  4. char* name;
  5. char gender;
  6. int age;
  7. struct//嵌套定义了这个匿名结构体
  8. {
  9. int Student_ID;
  10. long phone_number;
  11. };
  12. };
  13. int main(void)
  14. {
  15. struct Stu A= {"A", 'M', 19, {30, 13930422035}};
  16. printf("%d\n", A.Student_ID);
  17. }

如果不使用匿名结构体,则上述例子会变成这样:

  1. #include <stdio.h>
  2. struct Stu_information
  3. {
  4. int Student_ID;
  5. long phone_number;
  6. };
  7. struct Stu A
  8. {
  9. char* name;
  10. char gender;
  11. int age;
  12. struct Stu_information;
  13. };
  14. int main(void)
  15. {
  16. struct Stu A = {"A", 'M', 19, {30, 13930422035}};
  17. printf("%d\n", A.Stu_information.Student_ID);
  18. }

对比上述两个例子可以看出:

使用匿名结构体,结构体对象 A 可以通过 A.Student_ID直接访问匿名结构体成员变量 .Student_ID,代码比较简洁,反之则必须通过A.Stu_information.Student_ID 来访问结构体成员变量 ,比较繁琐。

  1. 结构体的自引用

我们不由地思考一个问题,结构体把自己当作自己的一个成员变量,这是否可行呢?

  1. struct Node
  2. {
  3. int date;
  4. struct Node n;//错误!
  5. };

结论是:这样写是绝对错误的!

请想一想,如果我们想要求struct Node 的大小,其中,成员data占4个字节,那么struct Node n成员的大小是多少呢?

这就陷入了一个思维上的死循环:要求它自己的大小,那么首先就要求它自己的大小?这是什么鬼啊?!

所以这样写是绝对不可以的!!!

那么,如果想要正确的自引用,我们应该怎么做呢?

这里要先补充一点关于“数据结构”的知识

数据的存储可以有很多方式,比如“顺序存储”、“二叉树”等等,这些存储方式都可以让我们很顺利地从一个数据找到另一个数据。

所以,关于结构体的自引用,我们可以把它看作是通过一个结构体,可以找到下一个结构体。这样一个一个像线一样访问数据,即“线性数据结构(通过链表方式可以顺序访问内存空间中不相邻的数据)”,我们就要使用到结构体指针。而指针的大小跟其所指向的类型无关,仅跟编译器有关,32位平台上指针大小为4个字节,在64位平台上,指针大小为8个字节。所以,通过指针大小的确定性,我们就可以确定结构体类型的大小了。

  1. struct Node //结构体的自引用
  2. {
  3. int date;
  4. struct Node* next;
  5. };
  6. int main()
  7. {
  8. struct Node n1;
  9. struct Node n2;
  10. n1.next = &n2;//这样就可以通过n1找到n2了.
  11. return 0;
  12. }

相当于每个结构体,前面存数据,后面下一个结点地址的指针,通过该结点可以找到下一个结构体。

匿名结构体的重命名+自引用:

  1. typedef struct
  2. {
  3. int data;
  4. int d;
  5. }Node; //一种错误的写法
  6. typedef struct Node
  7. {
  8. int data;
  9. int d;
  10. }Node; //正确
  11. struct
  12. {
  13. int data;
  14. struct Node* next;
  15. }Node1;
  16. //结构体在未定义Node之前就定义了一个Node*的指针,这样是错误的!!!
  17. typedef struct
  18. {
  19. int date;
  20. struct Node* next;//在此之前,都没有出现Node
  21. }Node;
  22. //把一个匿名结构体重命名为Node?不可以!
  23. //正确的写法是这样的:
  24. typedef struct Node
  25. {
  26. int data;
  27. struct Node* next;
  28. }Node;
  1. 结构体的初始化:

  1. struct S
  2. {
  3. int a;
  4. char c;
  5. };
  6. struct F
  7. {
  8. int a;
  9. char arr[10];
  10. struct S;
  11. };
  12. int main()
  13. {
  14. struct F pd= { 12,"sgdkjklaj",{345,'o'}};//结构体的嵌套初始化
  15. struct S s2 = { 2000,'p'};//默认顺序初始化
  16. struct S s3 = { .c = 'r',.a = 2000 };//自己确定初始化的顺序
  17. }
  1. 结构体的内存对齐原则:

1.第一个成员永远在结构体变量偏移量为0的地址处。

2.从第二个成员开始,以后每个成员变量都要对齐到某个数字(对齐数)的整数倍的地址处。

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

  • vs2017环境中,VS编译器默认的值为8

  • 一个是Linux环境,gcc编译器下,没有默认对齐数,对齐数就是成员自身的大小

3、当成员全部存放进去后,结构体总大小必须是所有成员的对齐数中最大对齐数(每个成员变量都有一个对齐数)的整数倍。如果不够,则浪费空间。

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

接下来,让我们举一些例子:

为什么会存在内存对齐呢?

1.平台原因(移植原因)∶不是所有的硬件平台都能访问任意地址上的任意数据的; 某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。(所以我们对齐到能够被硬件访问的位置,在对齐位置进行存储数据)

⒉性能原因∶数据结构(尤其是栈)应该尽可能地在自然边界上对齐。

原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;

而对齐的内存访问仅需要一次访问。它能够提高效率。

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

64位平台上,编译器一次访问8个字节。

但是,节省空间不能无脑地节省空间啊。

如果既要满足对齐,又要节省空间,怎么办呢?

解决方案:把占用空间较小的数据集中在一起。

  1. struct s1
  2. {
  3. char c1;
  4. int i;
  5. char c2;
  6. };//12个字节
  7. struct s2
  8. {
  9. char c1;
  10. char c2;
  11. int i;
  12. };//8个字节

设置默认对齐数:

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

如何修改默认对齐数呢?通过#pragma修改。

  1. #include <stdio.h>
  2. #pragma pack(8)//设置默认对齐数为8
  3. struct s1
  4. {
  5. char c1;
  6. int i;
  7. char c2;
  8. };
  9. #pragma pack()//取消设置的默认对齐数,还原为默认
  10. #pragma pack(1)//设置默认对齐数为1,这样其实相当于根本没有对齐数了。
  11. struct s2
  12. {
  13. char c1;
  14. int i;
  15. char c2;
  16. };
  17. #pragma pack()//取消设置的默认对齐数,还原为默认
  18. int main()
  19. {
  20. printf("%d\n", sizeof(struct s1));
  21. printf("%d\n", sizeof(struct s2));
  22. return 0;
  23. }

offsetof,这是一个宏,用来计算结构体成员相对于结构体起始位置的偏移量

  1. #include<stddef.h>
  2. struct S
  3. {
  4. char c1;
  5. char c2;
  6. int i;
  7. };
  8. int main()
  9. {
  10. struct S s = {0};
  11. printf("%d\n",offsetof(struct S,c1)); //0
  12. printf("%d\n", offsetof(struct S, c2));//1
  13. printf("%d\n", offsetof(struct S, i)); //4
  14. return 0;
  15. }

  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. }

上面的print1和print2哪一个更好呢?

print2更好一点

对于结构体的传参首选传递地址,原因如下:

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

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

那么如何应对传地址的不安全性呢?很简单,用一个const 就可以了。

  1. 位段

什么是位段 ?

位段的声明和结构是类似的,但是有两点不同︰

1.位段的成员必须是int、unsigned int,signed int、char这些整型家族的类型。

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

  1. #include<stdio.h>
  2. struct A
  3. {
  4. int _a : 2; //int _a;
  5. int _b : 5; //int _b;
  6. int _c : 10; //int _c;
  7. int _d : 30; //int _d;这是结构体
  8. };
  9. int main()
  10. {
  11. struct A sa={0};
  12. printf("%d\n", sizeof(sa));
  13. return 0;
  14. }

位段位段,“位”指的是二进制位

如果_a只会存储0,1,2,3(00,01,10,11),那么这个时候2个比特位就够了。所以位段是一种更节省空间的方法。

  1. #include<stdio.h>
  2. struct A
  3. {
  4. int _a : 2;
  5. int _b : 5;
  6. int _c : 10;
  7. int _d : 30;
  8. };//47个比特位
  9. //实际上占了8个字节
  10. //当然,这已经很节省空间了!

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

位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。(所以一些细节方面不能够确定下来)

现在还有一些问题待确定:

1、空间是从左向右使用还是从右向左使用?

2、当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用呢?

  1. #include<stdio.h>
  2. struct S
  3. {
  4. char a : 4;
  5. char b : 3;
  6. char c : 5;
  7. char d : 4;
  8. }s;
  9. int main()
  10. {
  11. struct S s = { 0 };
  12. s.a = 10;
  13. s.b = 12;
  14. s.c = 3;
  15. s.d = 4;
  16. return 0;
  17. }

但是,这些在不同编译器上都是不同的

位段的跨平台问题:

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

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

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

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

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

位段的应用:

关于位段的一些练习题:

  1. #define MAX_SIZE A+B
  2. struct _Record_Struct
  3. {
  4. unsigned char Env_Alarm_ID : 4;
  5. unsigned char Para1 : 2;
  6. unsigned char state;
  7. unsigned char avail : 1;
  8. }*Env_Alarm_Record;
  9. struct _Record_Struct *pointer =
  10. (struct _Record_Struct*)malloc(sizeof(struct _Record_Struct) * MAX_SIZE);

问:当A=2, B=3时,pointer分配( )个字节的空间?

位段成员作为unsighed char类型,是按1个字节开辟内存的,第一个成员Env_Alarm_ID开辟一个字节等于8个bit位,被Env_Alarm_ID用去4个bit,还剩4个bit可以给Para1用。Para1用去2个bit,故位段Env_Alarm_ID , Para1一共用一个字节;

state不是位段重新开辟一个字节的内存,它直接占一个字节;

位段avail只占1个bit,但也要开辟1个字节的空间用来存放。

所以前面的算下来总共开辟了3个字节的空间。

(sizeof(struct _Record_Struct) * MAX_SIZE)=3*2+3(这里一定要注意宏定义并没有写成(A+B)的形式,所以并不是先计算A+B)=9.

故pointer分配的空间为9个字节

  1. int main()
  2. {
  3. unsigned char puc[4];
  4. struct tagPIM
  5. {
  6. unsigned char ucPim1;
  7. unsigned char ucData0 : 1;
  8. unsigned char ucData1 : 2;
  9. unsigned char ucData2 : 3;
  10. }*pstPimData;
  11. pstPimData = (struct tagPIM*)puc;
  12. memset(puc,0,4);
  13. pstPimData->ucPim1 = 2;
  14. pstPimData->ucData0 = 3;
  15. pstPimData->ucData1 = 4;
  16. pstPimData->ucData2 = 5;
  17. printf("%02x %02x %02x %02x\n",puc[0], puc[1], puc[2], puc[3]);
  18. return 0;
  19. }

代码结果是?

画个图:

最后,想要补充一个非常易错的练习题:

  1. #include<stdio.h>
  2. struct ord
  3. {
  4. int x,y
  5. }dt[2] = {1,2,3,4};
  6. int main()
  7. {
  8. struct ord* p = dt;
  9. printf("%d,", ++p->x);
  10. printf("%d\n", ++p->y);
  11. return 0;
  12. }

这个程序的运行结果是什么?

是不是有点意想不到?

让我们来简单分析一下:

结构体其实也是可以这样赋值的,和数组是很类似的。

printf("%d,", ++p->x);

printf("%d\n", ++p->y);

这两条语句中,->这个操作符的优先级是高于++的,所以其实是先取出p指向的x和y这两个数值,然后再对这两个数++。

所以实际上,p这个指针自始至终都指向dt[1]。

本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号