C语言超详细结构体知识

1.自定义类型:结构体的介绍

在之前的博客中,我们简单介绍过了关于结构体的基本知识,这里我们稍微复习一下。

结构体(struct)是C语言中一种重要的复合数据类型,它允许将不同类型的数据组合成一个整体。


1.1结构体的定义

结构体使用struct关键字定义,基本语法:

struct 结构体名 {数据类型 成员1;数据类型 成员2;// ...
};

 例如描述一个学生:

struct Stu {char name[20];int age;char sex[10];
};

1.2结构体的声明和初始化

struct Student {int id;char name[20];float score;
};// 方式1: 先定义结构体类型,再声明变量
struct Student stu1;// 方式2: 定义结构体类型的同时声明变量
struct Student {int id;char name[20];float score;
} stu2, stu3;// 方式3: 使用typedef创建别名
typedef struct {int id;char name[20];float score;
} Student;
Student stu4;//方法4:特殊声明,在声明结构体的时候,可以不完全声明
struct {int id;char name[20];float score;
}stu5;struct {int id;char name[20];float score;
}* stu6;stu6 = &stu5;

上述前三种声明都没有什么问题,而第四种声明我们要格外注意,我们在声明里省略了结构体标签,那么stu6 = &stu5这个代码能否能够正确运行呢?我们测试一下:

我们可以看到编译器报错了,编译器会把上面两个声明当成完全不同的类型。 

初始化结构体变量的方法一般有两种,如下:

struct Student {int id;char name[20];float score;
};//按定义顺序初始化
struct Student stu1 = { 20253265,"zhangsan",58.8 };//指定成员初始化
struct Student stu1 = { .id = 20251653,.name = "lisi",.score = 78.8 };

1.3结构体成员的访问

1.使用点运算符 访问结构体成员:

struct Student {int id;char name[20];float score;
};int main()
{struct Student stu1 = { 20253265,"zhangsan",58.8 };printf("%d\n", stu1.id);printf("%s\n", stu1.name);printf("%f\n", stu1.score);return 0;
}

 2.对于结构体指针,使用箭头运算符 ->访问成员:

struct Student {int id;char name[20];float score;
};int main()
{struct Student stu1 = { 20253265,"zhangsan",58.8 };struct Student* ps = &stu1;printf("%d\n", ps->id);printf("%s\n", ps->name);printf("%d\n", ps->score);return 0;
}

2. 结构体内存对齐

2.1对齐规则

学习上文已经使我们掌握了结构体的基本使用,现在我们要来深入探讨一个问题:计算结构体的大小。我们先来看一段代码:

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

大家可以猜一下这段代码的结果,会打印6,6吗,我们运行看结果:

 我们看到结果打印和我们预料结果完全不同,这是否说明在结构体中内存分配和正常内存分配有很大差异呢?答案是肯定的,我们先来学习结构体内存分配规则:对齐规则。

对齐规则:

1. 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处

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

    对齐数 = 编译器默认的⼀个对齐数 与 该成员变量大小的较小值。

    - VS 中默认的值为 8

    - Linux中 gcc 没有默认对齐数,对齐数就是成员自身的大小

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

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

S1结构体的第一个变量c1对齐到和结构体变量起始位置偏移量为0的地址处,对应0字节空间,第二个成员变量为c2,对齐数为1(字符类型变量大小为1)和8(vs2022默认对齐数为8)的最小值,其实位置要对齐1的整数倍,对应1字节空间,第三个成员变量为i,对齐数为4(整型类型变量大小为4)和8(vs2022默认对齐数为8)的最小值,起始位置要对齐4的整数倍,对应4~7的字节空间。结构体总大小是最大对齐数的整数倍,S1结构体最大对齐数是4,要存入三个成员变量,至少需要8个字节,所以该结构体总大小为8个字节。如上图所示。

S2结构体的第一个变量c1对齐到和结构体变量起始位置偏移量为0的地址处,对应0字节空间,第二个成员变量为i,对齐数为4(整型类型变量大小为4)和8(vs2022默认对齐数为8)的最小值,起始位置要对齐4的整数倍,对应4~7字节空间,第三个成员变量为c2,对齐数为1(字符类型变量大小为4)和8(vs2022默认对齐数为8)的最小值,起始位置要对齐1的整数倍,对应8的字节空间。结构体总大小是最大对齐数的整数倍,S1结构体最大对齐数是4,要存入三个成员变量,至少需要9个字节,所以该结构体总大小为12个字节。如上图所示。

 解决了上述两个问题,我们在看一个存在结构体嵌套求解结构体内存大小的问题。

 S3结构体的第一个变量c1对齐到和结构体变量起始位置偏移量为0的地址处,对应0字节空间,第二个成员变量为s2,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,对齐数为4,起始位置要对齐4的整数倍,对应4~15字节空间,第三个成员变量为d,对齐数为8(双精度浮点数类型变量大小为8)和8(vs2022默认对齐数为8)的最小值,起始位置要对齐8的整数倍,对应16~23的字节空间。结构体总大小是最大对齐数的整数倍,S3结构体最大对齐数是8,要存入三个成员变量,至少需要24个字节,所以该结构体总大小为24个字节。如上图所示。

其实我们在划分内存的时候,有些内存空间会被我们浪费掉,可不可以避免掉呢,其实是可以避免一部分的,在接下来的学习中我回讲到。

上述三体都是根据对齐规则求出来的,大家要好好掌握。


2.2为什么回存在内存对齐

1. 平台原因 (移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。
总体来说:结构体的内存对齐是拿空间来换取时间的做法。

 那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:

//例如:
struct S1//8
{char c1;int i;char c2;
};struct S2//12
{char c1;char c2;int i;
};

 S1 S2 类型的成员⼀模⼀样,但我们让占有空间小的成员集中在一起,可以让结构体所占空间大小变小一点。


 2.3修改默认对齐数

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

 可以看到我们将默认对齐数改为1,结构体所占内存空间变为了6个字节,减少了很大一部分空间。

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


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

上面的print1和print2函数那个好一些?首选print2函数。

 原因:

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

4.结构体实现位段

4.1什么是位段

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

1. 位段的成员必须是 int unsigned int signed int ,在C99中位段成员的类型也可以
选择其他类型。
2. 位段的成员名后边有⼀个冒号和⼀个数字。

比如:

struct A
{int _a : 2;//代表该变量在内存空间中仅占2个bit位int _b : 5;//代表该变量在内存空间中仅占5个bit位int _c : 10;//代表该变量在内存空间中仅占10个bit位int _d : 30;//代表该变量在内存空间中仅占30个bit位
};

这就是一个典型的位段示例。它可以控制成员在内存中所占bit位的个数,那么他总共占的内存空间有多大呢?

位段的内存分配规则很大一部分取决于编译器,以作者所用的vs2022环境下举例,看以下代码:

struct S
{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;return 0;
}

从调试结果看,所占空间为3个字节。我们来分析过程:

a = 10,二进制为01010,而结构体位段规定a中只能存放3个bit位的数据,所以存010

b = 10,二进制为01100,而结构体位段规定a中只能存放4个bit位的数据,所以存1100

c = 10,二进制为00011,而结构体位段规定a中只能存放5个bit位的数据,所以存00011

a = 10,二进制为00100,而结构体位段规定a中只能存放4个bit位的数据,所以存0100

知道它们在内存中存的是什么之后,我们还有一个问题,它们是按什么顺序,什么规则存进去呢? 在vs2022环境下,在一个字节中,他要从高地址往低地址存放,第一个字节8个bit位存01100010,最高位之所以是0,是因为下一个数据占5个bit位的内存,1个bit位存不下,所以只能存放至下一个字节中(下同),第二个字节存00000011,第三个字节存00000100,最后在调试结果下从低地址到高地址分别为:0x62,0x03,0x04。

4.2位段的跨平台问题

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

2.位段中最大位的数目不能确定。(16位机器最⼤16,32位机器最⼤32,写成27,在16位机器会出问题。)

3.位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。(vs2022环境下,从右向左,也就是从高地址向低地址存)

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

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

4.3位段的应用 

下图是网络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要几个bit位就能描述,这里使用位段,能够实现想要的效果,也节省了空间,这样⽹络传输的数据报大小也会较小⼀些,对网络的畅通是有帮助的。

4.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);//这是错误的//正确的⽰范int b = 0;scanf("%d", &b);sa._b = b;return 0;
}

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

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

相关文章

C++学习:六个月从基础到就业——内存管理:new/delete操作符

C学习:六个月从基础到就业——内存管理:new/delete操作符 本文是我C学习之旅系列的第十七篇技术文章,也是第二阶段"C进阶特性"的第二篇,主要介绍C中动态内存管理的核心操作符——new和delete。查看完整系列目录了解更多…

15~30K,3年以上golang开发经验

继续分享最新的面经,前面发的两篇大家也可以看看: 「坐标上海,20K的面试强度」「北京七猫,薪资25~35K,瞧瞧面试强度」 今天分享的是golang开发岗面经,要求是3年以上golang开发经验,薪资为15~3…

Python爬虫实战:获取优志愿专业数据

一、引言 在信息爆炸的当下,数据成为推动各领域发展的关键因素。优志愿网站汇聚了丰富的专业数据,对于教育研究、职业规划等领域具有重要价值。然而,为保护自身数据和资源,许多网站设置了各类反爬机制。因此,如何高效、稳定地从优志愿网站获取计算机专业数据成为一个具有…

ArcPy工具箱制作(下)

在上一篇博客中,我们已经初步了解了如何制作ArcPy工具箱,包括工具箱的基本概念、准备工作、脚本编写以及将脚本转换为工具箱的步骤。今天,我们将继续深入探讨ArcPy工具箱的制作,重点介绍一些进阶技巧和优化方法. 一、优化工具箱的…

不一样的flag 1(迷宫题)

题目 做法 下载压缩包,解压,把解压后的文件拖进Exeinfo PE进行分析 32位,无壳 扔进IDA(32位),找到main,F5反编译 没啥关键词,ShiftF12也找不到什么有用的点 从上往下分析吧 puts(…

工程化实践:Flutter项目结构与规范

工程化实践:Flutter项目结构与规范 在Flutter项目开发中,良好的工程化实践对于提高开发效率、保证代码质量和团队协作至关重要。本文将从项目结构、代码规范、CI/CD流程搭建以及包管理等方面,详细介绍Flutter项目的工程化最佳实践。 项目结…

[Java · 初窥门径] Java 语言初识

🌟 想系统化学习 Java 编程?看看这个:[编程基础] Java 学习手册 0x01:Java 编程语言简介 Java 是一种高级计算机编程语言,它是由 Sun Microsystems 公司(已被 Oracle 公司收购)于 1995 年 5 …

1187. 【动态规划】竞赛总分

题目描述 学生在我们USACO的竞赛中的得分越多我们越高兴。我们试着设计我们的竞赛以便人们能尽可能的多得分。 现在要进行一次竞赛,总时间T固定,有若干类型可选择的题目,每种类型题目可选入的数量不限,每种类型题目有一个si(解答…

使用KeilAssistant代替keil的UI界面

目录 一、keil Assistant的优势和缺点 二、使用方法 (1)配置keil的路径 (2)导入并使用工程 (3)默认使用keil自带的ARM编译器而非GUN工具链 一、keil Assistant的优势和缺点 在日常学…

【React】通过 fetch 发起请求,设置 proxy 处理跨域

fetch 基本使用跨域处理 fetch 基本使用 在node使用原生ajax发请求:XMLHttpRequest()1.获取xhr对象 2.注册回调函数 3.设置参数,请求头 4.发起连接原生ajax没有带异步处理 promise;原生ajax封装一下,以便重复调用jQuery&#…

Redis(二) - Redis命令详解

文章目录 前言一、启动Redis并进入客户端1. 启动Redis2. 进入Redis客户端3. 使用IDEA连接Redis 二、查看命令帮助信息1. 查看所有命令2. 查看指定命令帮助 三、键操作命令1. set命令2. mset命令3. keys命令4. get命令5. mget命令6. dump命令7. exists命令8. type命令9. rename命…

【Qt】初识Qt(二)

目录 一、显示hello world1.1 图形化界面1.2 写代码 二、对象树三、使用输入框显示hello world四、使用按钮显示hello world 一、显示hello world 有两种方式实现hello world: 通过图形化界面,在界面上创建出一个控件,显示hello world通过写…

空调制冷量和功率有什么关系?

空调的制冷量和功率是衡量空调性能的两个核心参数,二者既有区别又紧密相关,以下是具体解析: 1. 基本定义 制冷量(Cooling Capacity)指空调在单位时间内从室内环境中移除的热量,单位为 瓦特(W) 或 千卡/小时(kcal/h)。它直接反映空调的制冷能力,数值越大,制冷效果越…

【prometheus+Grafana篇】Prometheus与Grafana:深入了解监控架构与数据可视化分析平台

💫《博主主页》:奈斯DB-CSDN博客 🔥《擅长领域》:擅长阿里云AnalyticDB for MySQL(分布式数据仓库)、Oracle、MySQL、Linux、prometheus监控;并对SQLserver、NoSQL(MongoDB)有了解 💖如果觉得文章对你有所帮…

基于n8n的AI应用工作流原理与技术解析

基于n8n的AI应用工作流原理与技术解析 在AI技术深度融入企业数字化转型的今天,开源工作流自动化工具n8n凭借其灵活的架构和强大的集成能力,成为构建智能自动化流程的核心引擎。本文将从技术原理、AI融合机制、典型应用场景三个维度,解析n8n在…

经济指标学习(二)

系列文章目录 文章目录 系列文章目录1、市净率**一、定义与计算****二、核心意义****三、应用场景****四、局限性****五、分类与衍生指标****总结** 2、市销率**一、定义与计算****二、核心意义****三、优缺点分析****四、适用场景****五、与其他指标的对比****六、实际应用案例…

大语言模型减少幻觉的常见方案

什么是大语言模型的幻觉 大语言模型的幻觉(Hallucination)是指模型在生成文本时,输出与输入无关、不符合事实、逻辑错误或完全虚构的内容。这种现象主要源于模型基于概率生成文本的本质,其目标是生成语法合理、上下文连贯的文本&…

CSS 美化页面(四)

一、浮动float属性 ‌属性值‌‌描述‌‌适用场景‌left元素向左浮动,腾出右侧空间供其他元素使用,其他内容会围绕在其右侧‌。横向排列元素(如导航菜单)、图文混排布局‌。right元素向右浮动,腾出左侧空间供其他元素使…

如何将 .txt 文件转换成 .md 文件

一、因为有些软件上传文件的时候需要 .md 文件,首先在文件所在的目录中,点击“查看”,然后勾选上“文件扩展名”,这个时候该目录下的所有文件都会显示其文件类型了。 二、这时直接对目标的 .txt 文件进行重命名,把后缀…

C++ 迭代器失效详解:如何避免 vector 操作中的陷阱

目录 1. 什么是迭代器失效? 2. 哪些操作会导致迭代器失效? 2.1 vector 的插入操作(push_back, insert) 示例:push_back 导致迭代器失效 如何避免? 2.2 vector 的删除操作(erase, pop_back&…