结构体、位段与联合体
结构体和指针是数据结构的根基,所以这篇博客这算是对结构体有一个重新的认识,主要内容包括:匿名结构体、结构体的自引用、结构体的不完整声明、结构体内存对齐、位段的使用、联合体的应用场景等等。
匿名结构体
匿名结构体简言之就是没有名字的结构体,在结构体的时候就已经定义它的具体结构体对象。以后再也不允许创建新的结构体。这是我遇到的第一个坑,先看看下面这段代码:
1#define _CRT_SECURE_NO_WARNINGS
2#include <stdio.h>
3#include <stdlib.h>
4
5struct
6{
7 int age;
8 int id;
9}s;
10
11struct{
12 int age;
13 int id;
14}*p;
15
16int main(void){
17
18 p = &s;
19 p->age = 20;
20 p->id = 9;
21
22 printf("id = %d ,age = %d\n", s.id, s.age);
23 system("pause");
24 return 0;
25}
接着还是将VS的 #define _CRT_SECURE_NO_WARNINGS 去掉果然还是报出警告: warning C4133: “=”: 从“”到“”的类型不兼容这段代码居然连警告都没有直接跑起来了,结果发现是我冤枉了VS编译器,由于之前使用scanf和strcpy等函数的时候VS老是报警告说使用这些函数是不安全的,于是乎我果断在前面加了一句: #define _CRT_SECURE_NO_WARNINGS ,直接导致了VS没有报出警告信息。刚开始没有发现这个问题,接着在Linux下的gcc下跑了一回,警告!
原因:虽然两个结构体的成员都是一模一样的,但是都是匿名结构体,两个没有结构体标签,编译器认为上边的两个类型不同,所以这个操作时会报警告,但是由于部分编译器对这种情况的检查不严格,所以仍然是可以得出正确的结果,但是我们只需要明白这属于非法操作就行了!。
结构体的自引用
结构体的自引用就是在结构中包含一个类型为该结构体本身的成员
1//错误写法
2typedef struct Node{
3 Node* p;
4 int id;
5}Node;
6
7//错误写法
8struct Node{
9 Node p;
10 int id;
11};
①这种引用是非法的,这里的目的是使用typedef为结构体创建一个别名Node。但是这里是错误的,因为类型名的作用域是从语句的结尾开始,而在结构体内部是不能使用的,因为还没定义。
②这种引用是非法的,因为成员p是另外一个完整的结构,其内部还将包含它自己的成员p.这第二个成员又是另一个完整的结构,它仍将包含自己的成员p,这样重复下去将永无止境。就像一个永远没有出口的递归!
1//正确写法
2typedef struct Node{
3 struct Node* p;
4 int id;
5}Node;
6
7//正确写法
8struct Node{
9 struct Node* p;
10 int id;
11};
这个声明和前面那个声明的区别在于p现在是一个指针而不是结构,编译器在结构的长度确定之前就已经知道了指针的长度,所以其自引用是合法的。
结构体的不完整声明
结构体的不完整声明就是如果两个结构体互相包含,则需要对其中一个结构体进行不完整声明。比如在A结构体成员中包含B结构体指针,在B结构体成员中包含A结构体指针,但是总是得有一个在前面声明,所以就有了不完整声明!
1//结构体不完整声明
2struct B;
3
4struct A
5{
6 //结构体A中包含指向结构体B的指针
7 struct B* pB;
8};
9
10struct B
11{
12 //结构体B中包含指向结构体A的指针
13 struct A* pA;
14};
结构体内存对齐
结构体内存对齐的概念比较重要,也是面试中经常考到的问题! 结构体内存对齐:元素是按照定义顺序一个一个放到内存中去的,但并不是紧密排列的。从结构体存储的首地址开始,每个元素放置到内存中时,它都会认为内存是按照自己的大小来划分的,因此元素放置的位置一定会在自己宽度的整数倍上开始。
结构体内存对齐的好处
- 效率原因:CPU访问某个数据时,要求其存储地址必须是相应数据类型的自然边界。对于存储地址不在其相应类型自然边界的数据,不支持非对齐数据访问的CPU,会导致CPU异常;即使是支持非对齐数据访问的CPU,也会严重影响程序效率,因为需要多次访问才可以拿到完整的的数据!内存对齐这种做法相当于是在 拿空间换时间!
- 移植性原因:不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
结构体内存对齐的规则
1、第一个成员放在与结构体变量偏移量为0的地址处 2、剩下的其他成员对齐到对齐数的整数倍地址处。对齐数就是编译器默认对齐数与该成员大小的较小值,VS的编译器默认值是8,Linux的gcc编译器是4。更改方法:在结构体struct之前加上#pragma pack(对齐数),在struct之后加上#pragma pack;便可以设置两条指令之间的结构体的对齐参数。注意对齐参数不能任意设置,只能是内置类型已有的字节数,如设置为1、2、4… 3、结构体的总大小为最大对齐数的整数倍
4、如果有嵌套了结构体的情况,嵌套的结构体对齐到自身的最大对齐数的整数倍处,结构体的总大小就是所有对齐数中最大对齐数的整数倍
1#define _CRT_SECURE_NO_WARNINGS
2#include<stdio.h>
3#include<stdlib.h>
4#include <stddef.h>
5
6typedef struct S{
7 int a;
8 char b;
9 double c;
10 char d;
11}S;
12
13int main(void){
14
15 printf("%d\n", offsetof(S, a));
16 printf("%d\n", offsetof(S, b));
17 printf("%d\n", offsetof(S, c));
18 printf("%d\n", offsetof(S, d));
19
20 printf("sizeof(struct S) = %d\n",sizeof(S));
21
22 system("pause");
23 return 0;
24}
我们是如何得知结构体某个成员相对于结构体起始位置的偏移量呢?需要一个宏:offsetof,这个宏的设置比较巧妙,首先将0地址强制转换为type类型的指针,然后就可以定位到member在结构体中偏移位置,编译器把0当做有效地址,认为0是type指针的起始地址,这样就立刻得出了偏移量!
1#define offsetof(type,menber) (size_t)&(((type*)0)->member)
第一个成员放在与结构体变量偏移量为0的地址处,现在可用偏移为4偏移,接下来存char b; 由于4是1的倍数,故而,b占用4偏移,接下来可用偏移为5偏移,接下来该存double c; 由于5不是8的倍数,所以向后偏移5,6,7,都不是8的倍数,偏移到8时,8是8的倍数,故而c从8处开始存储,占用8,9,10,11,12,13,14,15偏移,现在可用偏移为16偏移,最后该存char d ;因为16是1的倍数,故d占用16偏移,接下来在整体向后偏移一位,现处于17偏移,min(默认对齐参数,类型最大字节数)=8;因为17不是8的倍数,所以继续向后偏移18…23都不是8的倍数,到24偏移处时,28为8的整数倍,故而,该结构体大小为24个字节。接下来我们再看这样一个结构体:
1#define _CRT_SECURE_NO_WARNINGS
2#include<stdio.h>
3#include<stdlib.h>
4#include <stddef.h>
5
6typedef struct S{
7 double a;
8 int b;
9 char c;
10 char d;
11}S;
12
13int main(void){
14
15 printf("%d\n", offsetof(S, a));
16 printf("%d\n", offsetof(S, b));
17 printf("%d\n", offsetof(S, c));
18 printf("%d\n", offsetof(S, d));
19
20 printf("sizeof(struct S) = %d\n",sizeof(S));
21
22 system("pause");
23 return 0;
24}
这说明我们在设计结构体的时候应该尽量让小的成员贴在一起,避免不必要的空间浪费!
结构体的应用场景
1、一般当内置内存无法满足用户需要,没有合适类型对应对象时,需要封装特定的类型 2、当函数有多个参数时,返回值过多,需要封装特定类型,将参数打包返回。
位段
C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域”( bit field) 。利用位段能够用较少的位数存储数据。一个位段必须存储在同一存储单元中,不能跨两个单元。如果第一个单元空间不能容纳下一个位段,则该空间不用,而从下一个单元起存放该位段。(见下例) 1.位段声明和结构体类似 2.位段的成员必须是int、unsigned int、signed int 3.位段的成员名后边有一个冒号和一个数字
1struct A
2{
3 int a : 2;
4 //先开辟4个字节的空间,也就是32个比特位
5 //a占掉2个比特位,32-2=30
6 int b : 5;
7 //b占掉5个比特位,30-5=25
8 int c : 10;
9 //c占掉10个比特位,25-10=15
10 int d : 30;
11 //d占30个比特位,前边开辟的4个字节已经不够用了,因此在开辟四个字节
12};
位段无跨平台性
1.int位段被当成是有符号还是无符号是不确定的 2.位段中最大位的数目不能确定 3.位段中的成员在内存中是从右向左还是从左向右分配的不确定 4.当一个结构包含两个位段,第二个位段成员比较大,放不下在第一个位段剩余的为时,舍弃还是利用第二个位段成员是不确定的
位段应用场景
由于位段比较节省内存,通常用于网络数据包的封装信息,在网络此次发达的时代,为了减缓网络拥堵提交网络访问速率,在封装数据包头部信息的时候通常是采用位段的方式来存储数据,减少网络流量!
联合体
在进行某些算法的C语言编程的时候,需要使几种不同类型的变量存放到同一段内存单元中。也就是使用覆盖技术,几个变量互相覆盖。这种几个不同的变量共同占用一段内存的结构,在C语言中,被称作“共用体”类型结构,简称共用体,也叫联合体。
联合体大小的计算
1.联合的大小至少是最大成员的大小 2.当最大成员大小不是最大对齐数的整数倍时,就要对齐要最大对齐数的整数倍
1union un1{
2 int i;
3 char arr[6];
4};
5
6union un2{
7 int i;
8 short arr[5];
9};
10
11int main(void){
12 printf("sizeof(un1) = %d\n",sizeof(union un1)); //8
13 printf("sizeof(un1) = %d\n", sizeof(union un2));//12
14 system("pause");
15 return 0;
16}
联合体判断大小端
1//返回1则是小端存储、返回0则是大端存储
2int decide()
3{
4 union un{
5 int i;
6 char c;
7 };
8 union un u1;
9 u1.i = 1;
10 return (int)u1.c;
11}
巧用联合体与结构体
1#define _CRT_SECURE_NO_WARNINGS
2#include<stdio.h>
3#include<stdlib.h>
4#include<string.h>
5#include<assert.h>
6
7typedef struct S2{
8 unsigned char a;
9 unsigned char b;
10 unsigned char c;
11 unsigned char d;
12}S2;
13typedef union S{
14 long num;
15 S2 s1;
16}S;
17
18int main(void){
19 S s;
20 s.num = 2378912378;
21
22 printf("%d.%d.%d.%d\n", s.s1.a, s.s1.b, s.s1.c, s.s1.d );
23 system("pause");
24 return 0;
25}
利用联合体这种巧妙地存储结构就可以轻松的将数据拆分出来,这绝对是一个非常有用的技巧!
本次对结构体、联合体、位段进行了比较综合的复习,温故而知新,啊哈哈!