IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> C语言结构体位段枚举共用体 -> 正文阅读

[数据结构与算法]C语言结构体位段枚举共用体

1. 结构体

1.1匿名结构体

结构体在声明的时候,可以不加上名字,例如:

struct
{
 int a;
 char b;
 float c;
}x;

struct
{
 int a;
 char b;
 float c;
}a[20], *p;

上面这两种都是匿名结构体。
问题来了:下面这种写法正确吗?

p = &x;

答案是不正确的,虽然两个都是匿名结构体,但很明显它们不是同一个结构体。类型自然也不一样了。两个类型不同的变量不能乱赋值。

1.2结构体自引用

结构体里面包含自己就是结构体的自引用。
在数据结构里这种写法非常非常常见。
如链表:

struct ListNode
{
	struct ListNode* next;
	int val;
};

注意:下面这种写法是不正确的。这成套娃了。ListNode里面还有一个ListNode肯定是不合理的。

struct ListNode
{
	struct ListNode next;
	int val;
};

我们都知道在写结构体的时候可以加上typedef来重新命名结构体。
有一种写法初学者经常犯这个错误
下面这种写法是错误的。
原因:程序是自上到下的,ListNode*出现的位置在typedef前面,程序还识别不了这个名字。

typedef struct ListNode
{
	ListNode* next;
	int val;
}ListNode;

1.3结构体大小计算(内存对齐)

结构体大小计算是有自己的一套规则的。并不是线性相加。

这里先说内存对齐的规则再给题目具体解释。

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

  1. 第一个成员在与结构体变量偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
  3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所
    有最大对齐数(含嵌套结构体的对齐数)的整数倍。

先举几道简单的题目来说明这个规则,后面再给几道难一点的。

第一道:

struct S1
	{
		char c1;
		int i;
		char c2;
	};
	printf("%d\n", sizeof(struct S1));

先文字描述再画图解释。
先算出每一个成员的对齐数

  • char的大小是1,vs的默认对齐数是8.因此第一个成员的对齐数是1.

    int的大小是4,vs的默认对齐数是8,因此第二个成员的对齐数是4.

    第三个成员同理,对齐数是1.

再算每一个成员的内存分布情况

  • 第一个成员在偏移量为0的地方存储。

    第二个成员由于对齐数是4,不能在偏移量为1,2,3的地方存储数据,因此这几个地方的空间被浪费了。(1,2,3都不是4的整数倍数)从偏移量为4的地方开始存储,直到偏移量为7的地方

    第三个成员由于对齐数为1,所以可以直接在偏移量为8的地方存储。(8是1的整数倍)

    最后我们可以发现三个成员所占的空间从偏移量0-偏移量8,共9个字节。由于9并不是最大对齐数4的整数倍,所以偏移量为9,10,11三个空间都被浪费了。

    因此总大小为12.

答案果然是12.
在这里插入图片描述
我们现在画图来说明一下上面的文字:
在这里插入图片描述
第二题:把第一题的几个成员的顺序换一下,答案还一样吗?

struct S2
	{
		char c1;
		char c2;
		int i;
	};
	printf("%d\n", sizeof(struct S2));

答案是不一样的。
这次直接上图片解释了。
答案是8个字节。
在这里插入图片描述
这两个题目告诉我们:让占空间小的成员集中在一起可以减少一些空间。

第三题:这次是嵌套结构体的题。

struct S3
	{
		double d;
		char c;
		int i;
	};
struct S4
	{
		double d;
		struct S3 s3;
		double e;
	};

先算S3的结构体大小。S3的大小是16(算法和前面的一模一样)
重点讲S4怎么算?
嵌套的结构体也需要对齐,不过它的对齐数是它自己所有的成员里面对齐数最大的那一个。S3的最大对齐数是8(double),因此S3需要对齐到8的整数倍位置。
在这里插入图片描述
会计算结构体大小后,问题又来了:为什么要存在内存对齐?直接线性的累加大小不就好了吗?

这个没有权威的说法。但大部分资料都阐述了两点原因:

  1. 平台原因:不是所有平台都能访问任意地址的任意数据的。换句话来说,有些地址是不能让你访问的。如果线性地放数据有可能会把数据放到禁区里。(这个点倒没什么所谓个人认为)
  2. 性能问题:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。如果没有对齐,可能会导致拿数据的时候出现错误。

举个例子:假如不存在对齐。就会出现以下的情况。
现在我想从偏移量0处拿出一个int,就会拿到一个属于char的字节和三个属于int的字节。这就导致了错误出现。

在这里插入图片描述
总的来说:结构体的内存对齐是拿空间来换取时间的做法

1.4offsetof

offsetof是一个宏,用来计算偏移量的。
有个问题:offsetof为啥是个宏呢?
看offsetof的参数是有类型名的,函数参数可不能是类型名。

在这里插入图片描述
offsetof的使用:

struct S4
	{
		double d;
		struct S3 s3;
		double e;
	}s4;
	printf("%d\n", offsetof(struct S4, d));
	printf("%d\n", offsetof(struct S4, e));
	printf("%d\n", offsetof(struct S4,s3));

在这里插入图片描述

1.6修改对齐数

这里有个很小的知识。对齐数是可以自己修改的。
用下面这行指令

#pragma pack(你想改的数字)

2.位段(大小计算)

相信很多人都没有听过位段。
位段和结构体是类似的。但有两点不同

  1. 位段的成员必须是整型家族(char也是整型家族)
  2. 位段成员后面接一个冒号和数字。

位段的写法如下:

struct A
	{
		int _a : 2;
		int _b : 5;
		int _c : 10;
		int _d : 30;
	};

C语言里面的位通常都说的是二进制位。这里也不例外。这里的每一个数字代表的是一个变量占几个bit(一个bit就是一个二进制数)。
如a就占两个bit,b就占5个bit,依次类推。

2.1位段的计算

位段的内存开辟和结构体还有一些不一样。

位段的开辟是以一个int或者一个char类型来开辟的,当一个int或者char不足以存放下剩余的成员就再开辟一个int或者char。(开辟什么类型取决于成员是什么类型)

举个例子就很好懂了。
下面这个A的大小是多少?

struct A
	{
		int _a : 2;
		int _b : 5;
		int _c : 10;
		int _d : 30;
	};

答:首先创建一个int类型前2个bit放a变量,放完a之后的5个bit放变量b,然后放c的10个bit.由于现在第一个int只剩下15个bit了。无法放下成员d的30个bit,因此再开辟一个int(这里开辟一个int而不是char的原因是成员d是int类型),这个int的30个bit来存放d。

所以总大小是8个字节。
在这里插入图片描述
为了方便理解,来画图。
在这里插入图片描述
问题来了:
为什么a,b,c,d都是先从右端开始放呢?不可以在左边先放吗?

第二个问题也随之来了。
为什么d不能先用完第一个字节没有用完的bit呢?要直接重新开一个新的字节来存放。

这两个问题确实是存在的。因为C语言标准并没有规定这些规则。这些规则是每一个编译器规定的。至少在msvc上面,位段的存放规则是笔者所说的。

因此位段是不具有可移植性的。

2.2位段的几道题目

题目1:输出多少?

#define MAX_SIZE A+B
struct _Record_Struct
{
  unsigned char Env_Alarm_ID : 4;
  unsigned char Para1 : 2;
  unsigned char state;
  unsigned char avail : 1;
}*Env_Alarm_Record;
struct _Record_Struct *pointer = (struct _Record_Struct*)malloc
(sizeof(struct _Record_Struct) * MAX_SIZE);

这个位段的大小不难计算。画个图就好了。(做这种题切记不要不动手,心算容易算错)
大小是3字节。
在这里插入图片描述
接下来要用位段的大小乘以宏。这个是经典老坑了。宏的使用要多加括号,这里没有加,本质上是在做下面这个操作。

3 × 2 + 3 == 9

答案是9

题目2:

在内存中s是怎么样的?(16进制)

struct S
{
 char a:3;
 char b:4;
 char c:5;
 char d:4;
};

struct S s = {0};
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;

位段由于有大小的限制,原先要存放的数据可能无法被完整的存放,因此要发生截断。
画图可以解决一切这种问题。
在这里插入图片描述
**因此在内存中应该是62 03 04的内容。**我们验证一下。
在这里插入图片描述
题目3:
这道题有点难度。输出什么?

int main()
{
  unsigned char puc[4];
  struct tagPIM
  {
    unsigned char ucPim1;
    unsigned char ucData0 : 1;
    unsigned char ucData1 : 2;
    unsigned char ucData2 : 3;
  }*pstPimData;
  pstPimData = (struct tagPIM*)puc;
  memset(puc,0,4);
  pstPimData->ucPim1 = 2; 
  pstPimData->ucData0 = 3;
  pstPimData->ucData1 = 4;
  pstPimData->ucData2 = 5;
  printf("%02x %02x %02x %02x\n",puc[0], puc[1], puc[2], puc[3]);
  return 0;
}

这个位段的大小是2个字节。(这个很简单,不画图了)
下面这句代码是这道题的核心。
把字符数组的首元素地址强制转换成了结构体tagPIM类型,然后让pstPimData指向了这个字符数组的首元素地址

  pstPimData = (struct tagPIM*)puc;

赋值过程:最后puc[0],puc[1]等于02,29(16进制)
在这里插入图片描述
由于后面的两个元素没有修改过,因此最后的答案就是
02 29 00 00
在这里插入图片描述

3.共用体(联合)

联合也是一种特殊的自定义类型 这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。

如:变量c和变量i是在同一段空间存储的。这段空间的大小是4个字节。

union Un 
{ 
 char c; 
 int i; 
}; 

3.1共用体大小计算

共用体的大小就比之前两个要好计算很多。
共用体的大小至少是最大成员的大小。

但是共用体的大小仍然要进行内存对齐。对齐到对齐数的整数倍。
例如:

union Un 
{ 
 char c; 
 int i; 
}; 

这个联合体的大小就是4.(最大对齐数是4,总大小是最大对齐数的整数倍)

3.2判断机器大小端

有两种方法:
方法1:

	int i = 1;
	char* pc = &(char)i;
	if (*pc == 1)
		printf("小端\n");
	else
		printf("大端\n");

这种方法不要写成

if(*(char*)i)==1;//err

初看好像没啥问题,其实错的很离谱。把i的值强制转换成一个地址,并对这个地址解引用,这就相当于对一个未知的指针解引用,这就肯定会让程序崩溃的。

第二种方法:共用体
其实判断大小端的方法就是取出整型1的第一个字节。共用体正好可以解决这个问题。

	union un
	{
		char i;
		int j;
	}un;
	un.j = 1;
	if (un.i == 1)
		printf("小端\n");
	else
		printf("大端\n");

3.3共用体的一些题目

这些题都比较简单。
题目一:

union Un
	{
		int i;
		char c;
	};
	union Un un;
	// 下面输出的结果是一样的吗?
	printf("%d\n", &(un.i));
	printf("%d\n", &(un.c));
	//下面输出的结果是什么?
	un.i = 0x11223344;
	un.c = 0x55;
	printf("%x\n", un.i);

答案:
前两个的地址是一样的。
11223355

原因:共用体所用的空间是一样的。它的每个成员地址都指向那个空间的开头。

题目二:

union Un1 
{ 
 char c[5]; 
 int i; 
}; 
union Un2 
{ 
 short c[7]; 
 int i; 
}; 
//下面输出的结果是什么?
printf("%d\n", sizeof(union Un1)); 
printf("%d\n", sizeof(union Un2)); 

答案:
8 16

注:数组其实可以看成有n个单独的这个类型的元素,因此它的对齐数仍然是数组元素的类型。算共用体大小的时候记住要对齐。

题目3:

int main()
{
  union
  {
    short k;
    char i[2];
  }*s, a;
  s = &a;
  s->i[0] = 0x39;
  s->i[1] = 0x38;
  printf(%x\n”,a.k);
  return 0;
}

这道题有点坑。
答案
3839
原因:把数字放进共用体的时候确实是3938.但是vs是小端储存,当它拿出来的时候,会默认还原成原来的样子,因此打印出来是3839.实际上里面仍然是3938.

  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2021-09-24 10:50:25  更:2021-09-24 10:53:00 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/17 11:28:11-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码