C语言 自定义类型-结构体 #结构体类型的声明 #结构体的自引用 #结构体内存对齐 #结构体位段的实现

news2024/9/23 16:11:52

文章目录

前言

一、结构体类型的声明

1、结构体的基础知识

2、结构体的声明

3、特殊声明

二、结构体的自引用

三、结构体变量的定义和初始化

1、结构体的定义分为两类

2、结构体的初始化

四、结构体内存对齐

1、结构体在内存中是怎样存放的呢?

2、结构体的对齐规则:

3、为什么存在内存对齐?

4、注意

5、修改默认对齐数

五、结构体传参

六、结构体位段的实现

1、什么是位段

2、位段的大小

3、位段的内存分配

4、位段不跨平台的原因

总结


前言

路漫漫其修远兮,吾将上下而求索;


一、结构体类型的声明

1、结构体的基础知识

结构体是一些成员变量的集合,每个成员可以是不同类型的变量;

2、结构体的声明

可以在创建结构体类型的时候同时捎带着创建结构体变量,也可以在有了结构体类型之后再创建结构体变量;

3、特殊声明

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

例如:匿名结构体类型(即无结构体标签 tag

在匿名结构体中,即使有两个成员变量的类型、大小都一样的结构体类型,只要该结构体变量不是在同一个匿名结构体创建的变量,编译器会认为它们的类型不同;例子如下图:

二、结构体的自引用

在结构中包含一个类型为该结构本身的成员是否可以呢?

例子如下:

以上例子是不可行的,因为将struct Node 作为类型定义的成员又作为结构体类型struct Node 的成员,就好比”先有鸡还是先有蛋“的问题,是个无厘头问题;

在讲述结构的自引用之前,我们先来了解一下一些相关的基础知识;

如果你想在内存中存放数据 1 2 3 4 5,想让其像数组一样在内存中连续存放,这便是顺序表;即顺序表这种结构中的数据在内存中连续存放;

那如果数据不在内存中连续存放呢?

如上图所示,如果你找到了1,1可以找到2,2可以找到3,3可以找到4,4可以找到5,即找到1后面的数字就都可以找到了;这种方法就像一个链条,将这些数据串起来了,故而叫做链表

那么如何实现链表呢?

你可能会这样想,在1的位置包含1的数值和2的结点,1便可以找到2;在2的位置包好2的数值和3的结点,2便可以找到3;在3的位置包含3的数值和4的结点,3便可以找到4;在4的位置上包含4的数值和5的结点,4便可以找到5;

那么这个结点该如何设计呢?

呈现出来就是上图这个样子,无法利用sizeof(struct Node) 的大小,因为结构体类型中包含用结构体类型创建的变量的这种写法本身就存在问题;

所以就有人想到,让上一个结点包含下一个结点的地址;

写作以下方式:

struct Node
{
    int date;
    struct Node* next;
};

在结构体类型 struct Node中包含的结构体指针next 能找到自己同类型的一个结点,故而叫作自引用; 

匿名结构体是否可以自引用?显然也是可以的。

请判断以下代码是否正确?

typedef struct 
{
    int date;
    Node* next;

}Node;

答案:此代码是错误的。因为首先匿名结构体类型首先要存在,才能对此类型进行类型重命名,而其成员Node* next ; 中的类型不存在;--> 在创建结构体类型的时候其成员使用了利用typedef 重定义的类型,非常矛盾;(也是先有鸡还是先有蛋这种无厘头的问题)

解决方案:

typedef struct Node //将结构体类型重定义为 Node
{
    int date;
    struct Node* next; //结构体类型 struct Node 的指针类型

}Node;

其实上面的这个代码还可以这样写:

扩展:在实现链表的时候通常有这么一种写法;

三、结构体变量的定义和初始化

1、结构体的定义分为两类

一是声明其类型的时候创建结构体变量;二是在其类型声明好之后再创建结构体变量

2、结构体的初始化

在结构体变量创建的时候就给其一个初始值;然而在创建结构体变量的时候就有两种情况,当然,初始化时也有两种情况;

如若初始化嵌套结构体:

当然,也可以不按照顺序来初始化(一般人不会这么做,所以了解就行):

四、结构体内存对齐

1、结构体在内存中是怎样存放的呢?

我们先来看下面的例子:

在上图中,明明两个结构体类型的成员都一模一样,只是成员的顺序不同,两者创建的变量所占内存空间的大小就截然不同; 说明结构体在内存中的存储别有天地;即结构体并中的成员并不都是在内存中紧挨着存放的;

上述问题就涉及到了结构体的对齐规则;

2、结构体的对齐规则:

1、第一个成员总是存放在结构体变量申请的空间偏移量为0的地址处

2、其他成员变量(除第一个成员变量以外的成员变量)要对齐到其对齐数的整数倍偏移量的地址处;

对齐数的计算原则:对齐数 = 在该成员所占空间的大小编译器默认对齐数中间取一个较小值

注:在VS编译器中,其默认对齐数为8(默认对齐数可进行修改,文章下面会讲述);可以说,只有在VS编译器上有默认对齐数的概念,而在其他编译器上,没有默认对齐数的概念,那么在其他编译器上,其对齐数就是该数据自身的大小;

3、结构体的总大小为其成员的对齐数中最大对齐数的整数倍

4、如果是嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数 (该结构体最大对齐数就是其成员中对齐数最大的那个数) 的整数倍偏移量的地址处;而此嵌套结构体的结构体的整体大小是其成员对齐数中最大那个数的整数倍

分析规则:

以 struct S1 为例;(所画的内存图解假设其一个格子为一个内存单元)

1、偏移量:以字节为单位算当前内存单元与起始位置的偏移;

由于规则1中所述,第一个成员总是存放在结构体变量申请的空间偏移量为0的地址处;而c1 为char 类型,故而所占内存空间的大小为1byte ,如上图所示;

2、对齐数,计算如下:

按照结构体的对齐规则,成员c2 、成员 i (第一个成员以外的成员)需要存放的偏移量为其对齐数整数倍的地址处;如下图所示;

结构体变量s1 的总大小为在成员中最大对齐数的整数倍,即在成员c1、c2、i中,对齐数分别为1、1、4, 其中最大值为4,所以结构体变量 s1 的大小必然是4 的整数倍;而在上图中,s1 的三个成员占了8byte 的空间,是4的整数倍;所以 sizeof( s1 ); 的结果为 8 ;

为了验证每个成员是不是如上图所画一样存放在内存中,我们可以使用一个库函数 offsetof ,此函数具体使用如下:

  • size_t offsetof ( structName,memberName);
  • offsetof 可返回一个结构体成员在此结构体类型中创建时存放在为多少偏移量的地址处

可见,我们的分析是对的;

同理,我们分析一下 s2 的对齐;如下图;

看到此处,你可能会有疑问,嵌套结构体的结构体变量的对齐规则呢?它在内存中又是如何存放的?例子如下:

如果是嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数 (该结构体最大对齐数就是其成员中对齐数最大的那个数) 的整数倍偏移量的地址处;而此嵌套结构体的结构体的整体大小是其成员对齐数中最大那个数的整数倍

用库函数 offsetof 进行验证:

3、为什么存在内存对齐?

1、平台原因( 也称为移植原因)

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

基于此原因,所以在结构体变量中要将某些数对齐到能被取出的地址处,这样才能访问到我们想要访问的数据;

2、性能原因:

数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问存放在未对齐内存中的数据,处理器需要作两次内存访问才能将此数据读取完整;而对齐的内存访问仅仅只需要一次访问;--> 结构体变量的内存对齐减少了处理器访问内存空间的次数而提高了效率;

综上,结构体变量在内存中的存储存在内存对齐,会浪费空间,但是减少了处理器访问内存空间的次数,而提高了处理的效率;

注:我们常说的32位机器、63位机器指的是32位CPU、64位CPU;而其中32位和64位指的是CPU对应的字长字长决定了CPU在读写数据的时一次会读多大的内存空间

32 位的机器在处理数据的时候一次可以操作4 byte 即32 比特位 的数据;64位机器在处理数据的时候一次可以操作8byte 即64 比特位的数据;

显然如上图所示,存在结构体对齐规则,虽会浪费空间,但计算机在访问内存中的数据的时候,极大地提高了访问的效率;

4、注意

如果想在设计结构体的时候,既满足对齐,又要节省空间,那么就得让占用空间小的成员尽量集中在一起,以便利用好零碎的空间;

5、修改默认对齐数

细心的你,可能就发现了之前在头文件中有 #pragma 这个预处理指令,在此处我们可以使用这个指令来改变我们的默认对齐数;

注:#pargma once 一般在头文件中使用,以防止该头文件被多次引用

VS的默认编译器的对齐数为 8 :

利用 #pragma pack(4) 将默认对齐数修改为4:

注意:如上图所示,此处的 #pragma pack(4) 并不是只生效一次而是与 #pragma pack()  配合得以实现对一定范围的控制;下图便可以清楚地体会到:

上图的输出结果分析如下图所示:

修改默认对齐数的意义:

当你觉得此对齐数不合适的时候;例如,按照8byte 默认对齐数时浪费的空间太多的时候,便可以利用 #pragma pack(修改的对齐数)   将默认对齐数修改地小一点;

或者你不想注重效率而不想浪费空间,便可以将默认对齐数设置为1,即 #pragma pack(1);  

五、结构体传参

其实结构体传参与函数传参一样的道理,结构体变量作为一个参数,存在传值调用与传址调用;

在函数中,我们知道传值调用,形参是对实参的临时拷贝,改变形参并不会影响实参

传址调用,就是将该变量的地址传过去,形参接收实参的地址,改变形参实际上也会改变实参;

可见,无论时传值调用还是传址调用,均可以完成打印的任务;经过之前的学习,你一定能够感受到,传值调用能做到的,传址调用也能做到;但是传值调用不能做到的,传址调用还是可以做到;相较而言,传址调用的功能要更加强大;

实际上对于结构体传参来说,最好采用传址调用的形式;因为传值调用中形参是对实参的临时拷贝,倘若此结构体所占用的空间内存较大,那么就会消耗空间与时间来进行拷贝;

同样的,还可以利用函数栈帧的知识来解释,在函数传参时会有参数压栈,在上图中体现为,结构体变量s 传参给形参 ss 时,会在栈区开辟一块较大的空间来接收传过去的数据;而参数压栈存在对系统空间内存的开销,况且在拷贝数据时还需要时间,即在时间与空间上均有开销而导致其效率低下;

用更加专业的语术来讲就是,函数传参时,参数是需要压栈的,于是会有时间与空间上的开销;如若采用传值调用而此结构体过大,参数压栈时的系统开销会比较大,故而导致性能下降;

传址调用能达到传值调用带来的同样的效果且在效率上更为高效;故而对于结构体传参做好采用传址调用的形式;

六、结构体位段的实现

位段只能在结构体中使用;

1、什么是位段

位段的声明与结构体的声明是相似的,但存在两个不同点:

1、位段的成员必须是整型家族的,即为 char ,unsigned char , short , unsigned short , int , unsigned int , long , unsigned long…… 

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

例如:

struct A
{
    int a : 2; 
    int b : 25;
    int c : 15;
    int d : 4;

};

在变量后面有了冒号和数字就代表着此变量非结构体变量而是位段;

  • question1:其中的数字代表了什么呢?

位段中“位” 的意思是比特位的意思;以上为例即 a 只占了 2 bit , b 只占了 25 bit ,c 只占了 15 bit , d 只占了 4 bit ;

  •  question 2: int 类型的数据不是在内存空间中占4byte 即 32 bit 吗?那为什么 int a : 2 ; 意为 a 只占了 2 bit ?

此a 为整型确实在内存中占 32 bit ,但是现在 a 可能不需要这么多的比特位来存放其数据2,而只需要 2个比特位便已足够(因为十进制2写作二进制 :10 );故而 :2  的意思是为变量a分配 2 bit 的空间就可以了;

  • question 3: 为什么不需要那么多空间呢?

在实际写代码的过程中,一些变量可能并不需要很大的取值范围

例如,在结构体有一个成员 flag --> int flag ; 利用flag 来表示真假,那么此flag 的取值范围只有两个,一个为 0 一个为1;而若想要存放一个取值范围在0~1的变量并不需要32 bit 的空间来存放,仅需要1 bit 大小的空间即可;

所以在有一些成员的取值范围有限的情况下,存放这些成员变量的空间在一定程度上可以减小;

在结构体中,结构体成员在内存中存放时要遵循对齐原则,浪费了空间但是提高了数据读取的效率--> 用时间来换取空间;

位段就是一种节约空间的语法;

2、位段的大小

位段的大小是否就是比特的单纯相加?答案是否。

上图中的A是一个位段类型,位段A 的大小为 8byte ;

可能你会有疑惑,位段不是节省内存空间吗,为什么此处还会浪费空间?

实际上,在此处的 8byte 确实浪费了一定的空间,但是位段所指的节省空间是相较于不用位段时所占的内存空间,此处不用位段便会占用16 byte 的内存空间,用的位段便节约了 8byte 的空间;

question 1: 此 8yte 是怎么来的?

我们有必要了解一下位段的内存分配

3、位段的内存分配

规则:

  • 位段的成员必须是整型家族

  • 位段的空间是按照需要以 4byte 或者 1 byte 等的方式来开辟的(看其成员类型所占内存空间的大小);

很少会出现位段成员类型不同的位段类型;因为位段本身就是不稳定的存在,一般情况下位段的成员是统一类型的,如果将不同的类型放在一起就会使得此位段非常复杂反而更加不稳定;

  • 位段涉及很多不确定因素,位段是不跨平台的,注重可移植性的程序应该避免使用位段;但是倘若非要使用位段的方式来节省空间,那么便要在面对不同的平台,机器上写出相应的代码;

所以,在上例中,看到位段A 的成员的类型为均int 类型,于是乎便会先开辟4 byte 的空间,当内存空间不够了再开辟4 byte 的空间;

这里存在两个问题,

一是申请得到内存空间之后,是先使用低地址处的空间还是先使用高地址处的空间

二是,当已申请的空间不够放不下下一个变量的时候,向内存再次申请空间的时候,会先将未用完的空间使用完再使用新空间还是直接使用新空间

上图是假设先使用低地址中继续使用未使用完的空间以及直接使用新空间的情况;下图是先使用高地址空间的情况;

显然,这三种方式所得到的结果都是 8byte,例子不够典型;

接下来,我们再看一个例子;

显然,由上图我们可以得知,当内存不够时,位段中的成员会直接使用新空间,并不会继续使用未被利用的空间

注:计算机在开辟内存空间的时候时一次性开辟;此处为了便于理解故而分开分析;

经以上推到,我们大致可以得到位段存储在内存空间中,内存空间不够时多余的空间会被浪费掉;但是存储顺序仍然不确定,即到底是从低地址往高地址存储还是高地址往低地址存储;

我们可以调式看一下;

显然,数据在内存中存放的数据与先使用高地址处空间这一情形中的数据相吻合;

所以位段在内存中存储时,会先使用高地址处的空间而后使用地址处的空间,并且不会跨字节存储一个数据(当这个数据所占空间的大小小于1byte时);

4、位段不跨平台的原因

1、int 位段被当作有符号数还是无符号数是不确定的

当你位段中的成员为int 类型的时候,位段会将因 int 所开辟的4byte 的空间当成signed int 还是 unsigned int 这是不确定的(于是就无法判断该数据最高位为有效位还是符号位),C语言中并没有明确的规定,故而体现不可跨平台性;

2、位段中最大位的数目不能确定

(在16位机器上,最大位为16;在32位机器上,最大位为32;如果在32位机器上位段成员的大小为20,在32位机器上不会出问题,但是转移到16位机器上就会出问题)

3、位段中的成员时从左向右分配,还是从右向左分配,标准尚未定义

只不过在vs编译器下,是从右向左使用的;但是在其他编译器上就不清楚了,因为标准未定义;

4、当一个结构包含两个位段的时候,第二个位段成员较大无法放入第一个位段成员剩余的空间的时候,是舍弃还是继续利用未被使用的空间,这是不确定的;

综上,跟结构体相比,位段可以达到同样的效果,位段可以节省空间,但是却存在跨平台的问题;

看到这,你可能还会有疑问,既然位段不可跨平台,那么为什么位段还没有被淘汰掉?

位段这种语法的设计本身就具有不可跨平台的属性,但是并不意味着不能利用位段写出可跨平台的代码,只不过会稍微麻烦一些;只要针对不同的平台写出不同的代码便就可以了


总结

戳目录~

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

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

相关文章

数字卫星:「超实时仿真」实现处理器性能5倍提升

01.数字孪生in卫星 卫星及其附属系统在航天领域具有十分重要的意义。近年来,卫星产业发展迅猛,数字化、网络化、智能化、服务化转型升级需求日益增长。为进一步完善星务软件验证工作、获取在轨卫星模拟数据,“数字卫星”成为当下主流趋势。 …

考研数学快9月了才开始强化,《660》《880》哪本优先?

快9月了,如果你刚开始强化,那要抓点紧了! 很多使用660880这两本习题册的同学提问:《660》《880》哪本优先? 其实,不管先做那本,都一样,做题的目的是为了查缺补漏,而不是…

一起学Java(5)-[起步篇]教你掌握本协作项目中的Gralde相关配置文件(下)

接上篇《一起学Java(4)-java-all-in-one协作项目相关文件研究(Gradle篇-上)》,本文继续研究项目中Gradle相关文件。 全文详见个人独立博客:https://www.coderli.com/java-go-5-project-config-files-intro-gradle-two/ 一起学Ja…

ACCESS 手工注入实战 凡诺靶场

简介 Access数据库注入攻击是一种常见的网络安全,通过注入SQL代码来获取未授权的数据访问权限。这种攻击利用了应用程序与数据库之间的交互漏洞,攻击者通过输入特定的SQL代码片段来操纵数据库查询,从而绕过应用程序的安全机制,获取…

R 语言学习教程,从入门到精通,R 绘图饼图(22)

1、R 绘图 饼图 R 语言提供来大量的库来实现绘图功能。 饼图,或称饼状图,是一个划分为几个扇形的圆形统计图表,用于描述量、频率或百分比之间的相对关系。 R 语言使用 pie() 函数来实现饼图,语法格式如下: pie(x, la…

软件测试——IDEA2023配置assert断言

IDEA2023配置assert断言 vm options输入框 输入以下内容 -ea -Dfile.encodingUTF-8 点击APPLY OK

js 手写图片懒加载插件

一、目标 模仿vue-lazyload插件 只需将img标签的src属性名替换为自定义属性v-lazy,即可实现图片懒加载功能 例如: 二、实现 不清楚图片懒加载原理的参考我的上一篇博客 1.封装自定义插件:暴露一个对象,包含一个install方法 …

咸鱼之王手游内购修复无bug运营版联网架设+后台

今天给大家带来一款单机游戏的架设:咸鱼之王手游。 另外:本人承接各种游戏架设(单机联网) 本人为了学习和研究软件内含的设计思想和原理,带了架设教程仅供娱乐。 教程是本人亲自搭建成功的,绝对是完整可…

在Ubuntu 部署 Grafana且监控MySQL数据

一、安装 打开终端按顺序执行以下命令 1.添加 Grafana 的 APT 仓库: sudo apt-get install -y software-properties-common sudo add-apt-repository "deb https://packages.grafana.com/oss/deb stable main" 2.导入Grafana GPG key: wge…

国产光耦合器的应用和优势浅析

光耦合器,也称为光隔离器,是电子领域的关键元件,为系统的不同部分提供必要的电气隔离。在国内半导体行业,高性能光耦合器的开发已成为一个重点关注领域,因为它们广泛应用于各个领域,包括医疗设备、汽车电子…

安泰功率放大器怎么使用

功率放大器是电子电路中的重要组件,用于增加输入信号的幅度,以便驱动各种负载。在不同的应用中,功率放大器有不同的使用方法和技巧。下面安泰电子将介绍功率放大器的基本使用方法,以及一些常见的应用示例。 1.连接电源 首先&#…

创意无限:7 个顶级广告设计软件推荐

在竞争激烈的市场中,一款引人注目的平面广告设计能够为设计师带来显著的业绩提升和收益。然而,除了设计师的专业技能,设计软件的选择同样对广告的最终效果有着不可忽视的影响。本文将介绍几款经过作者亲自测试并极力推荐的平面广告设计软件&a…

安装 podman 与 podman-compose

文章目录 Github官网文档Podman 简介Podman 与 Docker 区别 Podman 安装下载安装(推荐)brew 安装(Mac) Podman 虚拟机虚拟机基础rootful 模式 Podman 镜像与容器安装 podman-composedocker-compose.yml Github https://github.co…

秋招突击——8/23——知识补充——反向代理和正向代理——负载均衡算法

文章目录 引言正文正向代理反向代理负载均衡 面试题1、nginx属于七层网络结构中的哪一层?2、Nginx有哪些负载均衡算法3、什么是反向代理?什么是正向代理? 总结 引言 一步一步把以前忘记的计算机网络知识再捡起来,重新整理一遍&am…

Linux线上安装遇到的一些问题

本文目录 一、基于Linux安装php8二、Linux线上连接数据库问题三、关于线上nginx报错问题 一、基于Linux安装php8 首先登录ubuntu系统后运行命令:sudo apt update 更新完之后,安装我们需要的软件: sudo apt install nginx 安装 mysql。运行命…

UneMeta创始人讲述自己在Web3+IP领域创业的心路历程

昨日,UneMeta创始人,Ann_tyrion在X分享了一篇推文,分享了自己在探索Web3与IP产业结合过程中的心路历程,她并没有像很多项目方那样一味的讲述宏大的叙事,而是字里行间透露出对这个行业的探索和不断给自己充实信念&#…

2024最新Python+PyCharm保姆级安装教程【附激活码】

PyCharm 是由捷克的 JetBrains 公司开发的一款强大的 Python 集成开发环境(IDE),它为 Python 开发者提供了一个全面的编程工具集,支持从代码编写到代码测试、调试和优化等各个环节 ,它支持代码自动完成、代码检查、实时…

多功能秒达工具箱全开源源码,可自部署且完全开源的中文工具箱

简介: 多功能秒达开源工具箱源码,,可自部署且完全开源的中文工具箱,永远的自由软件,轻量级运行,全平台支持(包括ARMv8),完全类似 GPT 的支持,与高效的 UI 高…

MDK报错 .error: unknown register name ‘msp‘ in asm

似乎只能安装 V5的编译器 才能解决,下面是安装链接 https://blog.csdn.net/u011436603/article/details/136419969 V5的编译器 CSDN免费下载链接: 以下办法解决不了 解决办法: 在MDK 桌面图标,右击打开安装位置,在:\K…

论文相关知识

一、论文注意事项一 1.选题原则 开拓性:前人没有专门研究过或虽已研究但尚无理想的结果,有待进一步探讨和研究,或是学术界有分歧,有必要深入研究探讨的问题; 创新性:硕士学位论文要有新的见解,博士学位论文要做出创…