0%

结构体、位段与联合体

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

匿名结构体

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

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位段被当成是有符号还是无符号是不确定的
2.位段中最大位的数目不能确定
3.位段中的成员在内存中是从右向左还是从左向右分配的不确定
4.当一个结构包含两个位段,第二个位段成员比较大,放不下在第一个位段剩余的为时,舍弃还是利用第二个位段成员是不确定的

位段应用场景

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

联合体

在进行某些算法的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;
}

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

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

  • 本文作者: Tim
  • 本文链接: https://zouchanglin.cn/2714529611.html
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!