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

目录

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

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.mzph.cn/news/192182.shtml

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

相关文章

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

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

Java Web——动态Web开发核心-Servlet

1. 官方文档 官方文档地址:Overview (Servlet 4.0 API Documentation - Apache Tomcat 9.0.83) servlet 与 Tomcat 的关系:Tomcat 支持 Servlet Tomcat 是一个开源的 Java 服务器,它主要用来提供 Web 服务,包括 HTTP 请求和响应…

EasyExcel写入多个sheet

直接上代码&#xff1a; public static void main(String[] args) {// 设置excel工作簿ExcelWriter excelWriter EasyExcel.write("F:\\excel\\a.xls").build();List<User> userList new ArrayList<>();userList.add(new User("lisi", "…

初始数据结构(加深对旋转的理解)

力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台备战技术面试&#xff1f;力扣提供海量技术面试资源&#xff0c;帮助你高效提升编程技能&#xff0c;轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/rotate-array/submissions/ 与字…

11.1每日一题(关于函数定义域)

f(x1)&#xff1a;自变量为x&#xff0c;x1为中间变量&#xff0c;所以f(x1)的定义域的取值范围是x的取值范围 f(x)&#xff1a;自变量为x&#xff0c;f(x)的定义域等价于f(x1)中 x1整体的定义域

深度学习记录--logistic回归损失函数向量化实现

前言 再次明确向量化的目的&#xff1a;减少for循环的使用&#xff0c;以更少的代码量和更快的速度来实现程序 正向传播的向量化 对于,用向量化来实现&#xff0c;只需要就可以完成&#xff0c;其中,, ps.这里b只是一个常数&#xff0c;但是依然可以加在每个向量里(python的…

09-命令者模式-C语言实现

命令者模式是一个高内聚的模式&#xff0c; 其定义为&#xff1a; Encapsulate a request as an object,thereby letting you parameterize clients with different requests,queue or log requests,and support undoable operations.&#xff08;将一个请求封装成一个对象&…

JDK17的安装与配置

JDK17的安装与配置 下载地址安装步骤配置环境变量验证安装是否成功 下载地址 此jdk17安装的系统是win10系统 https://www.oracle.com/java/technologies/downloads/ 这里选择JDK17进行下载 下载完成之后&#xff0c;显示如下图&#xff1a; 安装步骤 自定义的安装路径&…

智能优化算法应用:基于探路者算法无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于探路者算法无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于探路者算法无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.探路者算法4.实验参数设定5.算法结果6.参考文献7.…

动态规划------方法汇总

核心&#xff1a; 状态定义 状态转移方程 启发思路&#xff08;两种情况&#xff09;&#xff1a;选 或 不选 / 选哪个 DP三步&#xff1a;先写回溯&#xff0c;时间复杂度 指数级别&#xff1b;递归的过程中会重复计算&#xff0c;要保存计算结果&#xff0c;递归搜索…

Windows微软常用运行库合集2023

微软常用运行库合集适用于Windows系统的运行库合集包&#xff0c;基于微软官方的运行库而制作的&#xff0c;包括了常用的vb&#xff0c;vc2005/2008/2010/2012/2013/2017/2019/2005-2022&#xff0c;Microsoft Universal C Runtime&#xff0c;VS 2010 Tools For Office Runti…

Programming Contest 2023(AtCoder Beginner Contest 331)D题 Tile Pattern --- 题解

目录 D - Tile Pattern 题目大意&#xff1a; 思路&#xff1a; 代码&#xff1a; D - Tile Pattern D - Tile Pattern (atcoder.jp) 题目大意&#xff1a; 给你一个n和q&#xff0c;n为局部棋盘大小(n*n) 并且给出局部棋盘中黑白子位置的放置情况&#xff0c;q为查询次数…

某60区块链安全之Create2实战一学习记录

区块链安全 文章目录 区块链安全Create2实战一实验目的实验环境实验工具实验原理实验内容Create2实战一 实验步骤分析合约源代码漏洞Create2实战一 实验目的 学会使用python3的web3模块 学会分析以太坊智能合约Create2引发的漏洞及其利用 找到合约漏洞进行分析并形成利用 实…

淘宝/天猫商品详情API接口丨京东商品详情丨1688商品详情丨接口key密钥获取方式

要获取淘宝/天猫商品详情API接口、京东商品详情API接口、1688商品详情API接口以及接口密钥&#xff08;Key&#xff09;&#xff0c;可以按照以下步骤进行操作&#xff1a; 注册并登录淘宝/天猫开发者中心或京东开放平台或1688开放平台&#xff0c;并创建应用。在创建应用的过…

GODOC命令无效,原因是需要手动安装

在看《GO程序设计语言》这本书&#xff0c;按照其中的内容&#xff0c;想看下GO自带的包的文档。 书中讲&#xff0c;可以直接输入GoDOC命令来打开一个服务器&#xff0c;从而可以用浏览器访问文档库。输入命令后&#xff0c;系统提示找不到该命令。 查了资料后才发现&#xff…

6-55.汽车类的继承

根据给定的汽车类vehicle&#xff08;包含的数据成员有车轮个数wheels和车重weight&#xff09;声明&#xff0c;完成其中成员函数的定义&#xff0c;之后再定义其派生类并完成测试。 小车类car是它的派生类&#xff0c;其中包含载人数passenger_load。每个类都有相关数据的输出…

解决Wireshark分析RTMP抓包时Unknown问题

使用Wireshark抓包时&#xff0c;经常出现很多Unknown包&#xff0c;但实际上的字节流实际是正常的。 其实&#xff0c;RTMPT设置里有一个最大包大小的设置&#xff0c;默认是32768&#xff0c;而且默认RTMPT协议配置了从多个TCP流中重组RTMPT的功能(应当是考虑基于HTTP的传输…

Redis RDB

基于内存的 Redis, 数据都是存储在内存中的。 那么如果重启的话, 数据就会丢失。 为了解决这个问题, Redis 提供了 2 种数据持久化的方案: RDB 和 AOF。 RDB 是 Redis 默认的持久化方案。当满足一定条件的时候, 会把当前内存中的数据写入磁盘, 生成一个快照文件 dump.rdb。Redi…

Day48力扣打卡

打卡记录 最大化城市的最小电量&#xff08;二分前缀和差分数组贪心&#xff09; 链接 class Solution:def maxPower(self, stations: List[int], r: int, k: int) -> int:n len(stations)sum list(accumulate(stations, initial0))for i in range(n):stations[i] sum[…

vscode插件离线下载

离线下载插件地址&#xff1a;https://marketplace.visualstudio.com/VSCode