当前位置: 首页 > news >正文

匿名结构体类型、结构体的自引用、结构体的内存对齐以及结构体传参

请添加图片描述

文章目录

  • 🚀前言
  • 🚀结构体
    • ✈️结构体类型的声明
    • ✈️结构体变量的创建与初始化
    • ✈️结构体类型的特殊声明
    • ✈️结构体的自引用
    • ✈️结构体的内存对齐
      • 🚁修改默认对齐数
    • ✈️结构体传参

🚀前言

在C语言中有着各种数据类型,这些类型有配划分为内置类型自定义类型两大类(如下图)。铁子们,今天阿辉要分享的就是自定义类型中的结构体联合体和枚举将在下篇文章分享,至于数组阿辉之前的文章数组篇中已经详细讲到,铁子们感兴趣的话可以点击跳转😘,不多bb直接开始我们今天的学习👊
请添加图片描述

🚀结构体

铁子们是否有这样的疑问——C语言为什么要引入结构体这一自定义类型?
别急,听阿辉一一道来👇

其实结构体数组有一点类似,数组是存储同一种数据类型的集合,而结构体是存储不同类型的集合,比如当你想描述一个学生时,你得有姓名、年龄、学号等等一系列特征
可是我们发现这不是某一种单一的数据类型能够描述的,这时引入结构体这一自定义类型是非常有必要的

对于结构体有何用想必铁子们有了初步的认识,咱们接着往下看👇

✈️结构体类型的声明

声明结构体的语法结构:

struct tag 
{member_list; 成员列表,
}variable_list;  变量列表,在结构体声明时就创建的变量
struct tag 这个整体属于类型名,和intchar等等类型名一样

我们来创建一个描述学生的结构体类型:

struct stu
{char name[20];名字int age;年龄int id;学号char sex[5];性别
};//注意这里的分号不能丢了

结构体类型的声明同样分为全局声明和局部声明,结构体全局声明以及声明时创建的变量作用域都是整个程序,而结构体的局部声明以及声明时创建的变量的作用域在该大括号内部{}
在这里插入图片描述

✈️结构体变量的创建与初始化

结构体变量有两种创建方式,一种在结构体类型声明时就创建,与结构体类型声明具有相同的作用域;另一种在结构体类型声明后创建,作用域与结构体类型声明无关,咱直接上代码👇

struct stu
{char name[20];int age;
}s1;//s1属于全局变量int main()
{struct stu s2;//s2局部变量,作用域在main函数内return 0;
}

结构体变量初始化:

#include <stdio.h>
struct Stu
{char name[20];//名字int age;//年龄char sex[5];//性别char id[20];//学号
};
int main()
{//按照结构体成员的顺序初始化struct Stu s1 = { "张三", 20, "男", "20230818001" };//按照指定的顺序初始化struct Stu s2 = { .age = 18, .name = "lisi",.id = "20230818002", .sex = "⼥};return 0;
}

✈️结构体类型的特殊声明

铁子们,结构体还有一种特殊的声明,这种声明把结构体的标签tag给干掉了,这种特殊声明的结构体被称为匿名结构体类型
我们来上一组例子:

struct
{int a;int b;
}a;struct
{int a;int b;
}*p;int main()
{p = &a;return 0;
}

上⾯的两个结构在声明的时候省略掉了结构体标签tag
那么问题来了?
p = &a这样写是否合法?

对于匿名结构体类型,上述两个结构体类型看似一样,实则不同,匿名结构体的变量只能在声明时创建且只能有一个变量,上述编译器会把匿名结构体指针变量p与&a当作两个不同的类型

✈️结构体的自引用

结构体的自引用本质是结构体的递归定义,但是这会存在很大问题,如下面这个代码

struct node
{int data;struct node next;
}

在编译期间,编译器需要知道结构体变量大小为结构体变量分配空间,但是上述这个结构体我们仔细想一下会发现这个结构体无限递归,根本无法确定其大小,为解决上述问题,我们可以通过指针来间接引用结构体,如下:

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

上述就是正确的结构体自引用,通过结构体自引用我们可以创建具有互相关联关系的数据结构,如链表、树等,在数据结构中结构体尤为重要。

✈️结构体的内存对齐

有了上面对于结构体的理解,铁子们对结构体的基本使用应该不成问题了,接下来咱们来研究一个深入的问题——结构体类型的大小

有的老铁可能会说:不是很简单吗❓直接把所有变量所占字节空间大小全都加起来就完事了

但是真有这么简单吗?我们接着看👇
其实结构体存在内存对齐这一规则:

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

在VS中char的对齐数就是1int对齐数就是4double对齐数就是8
我们来看看一个是否如此

#include <stdio.h>
struct S1
{char c1;int i;char c2;
};
int main()
{printf("%d\n", sizeof(struct S1));return 0;
}

在这里插入图片描述

我们可以看到对于两个char和一个int应该是6个字节,但是通过sizeof却打印出了12

这里我用用图为铁子们解释:
请添加图片描述
上图一个方块代表一个字节,对于第一个成员c1就存在偏移量为0的地址处,而对于第二个成员i它是int类型对齐数为4,要存在为4的倍数的偏移量处也就是上图位置出,第三个成员c2char类型对齐数为1存在i成员后面,现在整个大小只有9个字节并非最大对齐数4的整数倍所以还要补3个字节分配给它,上图中蓝色方块代表浪费的内存
知道了结构体内存对齐的计算之后,问题又来了:为什么存在内存对齐❓
有两个原因:

平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常

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

结构体的内存对齐是一种拿空间换时间的做法

在设计结构体的时候,我们既要满足对齐,又要节省空间,这该如何做呢?
在我们声明结构体时尽量让占用空间小的成员集中在一起,如:

#include <stdio.h>
//例如:
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;
}

在这里插入图片描述
struct s2就要比struct s1的空间小4个字节

🚁修改默认对齐数

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

#pragma pack(1)//设置默认对齐数为1
struct S1
{char c1;int i;char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{//输出的结果是什么?printf("%d\n", sizeof(struct S1));return 0;
}

输出为6当默认对齐数为1时也就不存在对齐了,直接把所有变量所占字节空间大小全都加起来就完事了

✈️结构体传参

与其他类型变量传参一样,同样可以传址和传值

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

上述代码都可以帮我们打印,但是传址调用更好

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

SO结构体在传参时要传递结构体地址


感谢老铁能看到这,到这里结构体的分享就到此为止了,如果觉得阿辉写得不错的话,记得给个赞呗,你们的支持是我创作的最大动力🌹
请添加图片描述

相关文章:

  • Mac电脑每次修改完java的版本后,没有成功
  • zookeeper实操课程Acl 访问权限控制,命令行测试
  • c语言练习13周(1~5)
  • Filebeat使用指南
  • JVM 内存结构
  • git rebase冲突说明(base\remote\local概念说明)
  • 上个班而已
  • mysql在linux环境下安装(rpm)以及初始化后的登录配置
  • 一小时玩转【负载均衡】
  • AD7124-4 实测热电偶数据读取,电压精度到稳定到±1uV, 电压波动260nV, 温度精度到±0.01℃
  • HarmonyOS——UI开展前的阶段总结
  • c++ day 4
  • 【slab/0x40 UAF】TPCTF2023 - core 一题多解
  • Linux查看计算机处理器相关的信息
  • 在oracle中的scn技术
  • 【译】JS基础算法脚本:字符串结尾
  • __proto__ 和 prototype的关系
  • ERLANG 网工修炼笔记 ---- UDP
  • ES6 学习笔记(一)let,const和解构赋值
  • JavaScript对象详解
  • Nacos系列:Nacos的Java SDK使用
  • nginx(二):进阶配置介绍--rewrite用法,压缩,https虚拟主机等
  • SQLServer之索引简介
  • ViewService——一种保证客户端与服务端同步的方法
  • 巧用 TypeScript (一)
  • 如何借助 NoSQL 提高 JPA 应用性能
  • 什么是Javascript函数节流?
  • 我建了一个叫Hello World的项目
  • 2017年360最后一道编程题
  • 湖北分布式智能数据采集方法有哪些?
  • !!Dom4j 学习笔记
  • #【QT 5 调试软件后,发布相关:软件生成exe文件 + 文件打包】
  • (10)STL算法之搜索(二) 二分查找
  • (2)STM32单片机上位机
  • (4)事件处理——(6)给.ready()回调函数传递一个参数(Passing an argument to the .ready() callback)...
  • (Oracle)SQL优化技巧(一):分页查询
  • (pt可视化)利用torch的make_grid进行张量可视化
  • (Redis使用系列) Springboot 使用redis的List数据结构实现简单的排队功能场景 九
  • (附源码)计算机毕业设计ssm本地美食推荐平台
  • (收藏)Git和Repo扫盲——如何取得Android源代码
  • (四)搭建容器云管理平台笔记—安装ETCD(不使用证书)
  • (学习日记)2024.04.04:UCOSIII第三十二节:计数信号量实验
  • (转)人的集合论——移山之道
  • (转载)虚函数剖析
  • ..回顾17,展望18
  • ./configure,make,make install的作用(转)
  • .bat批处理(五):遍历指定目录下资源文件并更新
  • .MyFile@waifu.club.wis.mkp勒索病毒数据怎么处理|数据解密恢复
  • .NET 分布式技术比较
  • .NET 中创建支持集合初始化器的类型
  • .net解析传过来的xml_DOM4J解析XML文件
  • .NET微信公众号开发-2.0创建自定义菜单
  • .NET应用架构设计:原则、模式与实践 目录预览
  • .one4-V-XXXXXXXX勒索病毒数据怎么处理|数据解密恢复
  • @html.ActionLink的几种参数格式