结构体、位段与联合体

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

匿名结构体

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>

struct
{
int age;
int id;
}s;

struct{
int age;
int id;
}*p;

int main(void){

p = &s;
p->age = 20;
p->id = 9;

printf("id = % d ,age = % d\n", s.id, s.age);
system ("pause");
return 0;
}

接着还是将 VS 的 #define _CRT_SECURE_NO_WARNINGS 去掉果然还是报出警告: warning C4133: “=”: 从 “” 到 “” 的类型不兼容这段代码居然连警告都没有直接跑起来了,结果发现是我冤枉了 VS 编译器,由于之前使用 scanf 和 strcpy 等函数的时候 VS 老是报警告说使用这些函数是不安全的,于是乎我果断在前面加了一句: #define _CRT_SECURE_NO_WARNINGS , 直接导致了 VS 没有报出警告信息。刚开始没有发现这个问题,接着在 Linux 下的 gcc 下跑了一回,警告!

原因:虽然两个结构体的成员都是一模一样的,但是都是匿名结构体,两个没有结构体标签,编译器认为上边的两个类型不同,所以这个操作时会报警告,但是由于部分编译器对这种情况的检查不严格,所以仍然是可以得出正确的结果,但是我们只需要明白这属于非法操作就行了!。

结构体的自引用

结构体的自引用就是在结构中包含一个类型为该结构体本身的成员

1
2
3
4
5
6
7
8
9
10
11
// 错误写法 
typedef struct Node{
Node* p;
int id;
}Node;

// 错误写法
struct Node{
Node p;
int id;
};

①这种引用是非法的,这里的目的是使用 typedef 为结构体创建一个别名 Node。但是这里是错误的,因为类型名的作用域是从语句的结尾开始,而在结构体内部是不能使用的,因为还没定义。

②这种引用是非法的,因为成员 p 是另外一个完整的结构,其内部还将包含它自己的成员 p. 这第二个成员又是另一个完整的结构,它仍将包含自己的成员 p,这样重复下去将永无止境。就像一个永远没有出口的递归!

1
2
3
4
5
6
7
8
9
10
11
// 正确写法 
typedef struct Node{
struct Node* p;
int id;
}Node;

// 正确写法
struct Node{
struct Node* p;
int id;
};

这个声明和前面那个声明的区别在于 p 现在是一个指针而不是结构,编译器在结构的长度确定之前就已经知道了指针的长度,所以其自引用是合法的。

结构体的不完整声明

结构体的不完整声明就是如果两个结构体互相包含,则需要对其中一个结构体进行不完整声明。比如在 A 结构体成员中包含 B 结构体指针,在 B 结构体成员中包含 A 结构体指针,但是总是得有一个在前面声明,所以就有了不完整声明!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 结构体不完整声明 
struct B;

struct A
{
// 结构体 A 中包含指向结构体 B 的指针
struct B* pB;
};

struct B
{
// 结构体 B 中包含指向结构体 A 的指针
struct A* pA;
};

结构体内存对齐

结构体内存对齐的概念比较重要,也是面试中经常考到的问题!
结构体内存对齐:元素是按照定义顺序一个一个放到内存中去的,但并不是紧密排列的。从结构体存储的首地址开始,每个元素放置到内存中时,它都会认为内存是按照自己的大小来划分的,因此元素放置的位置一定会在自己宽度的整数倍上开始。

结构体内存对齐的好处

  • 效率原因:CPU 访问某个数据时,要求其存储地址必须是相应数据类型的自然边界。对于存储地址不在其相应类型自然边界的数据,不支持非对齐数据访问的 CPU,会导致 CPU 异常;即使是支持非对齐数据访问的 CPU,也会严重影响程序效率,因为需要多次访问才可以拿到完整的的数据!内存对齐这种做法相当于是在 拿空间换时间!
  • 移植性原因:不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

结构体内存对齐的规则

1、第一个成员放在与结构体变量偏移量为 0 的地址处
2、剩下的其他成员对齐到对齐数的整数倍地址处。对齐数就是编译器默认对齐数与该成员大小的较小值,VS 的编译器默认值是 8,Linux 的 gcc 编译器是 4。更改方法:在结构体 struct 之前加上 #pragma pack (对齐数),在 struct 之后加上 #pragma pack; 便可以设置两条指令之间的结构体的对齐参数。注意对齐参数不能任意设置,只能是内置类型已有的字节数,如设置为 1、2、4…
3、结构体的总大小为最大对齐数的整数倍

4、如果有嵌套了结构体的情况,嵌套的结构体对齐到自身的最大对齐数的整数倍处,结构体的总大小就是所有对齐数中最大对齐数的整数倍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include <stddef.h>

typedef struct S{
int a;
char b;
double c;
char d;
}S;

int main(void){

printf("% d\n", offsetof (S, a));
printf("% d\n", offsetof (S, b));
printf("% d\n", offsetof (S, c));
printf("% d\n", offsetof (S, d));

printf("sizeof (struct S) = % d\n",sizeof(S));

system ("pause");
return 0;
}

我们是如何得知结构体某个成员相对于结构体起始位置的偏移量呢?需要一个宏: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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include <stddef.h>

typedef struct S{
double a;
int b;
char c;
char d;
}S;

int main(void){

printf("% d\n", offsetof (S, a));
printf("% d\n", offsetof (S, b));
printf("% d\n", offsetof (S, c));
printf("% d\n", offsetof (S, d));

printf("sizeof (struct S) = % d\n",sizeof(S));

system ("pause");
return 0;
}

这说明我们在设计结构体的时候应该尽量让小的成员贴在一起,避免不必要的空间浪费!

结构体的应用场景

1、一般当内置内存无法满足用户需要,没有合适类型对应对象时,需要封装特定的类型
2、当函数有多个参数时,返回值过多,需要封装特定类型,将参数打包返回。

位段

C 语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为 “位段” 或称 “位域”( bit field) 。利用位段能够用较少的位数存储数据。一个位段必须存储在同一存储单元中,不能跨两个单元。如果第一个单元空间不能容纳下一个位段,则该空间不用,而从下一个单元起存放该位段。(见下例)

  1. 位段声明和结构体类似
  2. 位段的成员必须是 int、unsigned int、signed int
  3. 位段的成员名后边有一个冒号和一个数字
1
2
3
4
5
6
7
8
9
10
11
12
struct A
{
int a : 2;
// 先开辟 4 个字节的空间,也就是 32 个比特位
//a 占掉 2 个比特位,32-2=30
int b : 5;
//b 占掉 5 个比特位,30-5=25
int c : 10;
//c 占掉 10 个比特位,25-10=15
int d : 30;
//d 占 30 个比特位,前边开辟的 4 个字节已经不够用了,因此在开辟四个字节
};

位段无跨平台性

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

  1. 位段中最大位的数目不能确定
  2. 位段中的成员在内存中是从右向左还是从左向右分配的不确定
  3. 当一个结构包含两个位段,第二个位段成员比较大,放不下在第一个位段剩余的为时,舍弃还是利用第二个位段成员是不确定的

位段应用场景

由于位段比较节省内存,通常用于网络数据包的封装信息,在网络此次发达的时代,为了减缓网络拥堵提交网络访问速率,在封装数据包头部信息的时候通常是采用位段的方式来存储数据,减少网络流量!

联合体

在进行某些算法的 C 语言编程的时候,需要使几种不同类型的变量存放到同一段内存单元中。也就是使用覆盖技术,几个变量互相覆盖。这种几个不同的变量共同占用一段内存的结构,在 C 语言中,被称作 “共用体” 类型结构,简称共用体,也叫联合体。

联合体大小的计算

  1. 联合的大小至少是最大成员的大小
  2. 当最大成员大小不是最大对齐数的整数倍时,就要对齐要最大对齐数的整数倍
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
union un1{
int i;
char arr [6];
};

union un2{
int i;
short arr [5];
};

int main(void){
printf("sizeof (un1) = % d\n",sizeof(union un1)); //8
printf("sizeof (un1) = % d\n", sizeof(union un2));//12
system ("pause");
return 0;
}

联合体判断大小端

1
2
3
4
5
6
7
8
9
10
11
// 返回 1 则是小端存储、返回 0 则是大端存储 
int decide()
{
union un{
int i;
char c;
};
union un u1;
u1.i = 1;
return (int) u1.c;
}

巧用联合体与结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>

typedef struct S2{
unsigned char a;
unsigned char b;
unsigned char c;
unsigned char d;
}S2;
typedef union S{
long num;
S2 s1;
}S;

int main(void){
S s;
s.num = 2378912378;

printf("% d.% d.% d.% d\n", s.s1.a, s.s1.b, s.s1.c, s.s1.d );
system ("pause");
return 0;
}

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

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