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提供了两种类型的聚合数据类型:数组、结构。
数组是相同类型的元素的集合,而结构可以是不相同的数据类型的值的集合。这些值称为结构的成员

结构体类型的声明

在声明结构的时候,必须列出它所有成员的类型和名字。

struct tag
{
   member-list;
}variable-list;

tag:是构造这个结构类型时,所取的类型名

member-list:结构成员

variable-list:定义类型为struct tag的变量

例如描述一个学生:

//描述一个学生
struct Stu
{
	char name[15]; //名字
	char sex[3]; //性别
	int age; //年龄
	char ID[20]; //学号
};

注意结构体声明也是声明,最后的分号一定不要忘记!!!这个类型的变量可以在分号后面创建,也可以在其他合法的地方创建,毕竟现在它只是创建了一个类型。

特殊的结构声明

//匿名结构体类型
struct
{
 int a;
 char b;
 float c; }x;
struct
{
 int a;
 char b;
 float c; }a[20], *p;

这两个声明省略了结构体的标签(tag)
我们来思考一下这个步骤是否正确:
*p = &x;
答案是:这条语句是非法的。
由于上面的成员列表完全相同,以至于我们看上去认为那两个声明是一样的,但是编译器却把它们当作两个截然不同的类型。类型不同就不能进行这样的操作。

结构体类型的自引用

在一个结构体的内部包含一个类型为该结构本身的成员是否合法?
例如:

//代码一
struct Node
{
 int a;
 struct Node b;
 char c;
};
//该声明是否合法

这种类型的自引用是非法的。,因为成员 b 也是一个完整的结构,该结构里又有一个结构……,这样一直重复下去,有点递归的影子,但是却没有终止条件。
正确的自引用如下:

//代码二
struct Node
{
  int a;
  struct Node *b;
  char c;
}

代码一和代码二的区别在于,代码一的b是结构,代码二中的 b 是一个指针。这个指针所指向的是同一类型的不同结构。编译器在还没有确定结构的大小的之前,就已经知道指针的大小了。
我们需要警惕下面的陷阱

//代码3
typedef struct
{
   int a;
   Node* b;
   char c; 
 }Node;
//这样写代码,是否合法?

代码3是非法的,它的目的是为这个结构创建类型名Node,但是没有达到目标,因为在声明的内部还没有进行定义,而在声明的快结尾处才进行了创建。

//解决方案:
typedef struct Node
{
   int a;
   struct Node* b;
   char c;
}Node;

结构体变量的定义及初始化

我们已经大致知道了结构体变量的声明,那么如何对变量进行定义和初始化呢?

//结构体定义
struct Simple
{
	char x[10];
	int y;
}p1; //直接在声明处定义

struct Simple p2; //定义结构体变量p2

struct Simple p3 = { "abcdef", 123 }; //定义变量p3的时候同时赋值


struct Simple2
{
	char x[10];
	int y;
}p4 = {"abcd", 123}; //直接在声明处定义并初始化


//嵌套定义及初始化
struct Simple3
{
	char x[10];
	int y;
	struct Simple3* z;
}p5 = { "abc", 123,NULL};

结构体的内存对齐

我们知道一个整型类型占4字节,一个字符型类型占1字节。那么一个结构类型占多少内存空间大小呢?

struct S1
{
	char c1;
	int i;
	char c2;
};

你是否这样认为:一个char 占1个字节, 一个int占4字节,那么这个结构的所占内存大小为6字节。
我们不妨编写代码,让计算机来计算。
在这里插入图片描述
结果是12,因此刚才的想法是错误的!
这里就不得不提到一个非常重要的知识了:结构体内存对齐
结构体内存对齐的规则

    1. 第一个成员在与结构体变量偏移量为0的地址处。
    1. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
      对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
      (vs中默认的值为8、 Linux中的默认值为4)
    1. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
    1. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所
      有最大对齐数(含嵌套结构体的对齐数)的整数倍。

举例如下(图片可点击放大):
在这里插入图片描述在这里插入图片描述
在这里插入图片描述

这个时候我们就会非常的纳闷了,为什么结构体会存在内存对齐?老老实实的一个个分配内存它不香吗。
为什么会存在内存对齐?
大部分的资料是从两个角度来看的
一、平台可移植性:
不是所有的硬件平台都能访问任意地址上的数据;某些硬件平台只能在某些地址处取得某些特定类型的数据,否则会出现硬件异常。

二、性能
数据结构应该尽可能地在自然边界对齐。对齐的内存访问时只需一次访问,而未对齐的内存则需要进行两次访问。

总体来说:内存对齐是一种以空间换取时间的做法

在这里插入图片描述
在这里插入图片描述
上面两个结构体的成员是一样的,只有顺序不同,然而结构体所占空间大小也不一样了。
我们来看看它是如何分配内存的:
在这里插入图片描述

在试验了N多次的类似情况下得出结论:
最初设计结构体的时候,我们应该尽量让占用内存空间小一点的值集中在一起,以此来满足即节省空间又能节省时间的想法。

在VS编译器下,结构体对齐数默认是8。
但是在C中我们可以还能手动设置对齐数

//设置对齐数
# pragma pack(num)
//num是对齐数的值

例如:

//设置对齐数为4
# pragma pack(4)
struct S1
{
	char c1;
	int i;
	char c2;
};
# pragma pack() //还原为默认对齐数

学会了如何去使用# pragma (num)来更改默认对齐数。当我们遇到对齐数不合适时,就可以自行更改了。
上面一系列的代码中,大部分我们都是使用sizeof( )这个函数来得出结构体所占空间大小(包括了因边界对齐而浪费掉的字节)。如果你必须确认结构体中某个成员的实际位置,这个时候就可以使用offsetof了,这可不是一个函数,它是一个定义于stddef.h文件的。你也可以换一种理解,该宏可以计算出结构体某成员在内存中的偏移量(在结构体第一个成员开始存储的位置处偏移了几个字节)

//使用刚才设置对齐数的结构体举个例子
offsetof(struct S1, i)

这个语句的返回值为4。即 i 偏移了4字节

结构体传参和访问

如果我们已经知道了一个结构体类型,我们怎么访问它的每一个成员呢?
方法一:使用.来访问
方法二:使用->来访问

使用方法一来访问
在这里插入图片描述
现在我们将主函数内的功能封装成一个打印函数Print1
传结构体的情况:
在这里插入图片描述
使用->访问和传变量地址的情况,此时p->name等价于(*p).name
在这里插入图片描述
在这两种方法给出的效果一样的情况下,我们首选哪一个呢?

函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,性能会比较低

所以将结构体变量传参的时候尽量传地址。

结构体实现位段

什么是位段?
可能很多人就说是王者里的青铜、黄金、铂金、钻石……这些。
计算机里的位段:位段中的位指的是二进制位,段指的是范围。

位段的声明与结构类似,但是它们有两个不同点
一、位段成员必须是整型家族。
二、位段的成员标志是,成员后面有一个冒号,冒号后面接着一个数字。

//例子
struct S
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};

位段和我们之前的结构体内存分配一样吗?
如果我们按照之前的方法来计算应该是16字节,不妨编写代码让计算机算一算
在这里插入图片描述
发现和我们的预期结果是不一样的,也就是说位段的内存分配与结构体的内存分配不一样。
位段在内存中是以4字节或者1字节逐个开辟的。
在这里插入图片描述
在这里插入图片描述
根据上面的内存分配来看,一共占用了24个比特位,也就是3字节,因此结构体S的内存大小为3。

我们应该避免使用位段,位段是不跨平台的。
为什么它是不跨平台的?有四个不确定

  • 不确定成员是有符号还是无符号整型
  • 不确定最大位的数目
  • 不确定成员在内存从低到高还是从高到低分配
  • 不确定如果第一个成员位段有剩余且第二个成员所占空间比较大时,是舍弃剩余部分,还是继续用。

以上就是本文所要讲解的内容。

  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2021-10-07 14:04:31  更:2021-10-07 14:07:03 
 
开发: 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 10:03:42-

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