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

C语言零基础第18讲:自定义类型—结构体

目录

1.结构体类型的声明

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

3.匿名结构体类型

4.结构体的自引用

5.结构体内存对齐

5.1 对齐规则

5.2 为什么要内存对齐?

5.3 修改默认对齐数

6.结构体传参

7.特殊的结构体:位段

7.1 什么是位段?

7.2 位段的内存分配

7.3 位段的跨平台问题

7.4 位段的应用

7.5 位段使用的注意事项


正文开始

1.结构体类型的声明

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

        结构体是集合,数组也是集合。结构体有成员,数组也有成员(即元素)。结构体和数组的区别在于,结构体的成员可以是不同的类型,数组的成员必须是同一类型的。

        我们来看看结构体的声明:

struct tag            //struct是关键字
{                     //tag是类型名,这个名字可以自己取   member-list;      //成员列表:可以有1个或多个成员  }variable-list;       //变量列表,可以省略 

        比如,描述一个学生:

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

3.匿名结构体类型

        我们再来看一种特殊的声明方式,匿名结构体类型:

        我们再看一种情况:

        所以,需要注意的是,匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使用一次

4.结构体的自引用

        在结构体中,包含一个类型为结构体本身的成员,是否可以呢?请看解析:

        如上,在结构体中,不能包含一个类型与该结构体相同的结构体

        那么,正确的自引用方式是什么呢?

        在介绍自引用之前,我们先简单提一下链表的概念。我们知道,数组中的各个元素,在内存中是连续的。而对于链表,各个数据在物理上并不是连续存储的,那它是怎么存储的呢?每个数据分为两个部分,其中一个部分用来存储数据本身,另一个部分用来存储下一个数据的地址,这样就可以通过一个元素找到下一个元素了。请看图示:

        我们来看一下,结构体自引用示例,代码如下:

        需要注意的是,在结构体自引用的过程中,如果夹杂了typedef对结构体类型的重命名,结构体内部的指针域,是不能使用重命名后的名字来声明成员的。请看解析:

5.结构体内存对齐

        计算结构体的大小,要涉及到结构体的内存对齐

5.1 对齐规则

        我们先通过一个代码来引入这个问题:

        如上,S1和S2两个结构体类型,成员变量只有顺序的差异,而计算出的结构体大小却不相同。这是对齐方式不同导致的结果。

        在这里,我们先给出对齐规则,然后再对这些规则进行理解:

  1. 结构体的第1个成员,对齐到结构体变量相对于起始位置偏移量为0的地址处,即结构体的首地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的偏移量处。
  3. 对齐数 :用编译器默认的一个对齐数,与该成员的大小作比较,谁更小,谁就是对齐数。VS中msvc默认为8,Linux中gcc没有默认的数,对齐数就是成员自身的大小。
  4. 结构体中每个成员都有对应的对齐数,取最大的那个对齐数,结构体总大小为这个最大对齐数的整数倍。
  5. 如果嵌套了结构体的情况,嵌套的结构体成员,对齐到自己成员中最大对齐数的整数倍处,结构体的整体大小是所有对齐数(含嵌套结构体中成员对应的对齐数)中最大对齐数的的整数倍。

       以S1为例,我们来看看图示:

        如上,c1放在偏移量为0处,c2放在偏移量为1处,n放在偏移量为4处。到底对不对呢?

        我们可以利用offsetof来进行检验。offsetof是一个宏,可以计算出结构体的成员,相较于结构体起始位置的偏移量,对应的头文件是stddef.h

        请看S1的检验:

        如上,结果和我们分析的是一样的,S1的大小也确实为8。

        我们再看看S2的情况:

        使用offsetof检验一下:

        如上,结果和我们分析的是一样的,S2的大小也确实为12。

        我们再来看看,嵌套结构体的情况:

        我们来看看图示:

5.2 为什么要内存对齐?

        大部分参考资料是这样说的:

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

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

2.性能原因:

        数据结构(尤其是栈),应该尽可能地在自然边界上对齐。原因在于,假设一个处理器,每次总是从内存中取出8个字节,这就意味着,地址一定是8的倍数。在这种情况下,若一个double类型的数据,它的地址是不对齐的:7、8、9、10、11、12、13、14,而处理器一次取出8个字节,就得先取0、1、2、3、4、5、6、7中的数据,再取8、9、10、11、12、13、14、15中的数据,这就需要访问2次内存了。如果地址是对齐的:8、9、10、11、12、13、14、15,处理器一次取8个,刚好仅需访问1次内存即可。

3.总结:

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

4.启示:

        那么,在设计结构体的时候,我们应该尽可能地,既满足对齐, 又节省空间。

        做法就是,让占用空间小的成员,尽量集中在一起:

5.3 修改默认对齐数

        前面提到过,VS的msvc中,默认对齐数是8,成员要和这个默认对齐数去做比较,谁更小,谁就是对齐数。

        那么,这个默认的对齐数,可以修改吗?当然是可以的。

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

6.结构体传参

        请看第一种传参方式:

        如上,结构体中date成员所占空间非常大,那么这个结构体所占的空间也很大了。如果将结构体变量传给函数,那么形参也会申请这样大一块空间,同时数据的拷贝也需要花时间。因此,结构体在传参的时候,选择传结构体变量,在空间和时间上都会很浪费

        请看第二种传参方式:

        如上,结构体传参的时候,选择传结构体的地址比较好

7.特殊的结构体:位段

        接下来,我们来讨论一下,结构体实现位段的能力。

7.1 什么是位段?

        我们可以把位段看作一种特殊的结构体

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

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

        我们来看一个例子:

        上面的A就是一个位段类型。

        可知,使用位段是可以节省空间的

        上面的结果显示,位段A的大小为8个字节。那么,位段是如何存储的呢?

7.2 位段的内存分配

  1. 常见的位段成员有:int、unsigned int、signed int或char等类型。
  2. 位段的空间是按照需要,以4个字节(int)或1个字节(char)的方式来开辟的。
  3. 位段涉及很多不确定因素,是不跨平台的,注重可移植性的程序应该避免使用位段。

        我们来看一个例子:

7.3 位段的跨平台问题

  1. int位段,被当成有符号数还是无符号数,是不确定的。
  2. 位段中最大位的数目不能确定。如16位机器最大16,写成30,就会出现问题。
  3. 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
  4. 对于位段中第n个成员和第n+1个成员,在内存中,如果第二个位段成员比较大, 无法放进第一个位段剩余的位时,是否舍弃剩余的位,是不确定的。

        总而言之,跟结构体相比,位段可以达到同样的效果,并且很好地节省空间,但是存在着跨平台的问题

7.4 位段的应用

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

7.5 位段使用的注意事项

        我们知道,内存中以字节为单位分配地址,一个字节内部的比特位,是没有地址的。

        位段中,有时候可能几个成员是放在同一个字节中的。这样的话,有些成员的起始位置,并不是某个字节的起始位置,这些成员是没有地址的。

        所以,不能对位段的成员使用取地址操作符(&),也就不能使用scanf()直接给位段的成员输入值,只能是先将输入放在一个变量中,然后赋值给位段的成员

        我们来看一个示例:


完结

        

http://www.lryc.cn/news/623355.html

相关文章:

  • 新手向:GitCode疑难问题诊疗
  • C语言:文件操作详解
  • 从 MySQL 5.7 迁移到 8.0:别让 SQL 文件 “坑” 了你
  • 双指针和codetop复习
  • 【LeetCode每日一题】
  • JavaWeb开发_Day14
  • 嵌入式 Linux LED 驱动开发实验
  • Proteus 入门教程
  • KingbaseES主备读写分离集群安装教程
  • 通配符 重定向 管道符
  • 心路历程-三个了解敲开linux的大门
  • 高等数学 8.4 空间直线及其方程
  • 机器学习 [白板推导](十二)[卡曼滤波、粒子滤波]
  • Python语言---OrangePi全志H616
  • CPP多线程1:C++11的std::thread
  • Spring AI 进阶之路01:三步将 AI 整合进 Spring Boot
  • linux设备驱动之字符设备驱动
  • 链式二叉树的基本操作——遍历
  • 【论文笔记】Multi-Agent Based Character Simulation for Story Writing
  • 同创物流学习记录2·电车
  • 聊聊智慧这个东西之三:从食物的毒性、偏性聊起
  • 探秘gRPC——gRPC原理详解
  • [优选算法专题二滑动窗口——最大连续1的个数 III]
  • implement libwhich for Windows
  • Azure AI Search 探索总结
  • 软考 系统架构设计师系列知识点之杂项集萃(124)
  • [Responsive theme color] 动态主题 | 色彩工具函数 | HEX与RGB
  • OpenStack Neutron中的L2 Agent与L3 Agent:新手友好指南
  • SpringSecurity(一)入门
  • DAY12DAY13-新世纪DL(Deeplearning/深度学习)战士:破(改善神经网络)1