结构体、位段与联合体

结构体和指针是数据结构的根基,所以这篇博客这算是对结构体有一个重新的认识,主要内容包括:匿名结构体、结构体的自引用、结构体的不完整声明、结构体内存对齐、位段的使用、联合体的应用场景等等。

匿名结构体

匿名结构体简言之就是没有名字的结构体,在结构体的时候就已经定义它的具体结构体对象。以后再也不允许创建新的结构体。这是我遇到的第一个坑,先看看下面这段代码:

 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}

利用联合体这种巧妙地存储结构就可以轻松的将数据拆分出来,这绝对是一个非常有用的技巧!

本次对结构体、联合体、位段进行了比较综合的复习,温故而知新,啊哈哈!