【C语言自定义类型详解进阶】结构体(补充结构体的对齐和位段,一口气看完系列,央妈都点赞的博文)

目录

1.结构体

1.1 结构的基础知识

1.2 结构的声明

1.2.1特殊的声明(匿名结构体类型)

1.3结构体变量的定义

1.4关于匿名结构体类型的补充

1.5结构体的自引用

1.6结构体变量的初始化

2.结构体内存对齐(重点)

2.1偏移量补充

2.2对齐规则 

2.3为什么会有对齐

2.4修改默认对齐数 

3.结构体传参 

4.位段

4.1什么是位段

4.2位段的内存分配 

4.3 位段的跨平台问题 

4.4位段的应用 


 

1.结构体

1.1 结构的基础知识

结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量

对比数组,数组是同类型数据的集合

1.2 结构的声明

struct tag     //首先给了一个struct的结构体关键字,tag是标签名我们可以自己自定义想取什么名字就取什么名字。

{

member-list;//成员列表,每一个成员都是成员变量,可以是不同的类型

}variable-list;//这是变量列表,到底在怎么用,我们来看实际例子,来上手定义一个学生例子。

struct Stu

{

char name[20];//名字

int age;//年龄

char sex[5];//性别

char id[20];//学号

}; //分号不能丢

1.2.1特殊的声明(匿名结构体类型)

在声明结构的时候,可以不完全的声明。将标签名省略的结构体

struct

{

int a;

char b;

float c;

 };

1.3结构体变量的定义

当我们有了结构体类型,我们来看一下如何定义结构体变量。

①在创建结构体的时候直接在类型后面定义结构体变量:

struct Stu
{
 char name[20];//名字
 int age;//年龄
 char sex[5];//性别
 char id[20];//学号
}s1,s2; //分号不能丢

s1,s2就是定义的结构体变量。

②声明好了结构体类型后单独利用类型创建变量

struct Stu
{
 char name[20];//名字
 int age;//年龄
 char sex[5];//性别
 char id[20];//学号
}; //分号不能丢


struct Stu s1,s2;

③匿名结构体变量的定义只能在声明或者创建结构体类型的时候就定义,因为没有标签名就无法单独创建。

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

上述代码定义了结构体类型的数组和指针。

1.4关于匿名结构体类型的补充

我们来看这两段代码:

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

这两段代码由于都没有标签名,在我们看来那结构体类型似乎是一模一样的

但是两个结构体本质上是不同的,所以在使用的时候不可以第二个结构体定义的指针指向第一个结构体创建的变量:
p = &x;//错误写法

编译器会把上面的两个声明当成完全不同的两个类型。 所以是非法的

1.5结构体的自引用

在结构体中包含一个类型为该结构体本身的成员。

一定用结构体指针的形式看如下解释

补充数据结构知识:

数据结构:描述的是数据在内存中的组织结构

线性数据结构:

①.顺序表:

假如我要在内存中存储1,2,3,4,5

我就可以在内存中找一块连续的存储空间,比如找一个数组来存储:这就是顺序表

②.链表:我们也可以不找联系存储的空间来存放数据,不过我们可以这样通过1可以找到,2可以找到3这种方式比如:

我们把1,2,3这样的位置叫做链表的节点,每个节点中包含了自身的数据和下一个节点。这就是我们要讲的结构体的自引用:

那么要实现这个链表,有些伙伴可能就会这样去实现:

struct Node
{
 int data;
 struct Node next;
};

这样对吗?

当我们这样写,那我们能求出那sizeof(struct Node)是多少吗?

所以这种写法从本质上来说是错误的。

正确的实现方式是,前一个节点里面存储下一个节点的内容。我们来看一下实现方式:

struct Node
{
 int data;
 struct Node* next;
};

存放数据的一般叫做数据域,存放数据,指针叫做指针域

补充一个思考:

我们有些伙伴可能会这样写:

typedef struct
{
 int data;
 Node* next;
}Node;

对匿名结构体类型重定义为Node可以,但是:

我们对一个类型进行重定义的时候他必须先是一个完整的类型,这段代码就相当于,我还没有创建好Node这个类型就已经在使用了是不对的,

如果要这样用,我们可以这样写:不使用匿名结构体类型

typedef struct Node
{
 int data;
 struct Node* next;
}Node;

1.6结构体变量的初始化

①直接对变量赋值

struct SN
{
	char c;
	int i;
}sn1 = { 'q',100 };

②利用点操作符进行赋值:.

struct SN
{
	char c;
	int i;
}sn2 = { .i = 200,.c = 'w' };//当用点初始化的时候可以不在意顺序

③结构体嵌套初始化

struct Point
{
 int x;
 int y;
};

struct Node
{
 int data;
 struct Point p;
 struct Node* next; 
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化

2.结构体内存对齐(重点)

题型考察结构体的大小,我们来看一下例子引入,请问如下这段代码输出分别为什么:

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

明明是两个一样的结构体,为什么却不一样大,要知道答案我们就要知道结构体的大小是如何计算的,结构体大小的计算并不是单单就靠结构体内部元素的类型大小来决定,让我们来看一下:

2.1偏移量补充

 offsetof()    这个宏可以计算结构体某一个成员相较于起始位置的偏移量

头文件:stddef.h

需要在宏中传入的是:结构体类型和结构体变量名,下面我们来计算一下结构体s1中成员的偏移量和S2结构体的偏移量

struct S1
{
	char c1;
	int i;
	char c2;
};
struct S2
{
	char c1;
	char c2;
	int i;
};
int main()
{
	/*printf("%d\n", sizeof(struct S1));
	printf("%d\n", sizeof(struct S2));*/
	printf("%d\n", offsetof(struct S1, c1));
	printf("%d\n", offsetof(struct S1, i));
	printf("%d\n", offsetof(struct S1, c2));
	return 0;
}

2.2对齐规则 

1. 第一个成员在与结构体变量偏移量为0的地址处。

2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。

对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 VS中默认的值为8 Linux中没有默认对齐数,对齐数就是成员自身的大小

3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。所有成员对齐数的最大值。

4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

我们来看一下图解S1的对齐:

 

对对齐规则第四条的解释:

让我们来看一下结构体嵌套的对齐算法:

 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

看一下这段代码输出风别为多少

struct S3
{
	double d;
	char c;
	int i;
};


struct S4
{
	char c1;
	struct S3 s3;
	double d;
};

int main()
{
	
	printf("%d\n", sizeof(struct S3));
	printf("%d\n", sizeof(struct S4));
	return 0;
}

 

 

如果出行数组,就当做多个同类型数据处理

2.3为什么会有对齐

大部分的参考资料都是如是说的:

1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。比如某些平台规定整型必须存放在4的整数倍地址处。

2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访 问。

虽然浪费了一些空间,但是换来了访问效率的提升

总体来说: 结构体的内存对齐是拿空间来换取时间的做法。 那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到: 让占用空间小的成员尽量集中在一起。

2.4修改默认对齐数 

一般将对齐数设置为2^n次方,之前我们见过了 #pragma 这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数。

例如:

#pragma pack(8)//设置默认对齐数为8

#pragma pack()//取消设置的默认对齐数,还原为默认

pragma pack(1)//设置默认对齐数为1

#pragma pack(1)//设置默认对齐数为1
struct S2
{
	char c1;
	int i;
	char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
	
	printf("%d\n", sizeof(struct S1));

	return 0;
}

结论: 结构在对齐方式不合适的时候,我们可以自己更改默认对齐数。

后续更新到宏的讲解的时候会补充实现offsetof的实现

3.结构体传参 

结构体作为函数传参有两种方式,传地址和传形参如下:

struct S
{
 int data[1000];
 int num;
};
struct S s = {{1,2,3,4}, 1000};
//结构体传参
void print1(struct S s)
{
 printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
 printf("%d\n", ps->num);
}
int main()
{
 print1(s);  //传结构体
 print2(&s); //传地址
 return 0;
}

 

 函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的 下降。所以首选地址传参

4.位段

结构体讲了就得讲讲结构体实现 位段 的能力。

4.1什么是位段

位段的声明和结构是类似的,有两个不同:

1.位段的成员必须是 int、unsigned int 或signed int 。

2.位段的成员名后边有一个冒号和一个数字。

例如:以下就是一个位段类型

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

我们来看一下位段的大小是多少:

这里为什么打印8,我们来往下了解:

为什么存在位段来这样分配空间:

有的时候比如0/1、2、3这几个数只用三个二进制位就可以表示出来:

000 0001 010 011

2个比特位就够了,如果分配四个字节就会浪费30个比特位。

4.2位段的内存分配 

1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型

2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。

3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

比如给上述位段中的-a开辟内存到底从左开辟还是从右开辟,当开辟不够的时候是从新使用一块空间还是补充使用就行,c语言也没有规定。

我们来验证一下当前我们的环境下是如何开辟的 

我们先看一下可能的内存分配方式:

接着我们给位段创建变量并复制:

那我们就知道了当前环境下的位段的内存开辟形式。

4.3 位段的跨平台问题 

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

2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机 器会出问题。:如果位段类型是int,如果在早期16位机器上,int是16位比特位,两个字节

3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。

4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是 舍弃剩余的位还是利用,这是不确定的。

总结:

跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。

4.4位段的应用 

网络底层中的ip数据包

设计成位段,好设计也方便,网络传输数据越小越好,传输速率快。

5.结语

以上就是本期的所有内容,知识含量蛮多,大家可以配合解释和原码运行理解。创作不易,大家如果觉得还可以的话,欢迎大家三连,有问题的地方欢迎大家指正,一起交流学习,一起成长,我是Nicn,正在c++方向前行的奋斗者,感谢大家的关注与喜欢。
 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/379878.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

报错ValueError: Unknown CUDA arch (8.6) or GPU not supported

文章目录 问题描述解决方案参考文献 问题描述 报错 ValueError: Unknown CUDA arch (8.6) or GPU not supported 本人显卡为 RTX 3060,CUDA 为 10.2,PyTorch 为 1.5 解决方案 修改 C:\Users\Administrator\Envs\test\Lib\site-packages\torch\utils\c…

nvm安装nodejs 报错certificate has expired or is not yet valid

今天在使用nvm安装nodejs时,突然报如下错误: 从报错信息中很容易知道这是因为镜像凭证过期,所以我们只需要换个镜像即可。 打开你nvm的安装目录下的settings.txt文件,将下面两行添加到里面,如果已经有的就覆盖。 nod…

【制作100个unity游戏之24】unity制作一个3D动物AI生态系统游戏3(附项目源码)

最终效果 文章目录 最终效果系列目录前言随着地面法线旋转在地形上随机生成动物不同部位颜色不同最终效果源码完结系列目录 前言 欢迎来到【制作100个Unity游戏】系列!本系列将引导您一步步学习如何使用Unity开发各种类型的游戏。在这第24篇中,我们将探索如何用unity制作一…

一周学会Django5 Python Web开发-Django5创建项目(用命令方式)

锋哥原创的Python Web开发 Django5视频教程: 2024版 Django5 Python web开发 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili2024版 Django5 Python web开发 视频教程(无废话版) 玩命更新中~共计11条视频,包括:2024版 Django5 Python we…

前端滚动组件分享

分享一个前端可视化常用的卡片列表滚动组件&#xff0c;常用于可视化项目左右两侧的卡片列表的滚动。效果如下图所示&#xff1a; 组件描述 当鼠标移入滚动区域时&#xff0c;滚动行为停止当鼠标再次离开时&#xff0c;滚动继续 源码展示 <template><div ref"…

停车场|基于Springboot的停车场管理系统设计与实现(源码+数据库+文档)

停车场管理系统目录 目录 基于Springboot的停车场管理系统设计与实现 一、前言 二、系统功能设计 三、系统实现 1、管理员功能实现 &#xff08;1&#xff09;车位管理 &#xff08;2&#xff09;车位预订管理 &#xff08;3&#xff09;公告管理 &#xff08;4&#…

Zoho Mail企业邮箱商业扩展第2部分:企业运营

在关于Zoho Mail企业邮箱商业扩展应用的这个系列的第一部分中&#xff0c;我们遇到了一位名叫王雪琳的个体企业家。她经营着自己的营销机构&#xff0c;并展示了如何创建和管理自己的企业。为了提升企业的专业形象&#xff0c;王雪琳使用了Zoho Mail来建立个性化的电子邮件域名…

【Linux】线程

线程 我们常常会在linux中或者在操作系统这门课中听到进程和线程的名称&#xff0c;我们之前认识了进程的概念&#xff0c;现在我们来了解一下线程的概念 线程概念&#xff1a; 什么是线程 在一个程序里的一个执行路线就叫做线程&#xff08;thread&#xff09;。更准确的定…

牛客网SQL264:查询每个日期新用户的次日留存率

官网链接&#xff1a; 牛客每个人最近的登录日期(五)_牛客题霸_牛客网牛客每天有很多人登录&#xff0c;请你统计一下牛客每个日期新用户的次日留存率。 有一个登录(login。题目来自【牛客题霸】https://www.nowcoder.com/practice/ea0c56cd700344b590182aad03cc61b8?tpId82 …

第三百一十五回

文章目录 1. 概念介绍2. 基本用法3. 补充用法4. 内容总结 我们在上一章回中介绍了"再谈ListView中的分隔线"&#xff0c;本章回中将介绍showMenu的用法.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介绍 我们在第一百六十三回中介绍了showMenu相关的内容…

C++学习Day04之单例模式

目录 一、程序及输出1.1 饿汉式实例1.2 饿汉式单例1.3 懒汉式单例1.4 线程安全的懒汉式单例 二、分析与总结 一、程序及输出 1.1 饿汉式实例 #include<iostream> using namespace std; #include <string> class Printer { public:static Printer * getInstance()…

《向量数据库指南》——Milvus Cloud「日志」问题定位的指南针

“2.X 集群的日志在哪里导啊”“现在没有对 Milvus Cloud 进行任何读写操作,但是日志还是不断增加,这正常吗?”“请教下 k8s 部署的 Milvus Cloud 日志如果持久化,只能使用共享存储吗?如果只想放在本地盘可以如何配置?” 社区讨论问题的时候基本都离不开日志,因为日志…

鸿蒙(HarmonyOS)项目方舟框架(ArkUI)之Span组件

鸿蒙&#xff08;HarmonyOS&#xff09;项目方舟框架&#xff08;ArkUI&#xff09;之Span组件 一、操作环境 操作系统: Windows 10 专业版、IDE:DevEco Studio 3.1、SDK:HarmonyOS 3.1 二、Span组件 鸿蒙&#xff08;HarmonyOS&#xff09;作为Text组件的子组件&#xff0…

探索Xposed框架:个性定制你的Android体验

探索Xposed框架&#xff1a;个性定制你的Android体验 1. 引言 在当今移动设备市场中&#xff0c;Android系统作为最受欢迎的操作系统之一&#xff0c;其开放性和可定制性备受用户青睐。用户希望能够根据个人喜好和需求对其设备进行定制&#xff0c;以获得更符合自己习惯的使用…

架构整洁之道-软件架构-展示器和谦卑对象、不完全边界、层次与边界、Main组件、服务

6 软件架构 6.9 展示器和谦卑对象 在《架构整洁之道-软件架构-策略与层次、业务逻辑、尖叫的软件架构、整洁架构》有我们提到了展示器&#xff08;presenter&#xff09;&#xff0c;展示器实际上是采用谦卑对象&#xff08;humble object&#xff09;模式的一种形式&#xff…

js手写Promise(下)

目录 resolve与reject的调用时机封装优化 回调返回PromiseisPromise手动调用then 微队列catchresolverejectall传入的序列为空传入的值非Promise race完整的Promise代码 如果没有看过上半部分的铁铁可以看看这篇文章 js手写Promise&#xff08;上&#xff09; resolve与reject…

GEE Colab——如何利用Matplotlib在colab中进行图形制作

在colab中绘制图表 笔记本的一个常见用途是使用图表进行数据可视化。Colaboratory 提供多种图表工具作为 Python 导入,让这一工作变得简单。 Matplotlib Matplotlib 是最常用的图表工具包,详情请查看其文档,并通过示例获得灵感。 线性图 线性图是一种常见的图表类型,用…

ELAdmin 的 CRUD

数据表结构 弄个测试的数据表&#xff0c;不同类型的几个字段&#xff0c;表名位 mp_reply。 生成代码 ELAdmin 可以自动生成代码。 左侧目录系统工具–代码生成&#xff0c;点开以后可以看到上面创建的数据表mp_reply&#xff0c;点击配置。 进入的页面内容有两部分&#…

88 SRC挖掘-拿下CNVD证书开源闭源售卖系统

目录 1&#xff0e;开源系统、闭源系统、售卖系统2&#xff0e;如何寻找上述三类系统并进行安全测试3&#xff0e;如何挑简单的入手最快速度获取证书装x演示案例:某开源逻辑审计配合引擎实现通用某闭源审计或黑盒配合引擎实现通用某售卖审计或黑盒配合引擎实现通用 涉及资源&am…

[职场] 进入大数据领域需要掌握哪些软件 #其他#职场发展#职场发展

进入大数据领域需要掌握哪些软件 学习大数据首先我们要学习Java语言和Linux操作系统&#xff0c;这两个是学习大数据的基础&#xff0c;学习的顺序不分前后。 Java 大家都知道Java的方向有JavaSE、JavaEE、JavaME&#xff0c;学习大数据要学习那个方向呢? 只需要学习Java的…
最新文章