自定义类型:结构体(自引用、内存对齐、位段(位域))

目录

一. 结构体类型的声明和定义

1.1结构体相关概念

1.11结构的声明

1.12成员列表

1.2定义结构体类型变量的方法

1.21先声明结构体类型再定义变量名

​​​​1.22在声明类型的同时定义变量

1.23直接定义结构类型变量

二、结构体变量的创建、初始化​和访问

2.1结构体成员的直接访问​

2.2结构体成员的间接访问​

2.3匿名的结构体类型

     

三、结构的自引用

 自引用的使用案例:

四、计算结构体的大小(结构体内存对齐)

4.1计算结构体大小示例:

4.2偏移量计算的示例:

4.3嵌套结构体的计算:

4.4为什么存在内存对齐?​

4.5修改默认对齐数​

五、结构体传参

六、结构体位段(位域)实现

6.1位段(位域)的介绍

6.2内存分配

6.3位段的跨平台问题​

6.4位段的应用​

6.5位段使用的注意事项​


一. 结构体类型的声明和定义

在实际问题时,有时候我们需要其中的几种一起来修饰某个变量,例如一个学生的信息就需要成绩(整型),姓名(字符串),年龄(整型)等等,这些数据类型都不同但是他们又是表示一个整体,要存在联系,那么我们就需要一个新的数据类型,结构体。

(数组是一组相同类型的元素集合)

1.1结构体相关概念

1.11结构的声明

结构体由不同类型的数据组合成一个整体,以便引用,这些组合在一个整体中的数据是互相联系的。

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

声明一个结构休类型的一般形式如下:

struct 结构体名
{成员列表};

1.12成员列表

成员列表称为域表,第一个成员也称为结构体中的一个域。成员名定名规则写变量名同。

成员列表:

类型名 成员名;

1.2定义结构体类型变量的方法

1.21先声明结构体类型再定义变量名

struct student{  
    成员表列
}student1, student2 //结构体变量名

​​​​1.22在声明类型的同时定义变量

struct 结构体名
{
    成员表列
}变量名表列;

1.23直接定义结构类型变量

struct
{
    成员表列
}变量名表列;

二、结构体变量的创建、初始化​和访问

2.1结构体成员的直接访问​


结构体成员的直接访问是通过点操作符(.)访问的。点操作符接受两个操作数
使用方式:结构体变量.成员名

int main()
{
 //按照结构体成员的顺序初始化​
 struct Stu s = { "张三", 20, "男", "20230818001" };
 printf("name: %s\n", s.name);
 printf("age : %d\n", s.age);
 printf("sex : %s\n", s.sex);
 printf("id : %s\n", s.id);
 
 //按照指定的顺序初始化​
 struct Stu s2 = { .age = 18, .name = "lisi", .id = "20230818002", .sex = "女")
 printf("name: %s\n", s2.name);
 printf("age : %d\n", s2.age);
 printf("sex : %s\n", s2.sex);
 printf("id : %s\n", s2.id);
 return 0;
}

2.2结构体成员的间接访问​

有时候我们得到的不是一个结构体变量,而是得到了一个指向结构体的指针。
使用方式:结构体指针->成员名

struct Point
{
    int x;
    int y;
};
int main()
{
    struct Point p = { 3, 4 };
    struct Point* ptr = &p;
    ptr->x = 1;
    ptr->y = 2;
    printf("x = %d y = %d\n", ptr->x, ptr->y);
    return 0;
}

2.3匿名的结构体类型

匿名结构体类型,也称为未命名结构体,是指在定义结构体成员时省略了结构体的名字,直接定义其成员。由于没有名称,因此不会创建它们的直接对象(或变量),通常我们在嵌套结构或联合中使用它们。匿名结构体类型的作用域仅限于包含它的联合体,它不能在其他地方被引用。

//匿名结构体类型
//只能使用一次
struct
{int a;char b;float c;
}x;
struct
{int a;char b;float c;
}a[20], * p;

问:在上面代码的基础上,下面的代码合法吗?​

编译器会把上面的两个声明当成完全不同的两个类型,所以是非法的。

匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使用一次。

   .

     

三、结构的自引用

注:结构体自引用方式里面必须包含同类型的结构体指针

在结构中包含一个类型为该结构本身的成员是否可以呢?
比如,定义一个链表的节点:

如果这样编写

struct Node
{
 int data;
 struct Node next;
};

sizeof(struct Node) 是多少?

仔细分析,其实是不行的,因为一个结构体中再包含一个同类型的结构体变量,这样结构体变量的大
小就会无穷的大,是不合理的。

正确的自引用方式:

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

 是否可以使用匿名结构体呢?

在结构体自引用使用的过程中,夹杂了 typedef 对匿名结构体类型重命名,也容易引入问题,看看
下面的代码,可行吗?
typedef struct
{
    int data;
    Node* next;
}Node;
答案是不行的,因为Node是对前面的匿名结构体类型的重命名产生的,但是在匿名结构体内部提前使用Node类型来创建成员变量,这是不行的

解决方案如下:定义结构体不要使用匿名结构体了

   
typedef struct Node
{
    int data;
    struct Node* next;
}Node;

 自引用的使用案例:

链表:

在链表中,每个节点都包含数据和指向下一个节点的指针,这个指针就是自引用,它指向下一个相同类型的节点。这种结构可以使得链表在内存中灵活存储,并且不要求数据元素连续存放,从而大大提高存储器的使用效率。

四、计算结构体的大小(结构体内存对齐)

对齐规则​:

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

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

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

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

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

问:为什么结构体中的成员相同但占用的空间不同?

答:因为要对齐。

4.1计算结构体大小示例:

 

因为其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处,所以有浪费的内存空间。

                                       对齐是从对齐数的倍数地址开始

在上述结构体S1和S2中,c1和c2都为char类型,占一个字节,对齐数为1。

i为整型,占四个字节,对齐数为4个字节,从4的倍数的地址开始对齐。

所以此处S1、S2大小分别为8、12个字节。 

struct S1
{char c1;char c2;int i;
};struct S2
{char c1;int i;char c2;
};int main()
{struct S1 s1 = { 0 };//8struct S2 s2 = { 0 };//12printf("%d\n", sizeof(struct S1));//8printf("%d\n", sizeof(struct S2));//12return 0;
}

此处也有一个偏移量的概念,即相对于起始位置偏移的数值。

4.2偏移量计算的示例:

struct Example {  char a;      // 1字节  int b;       // 4字节  short c;     // 2字节  double d;    // 8字节  
};

4.3嵌套结构体的计算:

计算时把嵌套中的结构体作为一个数据类型去计算,最大对齐数也要与其比较,故有时候会出现最大对齐数为3,5的情况(结构体中包含一个char数组,数组大小为奇数)

在上述结构体S3和S4中,c和c1都为char类型,占一个字节,对齐数为1。

i为整型,占四个字节,对齐数为4个字节,从4的倍数的地址开始对齐。

d为双精度浮点型,占8个字节,对齐数为8个字节,从8的倍数的地址开始对齐。

S3为结构体,占16个字节,但最大对齐数为8个字节,所以从8的倍数开始对齐。

所以S3占16个字符,S4占32个字符。

   

 .

4.4为什么存在内存对齐?​

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

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

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

4.5修改默认对齐数​

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

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

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

五、结构体传参

  • 值传递: 在值传递中,结构体的副本被传递给函数。这意味着函数内部对结构体所做的任何修改都不会影响到原始的结构体。这种传递方式适用于小型结构体,因为结构体的副本需要占用额外的内存空间。

  • 指针传递: 在指针传递中,结构体的地址被传递给函数,函数内部通过使用指针来访问和修改结构体的内容。这种方式可以避免结构体的副本创建,因此对于大型结构体更为高效。同时,函数内部对结构体的修改会影响到原始的结构体。

// 定义一个结构体 S,包含一个整数数组 data 和一个整数 num  
struct S  
{  int data[1000]; // data 是一个可以存储1000个整数的数组  int num;       // num 是一个整数  
};  // 通过值传递的方式打印结构体的内容  
void printf1(struct S t) // 通过值传递接收一个 S 类型的结构体  
{  printf("%d %d\n", t.data[0], t.num); // 打印结构体的 data 数组的第一个元素和 num 的值  
}  // 通过指针传递的方式打印结构体的内容  
void printf2(struct S* ps) // 通过指针传递接收一个 S 类型的结构体的指针  
{  printf("%d %d\n", ps->data[0], ps->num); // 使用指针访问并打印结构体的 data 数组的第一个元素和 num 的值  
}  int main() // 主函数  
{  // 初始化一个 S 类型的结构体 s,并为其 data 数组和 num 赋值 struct S s = { {1,2,3,4,5},100 }; // data 数组初始化为 {1,2,3,4,5},num 初始化为 100  printf1(s);  // 通过值传递调用 printf1 函数,打印 s 的内容  printf2(&s); // 通过指针传递调用 printf2 函数,打印 s 的内容(取 s 的地址传递给函数)  return 0; // 主函数返回 0,表示程序正常结束  
}

问:上面的 print1 和 print2 函数哪个好些?
答:首选print2函数。​

原因:

  • 函数传参的时候,参数是需要压栈,会有时间和空间上的损耗。

  • 如果传递一个结构体对象的时候,结构体过大,参数压栈的的损耗比较大,所以可能导致性能的下降。

六、结构体位段(位域)实现

6.1位段(位域)的介绍

(有些资料里称为“位段”,也有的称为“位域”)

C 语言的位域(bit-field)是一种特殊的结构体成员,允许我们按位对成员进行定义,指定其占用的位数。

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

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

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

以下是VS 中的实例:

struct A
{int _a : 2;int _b : 5;int _c : 10;int _d : 30;// 2+5+10+30 = 47bit
};
//变量名
//1.字母,数字,下划线
//2.不能是数字开头int main()
{printf("%d\n", sizeof(struct A));//8 - 64bit
}

此处为什么struct A是64bit而不是48bit呢?

这就涉及到位段的内存分配了!

6.2内存分配

  • 位段的成员可以是 int unsigned int signed int 或者是 char 等类型。

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

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

以下是VS 中的实例:

struct S
{char a : 3;char b : 4;char c : 5;char d : 4;
}s;int main()
{struct S s = { 0 };s.a = 10;s.b = 12;s.c = 3;s.d = 4;printf("%d\n", sizeof(s)); //3
}

    int _a : 2;    //8bit
    int _b : 5;    //8bit
    int _c : 10;  //8bit
    int _d : 30;  //8bit

所以对于6.1中的实例,占用内存上为8bit+8bit+16bit+32bit = 64bit。

6.3位段的跨平台问题​

  • int 位段被当成有符号数还是无符号数是不确定的。

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

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

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

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

6.4位段的应用​

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

6.5位段使用的注意事项​

位段的几个成员共有同一个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位
置处是没有地址的。内存中每个字节分配一个地址,一个字节内部的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.rhkb.cn/news/208514.html

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

相关文章

医疗器械设备模组的具体应用

直线模组是一种高精度、高速度的精密传动元件,目前被广泛应用在各种工业自动化领域;尤其是在激光加工、电子制造、医疗设备、物流设备和机器人等行业中,都发挥着重要作用,接下来我们看看医疗器械设备模组的具体应用吧!…

avue-crud中时间范围选择默认应该是0点却变成了12点

文章目录 一、问题二、解决三、最后 一、问题 在avue-crud中时间范围选择&#xff0c;正常默认应该是0点&#xff0c;但是不知道怎么的了&#xff0c;选完之后就是一直是12点。具体问题如下动图所示&#xff1a; <template><avue-crud :option"option" /&g…

Elasticsearch 的使用

一、简介 1.Shard&#xff08;分片&#xff09; 数据分散集群的架构模式&#xff0c;Elasticsearch 将一个 Index&#xff08;索引&#xff09;中的数据切为多个 Shard&#xff08;分片&#xff09;&#xff0c;分布在不同服务器节点上。 默认每个索引会分配5个主分片和1个副本…

Discuz论坛自动采集发布软件

随着网络时代的不断发展&#xff0c;Discuz论坛作为一个具有广泛用户基础的开源论坛系统&#xff0c;其采集全网文章的技术也日益受到关注。在这篇文章中&#xff0c;我们将专心分享通过输入关键词实现Discuz论坛的全网文章采集&#xff0c;同时探讨采集过程中伪原创的发布方法…

Linux中文件的打包压缩、解压,下载到本地——zip,tar指令等

目录 1 .zip后缀名&#xff1a; 1.1 zip指令 1.2 unzip指令 2 .tar后缀名 3. sz 指令 4. rz 指令 5. scp指令 1 .zip后缀名&#xff1a; 1.1 zip指令 语法&#xff1a;zip [namefile.zip] [namefile]... 功能&#xff1a;将目录或者文件压缩成zip格式 常用选项&#xff1a…

CSS BFC特性和应用

目录 1&#xff0c;介绍2&#xff0c;BFC布局规则3&#xff0c;创建BFC4&#xff0c;BFC应用1&#xff0c;浮动子元素使父级高度坍塌2&#xff0c;非浮动元素被浮动元素覆盖3&#xff0c;margin 合并1&#xff0c;父子 margin 合并&#xff1a;父级和第1个/最后1个子元素2&…

Unity中Shader指令优化(编译后指令解析)

文章目录 前言一、我们先创建一个简单的Shader二、编译这个Shader&#xff0c;并且打开1、编译后注意事项2、编译平台 和 编译指令数3、顶点着色器用到的信息4、顶点着色器计算的核心部分5、片元着色器用到的信息6、片元着色器核心部分 前言 我们先读懂Shader编译后代码&#…

centos上安装并持久化配置LVS

1 实验背景 1&#xff09;系统版本&#xff1a;centos7.8 2&#xff09;虚拟机&#xff1a;3个centos虚拟机&#xff0c;&#xff08;其中一个做Director Server,另外两个做Real Server) 3) LVS大致有NAT ,DR ,Tun这三种模式&#xff0c;这里搭建一个典型的DR模式的LVS Direc…

ftp的服务安装配置

安装 yum install -y vsftpd # 是否安装成功 rpm -qa | grep vsftpd # 是否开机启动 systemctl list-unit-files | grep vsftpd # 开机启动 systemctl enable vsftpd.service # ftp端口 netstat -antup | grep ftp # 状态 service vsftpd status service vsftpd start service…

flink源码分析之功能组件(四)-slot管理组件II

简介 本系列是flink源码分析的第二个系列&#xff0c;上一个《flink源码分析之集群与资源》分析集群与资源&#xff0c;本系列分析功能组件&#xff0c;kubeclient&#xff0c;rpc&#xff0c;心跳&#xff0c;高可用&#xff0c;slotpool&#xff0c;rest&#xff0c;metrics&…

hexo博客部署到云服务器

欢迎大家到我的博客浏览。hexo博客部署到云服务器 | YinKais Blog 这篇文章带大家将hexo博客部署到云服务器上&#xff01; 一、服务器环境安装 1、安装 node js yum install gcc-c make yum -y install nodejs yum -y install npm 验证 node -v npm -v 2、安装git、ngin…

同旺科技 USB TO SPI / I2C --- 调试W5500_Ping测试

所需设备&#xff1a; 内附链接 1、USB转SPI_I2C适配器(专业版); 首先&#xff0c;连接W5500模块与同旺科技USB TO SPI / I2C适配器&#xff0c;如下图&#xff1a; 设置寄存器&#xff1a; SHAR&#xff08;源MAC地址寄存器&#xff09;&#xff0c;该寄存器用来设置源MAC…

使用java批量生成Xshell session(*.xsh)文件

背景 工作中需要管理多套环境, 有时需要同时登陆多个节点, 且每个环境用户名密码都一样, 因此需要一个方案来解决动态的批量登录问题. XShell Xshell有session管理功能: 提供了包括记住登录主机、用户名、密码及登录时执行命令或脚本(js,py,vbs)的功能 session被存储在xsh文…

[二分查找]LeetCode2009 :使数组连续的最少操作数

本文涉及的基础知识点 二分查找算法合集 作者推荐 动态规划LeetCode2552&#xff1a;优化了6版的1324模式 题目 给你一个整数数组 nums 。每一次操作中&#xff0c;你可以将 nums 中 任意 一个元素替换成 任意 整数。 如果 nums 满足以下条件&#xff0c;那么它是 连续的 …

勒索解密后oracle无法启动故障处理----惜分飞

客户linux平台被勒索病毒加密,其中有oracle数据库.客户联系黑客进行解密【勒索解密oracle失败】,但是数据库无法正常启动,dbv检查数据库文件报错 [oraclehisdb ~]$ dbv filesystem01.dbf DBVERIFY: Release 11.2.0.1.0 - Production on 星期一 11月 27 21:49:17 2023 Copyrig…

详解Spring中基于注解的Aop编程以及Spring对于JDK和CGLIB代理方式的切换

&#x1f609;&#x1f609; 学习交流群&#xff1a; ✅✅1&#xff1a;这是孙哥suns给大家的福利&#xff01; ✨✨2&#xff1a;我们免费分享Netty、Dubbo、k8s、Mybatis、Spring...应用和源码级别的视频资料 &#x1f96d;&#x1f96d;3&#xff1a;QQ群&#xff1a;583783…

《管家婆》辉煌2005+(V4.0)简单教程

《管家婆》辉煌2005&#xff08;V4.0&#xff09;简单教程 呉師傅 运行环境&#xff1a;   操作系统推荐使用Win2000&#xff08;32位&#xff09;、WinXP&#xff08;32位&#xff09;、Win7&#xff08;位&#xff09; 兼容&#xff1a;Win7&#xff08;64位&#xff09…

机器学习笔记 - 异常检测之OneClass SVM算法简述

一、异常检测是什么? 如下图,理想中我们可以找到一个框住大部分正常样本的决策边界,而在边界外部的数据点(蓝点)即视为异常。 但实际情况下数据都没有标签,因此很难定义正常还是不正常。异常检测的主要挑战如下:正常与异常行为之间的界限往往并不明确、不同的应…

一文搞懂设计模式之责任链模式

大家好&#xff0c;我是晴天。我们又见面了&#xff0c;本周我们继续学习设计模式&#xff0c;本周将同大家一起学习责任链模式。 场景引入 我们回想一下自己曾经经历过的考学场景&#xff0c;我们是如何一步一步考上大学的&#xff08;为了简化过程只提取核心环节&#xff09…

Fiddler抓包工具之fiddler的命令行窗口用法

fiddler的命令行窗口的相关命令 在fiddler官网有QuickExec使用教程&#xff0c;地址是QuickExec Reference - Fiddler Classic QuickExec 命令行窗口位于fiddler左下角黑色输入框&#xff0c;如下图&#xff1a; 按ALT Q&#xff0c;将焦点快速设置到QuickExec框。如果Fiddl…