C语言(结构体)

      Hi~!这里是奋斗的小羊,很荣幸各位能阅读我的文章,诚请评论指点,欢迎欢迎~~     

                                                💥个人主页:小羊在奋斗

                                                💥所属专栏:C语言   

        本系列文章为个人学习笔记,在这里撰写成文一为巩固知识,二为一些学友们展示一下我的学习过程及理解。文笔、排版拙劣,望见谅。 

                                1、结构体类型的声明

                                2、结构体内存对齐

                                3、结构体传参

                                4、结构体实现位段

1、结构体类型的声明

        1.1结构体变量的创建和初始化

        其实之前在C语言(操作符)2中,我们已经比较详细地介绍过结构体变量的创建和初始化,这里再补充一个特殊的初始化方法——按照指定的顺序初始化。

        前面我们学到的初始化方法是按结构体成员的顺序初始化,就像下面这样:

        除了按顺序初始化,我们也可以按指定的顺序初始化:

        这两种初始化方法得到的效果是一样的。

        1.2结构体的特殊声明

        我们之前学过的结构声明常规形式是这样的:

         但在声明结构的时候,还可以不完全声明,就是省略掉自定义名。

        但是这种不完全的结构体声明必须在声明的同时直接创建变量,并且这个类型只能使用一次,也就是创建一次变量,但是一次可以创建多个。

        下面这个代码的问题在哪儿呢?用这个不完全的结构类型创建一个指针p,将p1的地址赋给p。

       当我们运行起来就会发现编译器报警告,说两个指针类型不兼容。

        这是因为我们创建的结构体类型是没有名字的,虽然两个成员一样,但编译器认为它们两个的地址类型是不一样的。

        1.3结构的自引用

        什么是结构的自引用呢?说白了就是结构自己引用自己,有点递归的意思。举个例子,当我们想将一个数据存到内存中时,可以按顺序存,也可以随机地存,只要能找到就行。那当我们随机存的时候,找到第一个数怎么找到第二个数就是一个问题。

        这时候我们可以定义一个结构体类型,有两个成员名,一个存数据,另一个存下一个数据的地址,这样的话当我们找到第一个数就能找到第二个数,以此类推。

        在结构体的自引用过程中,夹杂了 typedef 对不完全结构体类型声明的重命名,也容易引出问题。

        上面的代码是否正确呢?我们重命名了结构体类型名,并且在结构体成员中也用了重命名后的类型名。这样做是不对的,因为我们是要重定义这个结构体类型的类型名,上面的代码是在没有重定义之前就使用了,打破了顺序的问题。

2、结构体内存对齐

        2.1对齐现象 

        在介绍之前我们可以先猜一下结构体类型是怎么计算大小的呢?

       上面两个结构体类型成员变量相同,都是两个char类型和一个int类型,那两个结构体类型的大小会是6个字节吗?

         可以看到结果并不是我们猜测的,而且上面两个结构体类型只是改变了成员变量的顺序,它们的大小就发生了变化。那我们可以得到的结论是,结构体类型的大小并不是单纯的成员变量类型大小之和,而且结构体类型的大小还跟成员顺序有关系。这是为什么呢?

        其实,结构体的成员在内存中是存在对齐现象的。

               

        接着我们就来探讨一下上面两个结构体类型的大小为什么是8个字节和12个字节。假设上面是一块内存,一小格表示一个字节,第一个字节相较于起始位置偏移量为1,第二个字节相较于起始位置偏移量为2,以此类推,这就是偏移量的概念。

        用结构体类型 struct S1 创建一个结构体变量s,假设s从第0个字节开始,我们知道s的大小是8个字节,那其成员n、c1、c2分别在哪个位置呢?这里再介绍一个宏 offsetof ,它的作用是计算结构体成员相较于结构体变量起始位置的偏移量。

         可以看到n的偏移量为0,c1的偏移量为4,c2的偏移量为5。也就是说这三个结构体成员在内存是像下面这样存的。

        但是,这也不够8个字节啊,难道说结构体变量s即使用不到第6、7个字节但还是将这两个字节霸占了吗?是的,这两个字节就相当于浪费掉了。

        那用结构体类型 struct S2 创建的结构体变量所占的12个字节里n、c1、c2三个成员变量是存在哪些位置呢? 

        可以看到c2的偏移量为0,n的偏移量为4,c1的偏移量为8。也就是说这三个结构体成员在内存中是像下面这样存的。

        可以看到上面也没有12个字节,并且把第1、2、3、9、10、11个字节都浪费了。那这又是为什么呢?

        通过上面的内容我们可以得到的结论是,结构体成员在内存中存的时候并不是一个挨着一个存的,而是按一定的规则存储的,这个规则就是结构内存对齐。 

2.2对齐规则

        如果我们想要计算结构体类型的大小,就必须要先了解结构体的内存对齐,才能知道结构体类型在内存中究竟是如何开辟空间的。具体的对齐规则如下:

        (1)结构体的第一个成员(不管是什么类型)对齐到和结构体变量起始位置偏移量为0的地址处;

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

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

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

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

         了解了对齐规则,我们就来解释一下上面两个结构体类型的大小为什么是8个字节和12个字节。

        先看结构体类型 struct S1,根据规则(1),我们知道n存在第0、1、2、3这四个字节中。根据规则(2),VS默认对齐数是8,c1的大小为1小于默认对齐数,c1要对齐到1的整数倍的地址处,所以c1存到了第4个字节中;再看c2,c2的大小也是1小于默认对齐数,c2要对齐到1的整数倍的地址处,所以c2存到了第5个字节中。根据规则(3),结构体成员中最大对齐数为4,此时结构体成员所占6个字节,不是4的倍数,所以还要再额外占用两个字节的空间,那这两个字节的空间就被浪费掉了。所以最终这个结构体类型的大小就是8个字节。

        再看结构体类型 struct S2,根据规则(1),我们知道c2存在第0个字节中。根据规则(2),VS默认对齐数是8,n的大小为4小于默认对齐数,n要对齐到4的整数倍的地址处,所以n存到了第4、5、6、7个字节中,那第1、2、3个字节就被浪费掉了;再看c1,c1的大小是1小于默认对齐数,c1要对齐到1的整数倍的地址处,所以c1存到了第8个字节中。根据规则(3),结构体成员中最大对齐数为4,此时结构体成员所占9个字节,不是4的倍数,所以还要再额外占用三个字节的空间,那这三个字节的空间也被浪费掉了。所以最终这个结构体类型的大小就是12个字节。

        再来通过下面这个练习理解规则(4):

        可以看到结构体类型 struct S2 的大小是24个字节,三个结构体成员的偏移量分别为0、4和16。那么其成员变量在内存中的存储就应该是这样:

        其中结构体变量s的大小是8个字节,其结构体类型中成员变量最大对齐数为4,而对于结构体类型 struct S2 其成员变量中最大对齐数为8,所以最终结构体类型的大小就是24。 

        2.3为什么存在内存对齐

        通过上面的学习我们知道内存对齐的时候很容易就浪费了内存空间,那为什么还要存在内存对齐呢? 大部分的参考资料都是这样说的:

        (1)平台原因(移植原因)

        不是所有的硬件平台都能访问任意地址上的任意数据的,某些硬件平台只能在某些地址处取某些特定类型的数据,比如只能取int类型,那就只能访问4的倍数的内存空间,否则抛出硬件异常。

        (2)性能原因

        数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要做两次内存访问,而对齐的内存访问仅需要一次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或写了。否则,我们可能需要执行两次内存访问,因为对象可能被分在两个8字节内存块中。

        什么意思呢?假设创建一个结构体类型,其中成员变量为char类型的c和int类型的n。

        下面是不考虑对齐的情况,直接在c后面存n:

        下面是考虑对齐的情况:

 

        因为n为int类型,考虑对齐的话c后面三个字节的空间就浪费掉了。

        假设我们现在要用一个32位的机器去访问这个结构体的成员变量n,32位的机器一次能访问4个字节的内存,那在开始位置访问不考虑对齐的情况时需要访问两次才能读取完整的n,但是在访问考虑对齐的情况时只需要访问一次就行了,因为n前面刚好是4个字节可以跳过就没有必要访问前面的内存了,这样的话效率就会提高。

        总的来说:结构体的内存对齐是拿空间来换取时间的做法。

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

        有一个小技巧:让占用空间小的成员尽量集中在一起。就像前面我们创建的 struct S1 和 struct S2 一样,虽然两个成员一样,但是成员顺序不一样最终两个结构体类型的大小也就不一样了。

         2.4修改默认对齐数

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

        上面结构体类型 struct S 的大小在默认对齐数下是12个字节。

        当我们将默认对齐数改为1时,结构体类型 struct S 的大小就变成了6个字节。因为结构体成员c1、n、c2的大小分别为1、4、1,而默认对齐数是1的时候,其每个成员的对齐数都为1,就相当于没有对齐,是紧挨着存储的。

        默认对齐数一般修改的都是2的次方数。

3、结构体传参

        来看下面的代码:

#include <stdio.h>

struct S
{
	int arr[1000];
	char ch;
	int n;
};

void print1(struct S tmp)
{
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", tmp.arr[i]);
	}
	printf("\n");
	printf("ch = %c\n", tmp.ch);
	printf("n = %d\n", tmp.n);
}

void print2(struct S* tmp)
{
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", tmp->arr[i]);
	}
	printf("\n");
	printf("ch = %c\n", tmp->ch);
	printf("n = %d\n", tmp->n);
}

int main()
{
	struct S s = { {1,2,3,4,5,6,7,8,9,10}, 'a', 8 };
	print1(s);
	print2(&s);
	return 0;
}

        上面写了两个函数print1和print2打印结构体变量s的内容,print1用的是传值调用,print2用的是传址调用,哪个更好呢?

        答案是传址调用更好。传值调用时形参tmp是拷贝了一份结构体变量s,需要压栈,要开辟一块和s大小相等的内存空间,而且拷贝的过程也是需要时间的,所以时间和空间上都要消耗;而传址调用指针tmp只需要接收一个4个字节或8个字节的地址就行,并不需要额外开辟新的空间,并且还没有拷贝过程中时间的消耗。

        另外,传值调用能做到的传址调用都能做到,但是传址调用能做到的传值调用未必都能做到,比如间接修改内存中的值。要是我们不想指针指向的对象被修改也可以加上const修饰,这样就没什么后顾之忧了。

        所以,结构体传参的时候,要传结构体的地址。

4、结构体实现位段

        4.1什么是位段

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

      (1)位段的成员必须是 int、unsigned int 或 signed int,在c99中位段成员的类型也可以选择其他类型。

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

        来看结构体和位段声明的比较:

        位段中的位指的是二进制位也就是比特位,所以能想到的是位段中冒号后面的数字指的就是比特位,其中_a占2个比特位,_b占5个比特位,_c占10个比特位,_d占30个比特位。(成员名前面的_只是编程习惯没有特殊意思)。而int型大小是4个字节最大32位,所以不能超过这个数。

        为什么要有位段呢?

        以前我们在写代码的时候,有没有想过这样一个问题。就是说我们创建了一个整型变量_a,它占4个字节,但是这个_a我们只是想让它表示0、1、2、3这四个值,而这四个值二进制表示为00、01、10、11只需要2个比特位就行了,那我们还给它开辟一个32位的空间是不是太浪费了。同样的如果_b只需要5个比特位就够了,_c只需要10个比特位就够了,_d只需要30个比特位就够了,那我们就没有必要给它们都开辟4个字节的内存空间了。

        为了实现这种比较精准的内存大小的开辟,位段就出现了。我们只需要按它们的需求开辟相应大小的内存空间,就能避免很多不必要的浪费。我们用上面声明的结构体和位段来做个验证,结构体的大小是16个字节,位段是不是真的变小了呢?

        可以看到位段所占内存是结构体的一半。但是_a_b_c_d加起来一共是47个比特位,按道理来说6个字节就够了,为什么是8个字节呢?这跟位段的内存分配有关。

        4.2位段的内存分配

        (1)位段的成员可以是 int、unsigned int、signed int 或者是 char 类型;

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

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

        那位段到底是如何分配内存的呢?

        以上面这个位段为例,char类型一次开辟8个比特位,而a只需要占用3个比特位就行,但是这时候就有个问题,a是从左往右存呢还是从右往左存呢(左右或上下都是一样的,这里以左右为例),这是不确定的,不妨假设是从右向左存的。a存好占用了3个比特位,剩下的5个比特位还足够存b,存好b后只剩下1个比特位不够存c了,还需要再开辟8个比特位,那这时候又有个问题,剩下的那一个比特位是浪费掉呢还是存一部分c呢,这也是不确定的,不妨我们再假设不够存下一个数据的话就浪费掉。在开辟的第二个字节中存好c后还剩3个比特位不足以存d,还需要再开辟一个字节存d,按我们的假设需要开辟3个字节的空间

        开辟好内存空间后,就需要给相应的成员变量赋值,10的二进制表示是1010,但是a只有3个比特位,所以a中只存了010;12的二进制表示是1100,所以b中存了1100;3的二进制表示是11,所以c中存了00011;4的二进制表示是100,所以d中存了0100。

        如果真按如上我们假设的存储方式存储,那在内存中应该是下面这样:

         

        可以看到,在当前VS环境下,我们的分析是正确的。了解了位段的内存分配,我们再回到上面的问题,为什么下面这个位段是8个字节而不是6个字节。

        一个int类型是32位,存好_a_b_c需要17个比特位,剩下15个比特位显然不足以存_d,所以还需要再开辟4个字节的空间存_d,所以总共需要8个字节,而不是6个字节。

        4.3位段的跨平台问题

        (1)int 位段被当成有符号数还是无符号数是不确定的;

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

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

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

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

        4.4位段使用的注意事项

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

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

            如果觉得我的文章还不错,请点赞、收藏 + 关注支持一下,我会持续更新更好的文章。   

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

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

相关文章

面向长文本处理的键值缓存压缩技术:智能压缩,无损性能,免微调

随着输入长度的增加&#xff0c;大型语言模型&#xff08;LLMs&#xff09;中的键值&#xff08;KV&#xff09;缓存需要存储更多的上下文信息以维持性能&#xff0c;这导致内存消耗和计算时间急剧上升。KV缓存的增长对内存和时间效率的挑战主要表现在两个方面&#xff1a;一是…

【Python数据预处理系列】精通Pandas:数据清洗中的字符串分割技巧(例子:如何将籍贯列中的横线替换为省份和市区)

本文将深入探讨Pandas库在数据清洗中的应用&#xff0c;特别是字符串分割技巧。 在数据分析的预处理步骤中&#xff0c;有效地处理和准备原始数据是至关重要的一步。我们将通过具体示例&#xff0c;展示如何使用Pandas中的 .str.split() 函数来对数据集中的字符串进行分割&…

关于文件上传失败问题的排查思路

问题场景&#xff1a; 最近公司的app有很多用户反馈上传文件失败了。业务路径就是简单的app前端调用后端文件上传接口&#xff0c;所以发生上传失败的可能因素可能是&#xff1a;1、文件大小/文件类型等是否有问题&#xff0c;公司用的是七牛的文件服务器&#xff0c;对文件上…

纷享销客BI智能分析平台常见问题QA

Q1在驾驶舱中查看图表时&#xff0c;图表间有什么动态交互吗? A&#xff1a;驾驶舱支持图表本身下钻&#xff0c;图表间联动&#xff0c;并且支持图表下钻的同时联动&#xff0c;可以基于驾驶舱的这个功能&#xff0c;实现图表间的动态交互。 Q2基于客户主题创建的统计图&…

PSO-LSSVM-Adaboost分类模型,粒子群算法优化基于最小二乘支持向量机结合Adaboost的数据分类-附代码

PSO-LSSVM-Adaboost是一种结合PSO-LSSVM和AdaBoost两种机器学习技术的方法&#xff0c;旨在提升模型的性能和鲁棒性。具体来说&#xff0c;AdaBoost是一种集成学习方法&#xff0c;通过组合多个弱分类器来形成一个强分类器&#xff0c;每个分类器针对不同的数据集和特征进行训练…

人脸识别——OpenCV

人脸识别 创建窗口创建按钮设置字体定义标签用于显示图片选择并显示图片检测图片中的人脸退出程序返回主界面 创建窗口 导入tkinter库&#xff0c;创建窗口&#xff0c;设置窗口标题和窗口大小。 import tkinter as tkwin tk.Tk() win.title("人脸识别") win.geom…

大模型时代的具身智能系列专题(十)

Sergey Levine团队 Sergey Levine目前是UC Berkeley电气工程与计算机科学系的副教授&#xff0c;同时是RAIL(Robotic AI&Learning LabBAIR)实验室主任。除了在Berkeley的教职&#xff0c;Levine也是Google Brain的研究员&#xff0c;他也参与了Google知名的机器人大模型PA…

VMD-PSO-LSTM单维时序预测模型(单输入单输出)-附代码

VMD-PSO-LSTM单维时序预测模型&#xff08;单输入单输出&#xff09; 1&#xff09;首先对原始单维数据进行VMD分解&#xff0c;分解为K个模态分量和1个残差分量 2&#xff09;将各个模态分量输入模型&#xff0c;建立模型进行预测 3&#xff09;将各个预测结果相加得到最终…

MCU 的最佳存储方案 CS 创世 SD NAND

MCU 的最佳存储方案 CS 创世 SD NAND 【SD NAND】大家都知道 MCU 是一种 “麻雀” 虽小&#xff0c;却 “五脏俱全” 的主控。 大家都知道 MCU 是一种 “麻雀” 虽小&#xff0c;却 “五脏俱全” 的主控。它的应用领域非常广泛&#xff0c;小到手机手表&#xff0c;大到航空航…

【堡垒机小知识】堡垒机审计日志的定义以及作用概述

随着数字化进程的加速&#xff0c;企业对于IT信息系统的依赖程度不断加深&#xff0c;而保障IT系统的网络安全至关重要&#xff0c;因此不少企业纷纷购买了堡垒机。今天我们就来简单概述一下堡垒机审计日志的定义以及作用。 堡垒机审计日志定义 堡垒机审计日志是记录堡垒机上所…

xml 取值错误 #{} boolean 一直为 false

取值时 #{param.msgStatus} 一直是false&#xff0c;java代码里面显示true。 <select id"findPageOaReading" resultType"com.focusin.data.office.func.dto.ProcessMessageInfoDTO">select i.*, t.template_name procdefNamefrom process_message_…

VBA excel 表格将多行拆分成多个表格或 文件 或者合并 多个表格

excel 表格 拆分 合并 拆分工作表按行拆分为工作表工作表按行拆分为工作薄 合并操作步骤 拆分 为了将Excel中的数万行数据拆分成多个个每个固定行数的独立工作表&#xff0c;并且保留每个工作表的表头&#xff0c;你可以使用以下VBA脚本。这个脚本会复制表头到每个新的工作表&…

练习实践-linux启动耗时分析

练习实践-启动耗时整体概览&#xff0c;具体服务的启动细节 参考来源&#xff1a; B站up主林哥讲运维&#xff1a;一分钟学会&#xff1a;可视化查看系统启动时的性能 如何使用Linux命令查看系统的启动进程&#xff08;linux查看启动进程&#xff09; 解决ubuntu开机变慢&…

BitMart 宣布将销毁 264万枚 BMX,为何平台币掀起销毁热潮?

根据 BitMart 2024年4月18日发布的官方公告&#xff0c;BitMart 将于 5 个工作日内销毁 2,637,063 枚 BMX&#xff0c;价值约 94.9 万美元。根据 BitMart 白皮书中关于「回购机制」的规定&#xff0c;BitMart 2024 年第一季度平台手续费收入的 20% 将用于 BMX 的月度回购和销毁…

碳化硅MOSFET短路保护方法

碳化硅MOSFET短路保护方法 1.概述2.IGBT和碳化硅MOSFET器件特性3.短路保护方法比较4.总结 1.概述 碳化硅 (SiC) MOSFET 已成为硅 (Si) IGBT 的潜在替代产品&#xff0c;适用于光伏逆变器、车载和非车载电池充电器、牵引逆变器等各种应用。与 Si IGBT 相比&#xff0c;SiC MOSFE…

C语言基础:字符串函数使用和剖析(2)

strcmp&#xff08;字符串比较,比较两个字符串是否相等&#xff09; int strcmp ( const char * str1, const char * str2 ); int main() {const char* p1 "abcdef";const char* p2 "sqwer";if ("abcdef" "sqwer")//千万不能这么比…

nginx中配置ssl证书(宝塔面板)

首先申请一个SSL证书&#xff0c;这里我申请的joyssl的免费证书。提交订单申请后&#xff0c;按照页面提示在域名解析中将CNAME和记录值配置好。 比如我用的阿里云&#xff0c; 这是好后&#xff0c;需要等几分钟&#xff0c;然后域名检验成功。 然后点击joyssl的左侧菜单的“证…

grep、sed、awk

grep&#xff1a;文本过滤工具 sed: 文本编辑工具 awk: 格式化文本 grep -n 显示行号 -i 忽略大小写 -v 取反 -o 只保留关键消息 # 找出文件的空行 grep ^$ test.txt -n # 找出文件非空行内容 grep ^$ test.txt -n -v # 找出文件非空行内容&#xff0c;并且排除注释&#xff…

大创报名步骤

目录 一、注册 二、创建项目 三、报名 一、注册 进入注册/登录 点击 点击 填写个人信息 二、创建项目 找到解压的文件 随便选一个 项目简介在你选择的文件中截取一段 询问自己寝室的人 被邀请者需要在微信公众号上搜索 “全国大学生创业服务网” 选择我的消息中同意 三、报名…

Facebook商城号怎么做?思路与操作分析

2016 年&#xff0c;Facebook打造了同名平台 Facebook Marketplace。通过利用 Facebook 现有的庞大客户群&#xff0c;该平台取得了立竿见影的成功&#xff0c;每月访问量将超过 10 亿。对于个人卖家和小企业来说&#xff0c;Facebook Marketplace是一个不错的销货渠道&#xf…