C语言自定义类型结构体(24)

news2024/9/24 11:22:56

文章目录

  • 前言
  • 一、结构体类型的声明
    • 结构体回顾
    • 结构体的特殊声明
    • 结构体的自引用
  • 二、结构体的内存对齐
    • 对齐规则
    • 为什么存在内存对齐?
    • 修改默认对齐数
  • 三、结构体传参
  • 四、结构体实现位段
    • 什么是位段
    • 位段的内存分配
    • 位段的跨平台问题
    • 位段的应用
    • 位段使用的注意事项
  • 总结


前言

  事实上,我们早就有过关于结构体的学习
  现在,我们再来深入理解它一下


一、结构体类型的声明

结构体回顾

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

结构的声明

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

例如,我们想要描述一位学生:

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

结构体变量的创建和初始化
  运用大括号 { } 即可,在其里面进行初始化
  可以按照默认顺序初始化,也可以按照指定顺序初始化
  struct Stu s1 = {.age = 30, .name = “Lisi”, .sex = “nv”, .id = “2023020405”};

结构体的特殊声明

在声明结构的时候,可以不完全的声明

// 匿名结构体类型
// 只能使用一次
struct
{
	int a;
	char b;
	float c;
}x;

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

我们可以思考一下,对于上面两个结构体, p = &x; 这样的语句对不对

答案是不对的,虽然两个结构体成员相同,但是因为是匿名的,在编译器看来是两种不同的类型
在这里插入图片描述

大部分情况下我们不会用到匿名结构体的,你在使用的时候也需要注意一下

结构体的自引用

  在结构中包含一个类型为该结构本身的成员是否可以呢?
  可以,我们之后学数据结构的链表就是如此,请持续关注我的博客!

在结构体自引用使用的过程中,夹杂了 typedef 对匿名结构体类型重命名,也容易引入问题

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

答案是不行的,因为Node是对前面的匿名结构体类型的重命名产生的,但是在匿名结构体内部提前使用Node类型来创建成员变量,这是不行的,一言以蔽之就是,你提前使用了重命名类型

二、结构体的内存对齐

  现在我们要来深入探讨一个问题:结构体的大小计算
  其实也就是知识点 -> 内存对齐

先让你诧异一下,哈哈,就拿这两个输出作为引子吧!
在这里插入图片描述

对齐规则

结构体的对齐规则:

  1. 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
  3. 对齐数 = 编译器默认的一个对齐数 与 该变量成员大小的较小值
    - VS中默认的值为8
    - Linux中gcc没有默认对齐数,对齐数就是成员自身的大小
  4. 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数当中最大的)整数倍
  5. 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍
struct S1
{
	char c1; // 1Byte
	char c2; // 1Byte
	int i; // 4Bytes
}

  我们会发现,起始c1放在偏移量为 0 的位置上,c2放在偏移量为1的位置上,而 int 占 4 个字节,取较小值得到对齐数是 4 ,对齐到 4 的整数倍的位置 4 上,并占 4 5 6 7 这四个位置,并且算下来整个结构体占据了 8 个字节,是最大对齐数 4 的整数倍,所以结构体的总大小为 8 ,浪费了 2 个字节的空间,图示如下:
在这里插入图片描述

我们再来看S2

struct S2
{
	char c1; // 1Byte
	int i; // 4Bytes
	char c2; // 1Byte
}

  c1先放在偏移量为 0 的位置上,而 int 占 4 个字节,对齐到整数位置 4 上,并占 4 5 6 7四个位置,而c2占在偏移量为8的位置上,这时候结构体的最大对齐数是4,而结构体的大小为0 ~ 8,为9,不是4的整数倍,因此还要再来 3 个字节,也就是 0 ~ 11,最终,会发现结构体的大小为 12 ,这其中就浪费了 6 个字节的内存空间,图示如下:
在这里插入图片描述

接下来我再给出一个例子,你用做练习自己分析吧

struct S3
{
	double d; // 0 ~ 7
	char c; // 8
	int i; // 12 ~ 15
};

答案是16个字节

那再来个嵌套结构体的:

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

  首先c1放在偏移量为0的位置上,而s3是个结构体,按照上述第五条规则,应该对齐到结构体成员中最大对齐数8的整数倍位置上,显然s3放在8上,并占 8 ~ 23位置,(s3大小为16,前面求过),这时候double放到对齐数8的整数倍上,刚好24满足,并占24~31位置,这样的话,S4大小为32,恰好也满足了所有成员中对齐数的整数倍这一必要性条件,浪费7个字节,图示如下:
在这里插入图片描述

如果你要验证,可以自己去写输出部分验证,或者可以打开内存监视
亦或者,有一个宏 offsetof ,可以用来计算结构体成员相较于起始位置的偏移量
需要注意的是,这个宏需要包含一个头文件 stddef.h
在这里插入图片描述

为什么存在内存对齐?

  1. 平台原因(移植原因)
    不是所有的硬件平台都能够访问任意地址上的任意数据;某些硬件平台只能在某些地址处获取某些特定类型的数据,否则则出现硬件异常
  2. 性能原因:
    数据结构(尤其是栈)应该尽量地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要两次内存访问,而对齐的内存访问只需要一次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证所有的double类型的数据都在对齐的地址中,那么就可以用一个内存操作来读取者写值了。否则,我们可能需要进行两次内存访问,因为对象可能分散在两个8字节内存块中。
    按图来说的话,性能原因如下:
    在这里插入图片描述

总而言之:结构体的内存对齐是拿空间来换时间的一种做法

所以在设计结构体的时候,我们既要满足对齐,又要节省空间的话,可以考虑将占用内存小的成员尽量集中在一起
比如上述的 S1 和 S2 结构体,S1 设计的就比 S2 好

修改默认对齐数

#pragma 这个预处理指令,可以改变编译器的默认对齐数

#include <stdio.h>  
#pragma pack(1) //设置默认对齐方式为1  
struct S  
{  
    char c1;  
    int i;  
    char c2;  
};  
#pragma pack() //取消设置的对齐,恢复默认  
int main()  
{  
    //输出的结果是什么?  
    printf("%d\n", sizeof(struct S));  
    return 0;  
}

答案是6,自行分析
当我们觉得对齐方式不合理的时候,我们可以自己更改默认对齐数

三、结构体传参

#include <stdio.h>

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;
}

  上面 printf1 和 printf2 函数哪个好些?
  printf2的方式更好一些,也就是说传地址方式更好一些,本质上还是因为形参是实参的一份拷贝,太浪费内存空间了

四、结构体实现位段

  你可能只听说过段位,没事,现在我来跟你讲一下位段
  没听过很正常,这个知识点比较细致,但是对于了解底层以及未来网络的学习还是很有用的

什么是位段

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

  1. 位段的成员必须是int、unsigned int或 signed int,在C99中位段成员的类型也可以选择其他类型
  2. 位段的成员名后边有一个冒号和一个数字
struct A
{
	int _a:2;
	int _b:5;
	int _c:10;
	int _d:30;
}// A就是一个位段类型

那么位段A所占内存的大小是多少?
在这里插入图片描述

为什么不是按照前面讲述的内存对齐,是16反而是8?

位段的内存分配

位段的出现就是为了节省空间

  1. 位段的成员可以是 int、unsigned int、signed int 或者是 char 等类型。
  2. 位段的空间上是按照需要以 4 个字节(int)或者 1 个字节(char)的方式来开辟的。
  3. 位段有很多不确定因素,位段是不跨平台的,注意可移植的程序应避免使用位段。(比如说一个字节从左向右还是从右向左使用?)

假设从右向左,来个例子如下:
在这里插入图片描述
  _a给两个比特位,_b给5个比特位,_c给10个比特位,_d给30个比特位
  _a、_b、_c没什么好说的,但是这个_d就有两种存储方式了,是直接弃掉第一个 int 剩下的15个比特位,新开一个 int 用30个比特位存_d ;还是继续使用完这15个比特位,再新开个 int 用15个存_d,其实,C语言也没有规定,完全取决于编译器,哈哈,这又是不确定性

VS下其实是从右向左存储,如果剩余的空间不够下一个成员使用,就新开空间并浪费
我们来通过下面这段代码来进行验证:

#include <stdio.h>

struct S
{
	// char就好在是一个字节一个字节开辟
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};

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

	printf("%zd", sizeof(struct S)); // 3
	return 0;
}

首先,a 和 b共占用了7个比特位,剩下1个比特位不够 c ,于是新开一个字节放 c,同理再新开一个字节放 d
在这里插入图片描述
接下来我们给 a 赋值为 10,即1010,截断后三位,放入010,同理b放入1100

这时候,第一个字节的8个比特位就是01100010

同理c放入00011

这时候,第二个字节的8个比特位就是00000011

同理d放入0100

这时候,第三个字节的8个比特位就是00000100

那么,按照十六进制位,这三个字节就是分别就是 0x62、0x03、0x04,打开内存!
在这里插入图片描述

果然如此!!

位段的跨平台问题

  1. int 位段被当成有符号数还是无符号数是不确定的
  2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。)
  3. 位段中的成员在存储中有右分配,还是从右向左分配标准尚未定义。
  4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位置,是含有剩余的位置是利用,这是不确定的。

位段的应用

  同开头所说,位段在于网络传输部分还是用处蛮大的
  这个自行了解,我也是现查的,具体原理大家自辨

位段使用的注意事项

  位段的几个成员共享同一个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位段处是没有地址的。内存中每个字节分配一个地址,一个字节内部的bit位是没有地址的。

  所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输入值,只能是先输入放在一个变量中,然后赋值给位段的成员。

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

int main()  
{  
    struct A sa = {0};  
    scanf("%d", &sa._b); // error
    
    // right 
    int b = 0;  
    scanf("%d", &b);  
    sa._b = b;  
    return 0;  
}

总结

  我也没想到结构体深入了解竟然能讲出那么多东西来,哈哈
  本篇还是比较难的,请你和我好好消化一下,准备接下来的学习~

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

安全区域边界等保测评

1.边界防护 应保证跨越边界的访问和数据流通过边界设备提供的受控接口进行通信。 [测评方法] 1)应核查在网络边界处是否部署访问控制设备;网闸和防火墙2)应核查设备配置信息是否指定端口进行跨越边界的网络通信,指定端口是否配置并启用了安全策略acl 3)应采用其他技术手…

C盘垃圾一个你不能忽视的问题!电脑小白学用这方法最简单好用!

C盘垃圾一个你不能忽视的问题&#xff01;电脑小白学用这方法最简单好用&#xff01;大家的电脑有没经常看到下图这样&#xff1f;C盘变成红色的了&#xff0c;这个因为是太多的垃圾文件堆积&#xff0c;而你又不经常清理这些垃圾。 所以就直接的导致了这些垃圾的堆积&#xff…

FloodFill算法(DFS+BFS)【上】

文章目录 FloodFill算法733. 图像渲染题目解析算法原理代码实现 200. 岛屿数量题目解析算法原理代码实现 695. 岛屿的最大面积题目解析算法原理代码实现 130. 被围绕的区域题目解析算法原理代码实现 FloodFill算法 FloodFill算法&#xff0c;中文名叫洪水灌溉 这些模拟一块区域…

SQL进阶技巧:火车票相邻座位预定一起可能情况查询算法 ?

目录 0 场景描述 1 数据准备 2 问题分析 2.1 分析函数法 2.2 自关联求解 3 小结 如果觉得本文对你有帮助&#xff0c;那么不妨也可以选择去看看我的数字化建设通关指南博客专栏 &#xff0c;或许对你更有用。专栏原价99&#xff0c;现在活动价29.9&#xff0c;按照阶梯…

深圳前海港湾学校天桥下的免费停车点探寻

​最近开车去桂湾公园停车场的路上&#xff0c;看到有个天桥下有很多车停着&#xff0c;关键是这个地方离我搬砖的前海卓越写字楼还近&#xff0c;要是有时候桂湾公园停车位满了&#xff0c;停不进去的时候&#xff0c;是不是可以去这个天气下薅免费停车的羊毛&#xff1f;哈哈…

基于YOLO深度学习和百度AI接口的手势识别与控制项目

基于YOLO深度学习和百度AI接口的手势识别与控制项目 项目描述 本项目旨在开发一个手势识别与控制系统&#xff0c;该系统能够通过摄像头捕捉用户的手势&#xff0c;并通过YOLO深度学习模型或调用百度AI接口进行手势识别。识别到的手势可以用来控制计算机界面的操作&#xff0…

【OJ刷题】双指针问题4

这里是阿川的博客&#xff0c;祝您变得更强 ✨ 个人主页&#xff1a;在线OJ的阿川 &#x1f496;文章专栏&#xff1a;OJ刷题入门到进阶 &#x1f30f;代码仓库&#xff1a; 写在开头 现在您看到的是我的结论或想法&#xff0c;但在这背后凝结了大量的思考、经验和讨论 目录 1…

Google宣布所有英语语种的Gemini Live用户现可免费使用

谷歌在Pixel 9系列手机的发布会上&#xff0c;同步推出了Gemini Live&#xff0c;该服务提供了一种移动对话体验&#xff0c;让用户和Gemini展开自由流畅的对话。Google现在宣布&#xff0c;所有使用英语的Gemini Live用户&#xff0c;可免费使用&#xff0c;此前为需要Advance…

RockyLinux-软件实现RAID5

一、背景 RAID&#xff08;Redundant Array of Independent Disks&#xff0c;独立磁盘冗余阵列&#xff09;是一种将多个物理硬盘驱动器组合成单一逻辑单元的技术&#xff0c;目的是提高存储性能、可靠性和/或数据冗余度。虽然早期的名字中包含“独立”&#xff08;Independen…

【C++】stack 和 queue 以及 容器适配器

文章目录 一、stack1.1 stack的使用1.2 stack的模拟实现 二、queue2.1 queue的使用2.2 queue的模拟实现 三、优先级队列1.优先级队列的介绍2. priority_queue的使用的使用3.模拟实现优先级队列 四、 容器适配器1.STL标准库中stack和queue的底层结构2.deque&#xff08;双端对列…

Unity3D 发布后去除Development Build显示

问题描述&#xff1a; Build后在视野右下角看到“Development Build”白色小字 解决方法&#xff1a; build时不勾选Development Build项 PS: 游戏开发unity杂项知识系列&#xff1a;build时Development Build的作用_unity development build-CSDN博客

结构体的内存对齐

对⻬规则&#xff1a; 1.结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处 2.其他成员变量要对⻬到某个数字&#xff08;对⻬数&#xff09;的整数倍的地址处。 对⻬数编译器默认的⼀个对⻬数与该成员变量⼤⼩的较⼩值。 但一些编译器下并没有默认对其数 3.结…

ElK 8 收集 Nginx 日志

1. 说明 elk 版本&#xff1a;8.15.0 2. 启个 nginx 有 nginx 可以直接使用。我这里是在之前环境下 docker-compose.yml 中启动了个 nginx&#xff1a; nginx:restart: alwaysimage: nginx:1.26.1ports:- "80:80"- "443:443"volumes:#- ./nginx/html:/…

探索Python轻量级数据库:TinyDB的奇妙之旅

文章目录 探索Python轻量级数据库&#xff1a;TinyDB的奇妙之旅背景&#xff1a;为何选择TinyDB&#xff1f;什么是TinyDB&#xff1f;如何安装TinyDB&#xff1f;简单库函数使用方法场景应用常见Bug及解决方案总结 探索Python轻量级数据库&#xff1a;TinyDB的奇妙之旅 背景&…

H5 three.js 实现六年级观察物体

o(&#xffe3;▽&#xffe3;)ブ 我又带着新的demo来啦~ 预览 功能点 立方体的阴影 立方体的添加 位置记录 最大限制 三视图展示 立方体的移除 答题模式 随机出题 题库出题 源码 注释算是比较全了&#xff0c;可能部分会有点绕&#xff0c;还能够再优化一下~ <!DOCTYPE …

51单片机+proteus+(DS1302+DS18B20)

目录 1.温度传感器检测&#xff08;DS18B20&#xff09; 1.1基本概念 1.1.1 DS18B20的简介 1.1.2 DS18B20的框图 ​编辑 1.1.3 DS18B20的指令 1.1.4 总线时序协议 1.2代码 1.2.1DS18B20的初始化代码 1.2.2DS18B20的读代码 1.2.3DS18B20的写时序 ​编辑 1.2.4DS18B20…

[C语言]第九节 函数一基础知识到高级技巧的全景探索

目录 9.1 函数的概念 9.2 库函数 9.2.1 标准库与库函数 示例&#xff1a;常见库函数 9.2.2 标准库与头文件的关系 参考资料和学习工具 如何使用库函数 ​编辑 9.3 ⾃定义函数 9.3.1 函数的语法形式 9.3.2函数的举例 9.4 实参与形参 9.4.1 什么是实参&#xff1f; 9…

嵌入式单片机中数码管基本实现方法

1. 点亮数码管 本节课利用已经学习的LED知识去控制一个8位数码管。 本节的原理比较简单。不需要多少时间讲。 更多时间是跟大家一起编码调试,从中学习一些编码思路和学习方法。 1.1. 什么是数码管 数码管是什么?下图就是一个数码管 从硬件上个看,其实就是8个LED组合…

Python实用的27个实例,涵盖从基础到进阶的所有领域!

Python 是一种广泛使用的高级编程语言&#xff0c;以其简洁的语法和丰富的库支持而受到开发者们的喜爱。以下列出了 27 个实用的 Python 实例&#xff0c;涵盖从基础到进阶的不同领域&#xff0c;帮助你提升编程技能。 1. 打印 "Hello, World!" print("Hello,…

企业微信如何利用群聊机器人定时推送消息

背景&#xff1a;公司其他部门有在群聊中新建机器人并定时推送消息的需求&#xff0c;webhook已准备好 1、准备一台服务器或者虚拟机&#xff0c;编写脚本 [rootlocalhost opt]# vim crontab.sh curl https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key4d38964b-04f2-44…