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

news2025/2/26 1:12:21

目录

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

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 };//8
	struct S2 s2 = { 0 };//12

	printf("%d\n", sizeof(struct S1));//8
	printf("%d\n", sizeof(struct S2));//12
	return 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.coloradmin.cn/o/1278733.html

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

相关文章

力扣.特定深度节点链表(java BFS解法)

Problem: 面试题 04.03. 特定深度节点链表 文章目录 题目描述思路解题方法复杂度Code 题目描述 思路 根据题意需要取出二叉树每一层节点组成的链表并将其添加到一个数组中。我们将该要求分解成如下的操作: 1.利用BFS获取二叉树每一层的节点 2.利用链表的尾插法将二…

手敲MyLinkedList,简单了解其运行逻辑

1.LinkedList的介绍和结构 LinkedList的底层是双向链表结构,相对于之前的单向无头非循环链表来说,LinkedList最大的区别就是该链表可以增加了一条链接逻辑,可以从最后一个节点通过地址访问来到整个链表的头结点。 通过以下集合框架&#xff0…

【数据库】数据库多种锁模式,共享锁、排它锁,更新锁,增量锁,死锁消除与性能优化

多种锁模式的封锁系统 ​专栏内容: 手写数据库toadb 本专栏主要介绍如何从零开发,开发的步骤,以及开发过程中的涉及的原理,遇到的问题等,让大家能跟上并且可以一起开发,让每个需要的人成为参与者。 本专栏会…

判断三角形-第11届蓝桥杯选拔赛Python真题精选

[导读]:超平老师的Scratch蓝桥杯真题解读系列在推出之后,受到了广大老师和家长的好评,非常感谢各位的认可和厚爱。作为回馈,超平老师计划推出《Python蓝桥杯真题解析100讲》,这是解读系列的第12讲。 判断三角形&#…

基于景区智慧灯杆、智能指路牌基础设施的景区建设应用

智慧景区是指运用现代信息技术手段,将景区内的资源、服务、管理等进行数字化、网络化和智能化整合,打造出高效便捷、安全舒适、互动体验和可持续发展的景区。智慧景区可以从以下几个方面进行体现: 智慧导览:通过使用智能化的导览…

二叉树OJ题目——C语言

LeetCode 104.二叉树的最大深度 1. 题目描述: 给定一个二叉树 root ,返回其最大深度。 二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。 示例 1: 输入:root [3,9,20,null,null,15,7] 输出:3示例…

Ubuntu 安装 MySQL8 配置、授权、备份、远程连接

目录 0100 系统环境0200 下载0300 安装0400 服务管理0401 关闭、启动、重启服务0402 查看服务状态 0500 查看配置文件0600 账号管理0601 添加账号0602 删除账号0603 修改密码0604 忘记root密码 0700 自动备份0800 远程访问 0100 系统环境 [rootlocalhost ~]# cat /proc/versio…

canvas基础:绘制虚线

canvas实例应用100 专栏提供canvas的基础知识,高级动画,相关应用扩展等信息。 canvas作为html的一部分,是图像图标地图可视化的一个重要的基础,学好了canvas,在其他的一些应用上将会起到非常重要的帮助。 文章目录 示例…

rtmp 协议详解

1. handshake 1.1 概述 rtmp 连接从握手开始。它包含三个固定大小的块。客户端发送的三个块命名为 C0,C1,C2;服务端发送的三个块命名为 S0,S1,S2。 握手序列: 客户端通过发送 C0 和 C1 消息来启动握手过程。客户端必须接收到 S1 消息,然后…

使用 Go 构建高性能的命令行工具

命令行工具(CLI)在软件开发中扮演着重要的角色,尤其是在自动化工具、开发工具链和服务器管理等领域。Go 语言以其简洁性和高性能而闻名,非常适合用来创建强大且高效的 CLI 工具。本文将详细介绍如何使用 Go 语言来构建 CLI 应用&a…

【c++中的四种类型转换,应用场景】

c中的四种类型转换 1.静态转换 &#xff1a; static_cast 用法&#xff1a;static_cast<type_name>(val) 1.基本类型间的转换 enum Day { Mon 1, Tues 2, Wed 3, Thu 4, Fir 5, Sat 6, Sun 7 };int main() {int a 10;char ch t;double dx 21.65;a static_…

来CSDN一周年啦!!!

各位CSDN的uu们你们好呀&#xff0c;今天是小雅兰来到CSDN创作的一周年啦&#xff0c;时间&#xff0c;说长不长&#xff0c;说短也不短&#xff0c;在这一年中&#xff0c;我认为我也收获了一些很有价值的东西吧&#xff01;&#xff01; 一周年了&#xff0c;该创作的还得继续…

正则表达式(基础、常用)

正则&#xff08;RegExp&#xff09;:用于检测字符串是否符合该规则&#xff0c;符合返回值为true,不符合返回值为false 一、定义正则表达式 1、字面量方式 const reg/a/ // 字符串中含有a即可 2、构造函数方式 const reg0new RegExp(a)二、元字符(特殊字符) 1、\d :匹…

1998-2021年全国各区县PM2.5平均浓度数据

1998-2021年全国各区县PM2.5平均浓度数据 1、时间&#xff1a;1998-2021年 2、指标&#xff1a;省、省代码、市、市代码、县代码、县、年份、均值、总和、最小值、最大值、标准差 3、来源&#xff1a;Washington university Atmospheric Composition Analysis Group 4、范围…

【Linux】-信号-(信号的产生,保存,处理,以及os是怎么读取硬件的输入,硬件异常和coredump,定时器的原理简单的用户态和内核态的详细介绍)

&#x1f496;作者&#xff1a;小树苗渴望变成参天大树&#x1f388; &#x1f389;作者宣言&#xff1a;认真写好每一篇博客&#x1f4a4; &#x1f38a;作者gitee:gitee✨ &#x1f49e;作者专栏&#xff1a;C语言,数据结构初阶,Linux,C 动态规划算法&#x1f384; 如 果 你 …

SpringBoot-Vue项目初始搭建

SpringBoot-Vue项目初始搭建 1、项目搭建 前提&#xff1a;配置过nodejs环境&#xff0c;安装了vuecli&#xff08;如果未配置&#xff0c;可以参照此教程&#xff1a;https://www.bilibili.com/video/BV18E411a7mC/ p12&#xff09; 新建文件夹(最好不要有中文) 打开cmd …

AirServer怎么用?如何AirServer进行手机投屏

什么是 AirServer&#xff1f; AirServer 是适用于 Mac 和 PC 的先进的屏幕镜像接收器。 它允许您接收 AirPlay 和 Google Cast 流&#xff0c;类似于 Apple TV 或 Chromecast 设备。AirServer 可以将一个简单的大屏幕或投影仪变成一个通用的屏幕镜像接收器 &#xff0c;是一款…

C语言实现猜数字游戏

前面我们已经了解了分支循环、数据类型及变量的知识点&#xff0c;今天我将用之前学过的知识进行实操&#xff0c;将所学的知识进行巩固和提升。下面的讲解仅我个人认知水平&#xff0c;如有欠缺之处&#xff0c;欢迎大家指正&#xff0c;并且我希望初学者在看完讲解后可以独立…

汇编语言实现音乐播放器

目标程序 用汇编语言实现一个音乐播放器&#xff0c;并支持点歌 Overview 乐曲是按照一定的高低、长短和强弱关系组成的音调&#xff0c;在一首乐曲中&#xff0c;每个音符的音高和音长与频率和节拍有关&#xff0c;因此我们要分别为3首要演奏的乐曲定义一个频率表和一个节拍…