C语言——结构体讲解

目录

一、结构体类型的声明

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

三、结构体的重命名

四、结构体的自引用 

 五、结构体内存对齐

六、结构体传参

七、结构体实现位段

7.1  什么是位段

7.2  位段的声明和使用

7.3  位段的空间大小计算

7.4  位段的内存分配

7.5  位段的跨平台问题

7.6  位段的应用

7.7  位段使用的注意事项 

C语言已经提供了内置类型,如:char、short、int、long、float、double等,但是只有这些内置类 型还是不够的,假设我想描述学生,描述⼀本书,这时单⼀的内置类型是不行的。描述⼀个学生需要名字、年龄、学号、身高、体重等;描述⼀本书需要作者、出版社、定价等。C语言为了解决这个问题,增加了结构体这种自定义的数据类型,让程序员可以自己创造适合的类型。

 结构体,他就将不同类型的数据存放在一起,作为一个整体进行处理。

一、结构体类型的声明

struct tag  //结构体标签tag
{
	member - list; //括号里面就是写成员变量的地方,可以写多个变量并且可以是不同类型的
}variable - list; //这个地方可写可不写,写了的话就是创建了一个全局的该结构体变量
 
struct tag
{
	member - list;
}* variable - list;//在这个地方加一个*号就是该结构体指针类型*variable - list就是一个全局的结构体指针变量
 
int main()
{
	struct stu s;//在这个地方创建的结构体变量是局部的结构体变量
	return 0;
}

例如描述⼀个学生: 

struct Stu
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
}; //分号不能丢

特殊声明: 

在声明结构体的时候,可以不完全的声明。

//匿名结构体类型
struct  //匿名结构体类型就是省略了结构体标签
{
	int a;
	char b[5];
	char c;
}st1,st2; //但是只能在这个位置创建该结构体变量且是全局的,也可以创建多个变量
 
 
struct
{
	int a;
	char b[5];
	char c;
}*t1,st2;//这个地方加一个*,t1就是该结构体指针类型,但是st2并不是指针。也是属于匿名结构体(指针)类型
 
int main()
{
    t1 = &st1;//这个地方是错误的,编译器会将这两种声明当成完全不同的两个类型,所以是非法的
    return 0;
}

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

struct Point
{
	int x;
	int y;
}p1; //声明类型的同时定义变量p1(全局变量)
struct Point p2; //定义结构体变量p2(全局变量)
 
//初始化:定义变量的同时赋初值。
struct Point p3 = { x, y };  //(全局变量)
struct Stu        //类型声明
{
	char name[15];//名字
	int age;      //年龄
};
struct Stu s = { "zhangsan", 20 };//初始化(全局变量)
struct Node
{
	int data;
	struct Point p;
	struct Node* next;
}n1 = { 10, {4,5}, NULL }; //结构体嵌套初始化(全局变量),含别的类型的结构体时初始化就在中间加一个大括号初始化嵌套的别的结构体
struct Node n2 = { 20, {5, 6}, NULL };//结构体嵌套初始化(全局变量)
 
int main()
{
    struct Node n3 = {30, {6,7}, NULL};//结构体嵌套初始化(局部变量)
}

三、结构体的重命名

结构体的重命名就是给一个结构体类型重新起一个名字

使用typedef关键字

比如,不想要struct前缀就可以重命名:

typedef struct Node//在struct前面加一个typedef
{
	int data;
}Node;//在这里写下新的名字
 
int main()
{
    Node d1;//这里就定义了一个Node类型的结构体
    struct Node d2;//但是加上struct也是可以的~
    return 0;
}

四、结构体的自引用 

在结构中包含一个类型为该结构本身的成员就是结构体的自引用

struct Node
{
	int data;
	struct Node* next;//带一个自身结构体的指针类型就是结构体的自引用
};

我们看一下错误的自引用

//错误的自引用:
//ex1
struct Node
{
	int data;
	struct Node next;//结构体的自引用只能引用自身结构体指针!!!
};
//ex2
typedef struct Node
{
	int data;
	Node* next;//结构体的自引用不能使用重命名的名字,因为还没有重命名成功。
}Node;

 五、结构体内存对齐

内存对齐是一个存储数据的规则,可以依此来计算结构体的大小

我们先来猜一下两个结构体的大小是多少:

struct node1
{
	char a;
	int i;
	char b;
};
 
struct node2
{
	int i;
	char a;
	char b;
};
int main()
{
	printf("%d\n%d\n", sizeof(struct node1), sizeof(struct node2));
	return 0;
}

 

虽然两个结构体的成员是一样的,但是结构体的大小确实不一样的,这就要考虑内存对齐了。

每个成员都要在该成员对齐数的整数倍的偏移量处开始存放!

结构体内存对齐的规则:

1. 第一个成员在与结构体变量偏移量为0的地址处。

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

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

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

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

接下来还是参照上面的两个结构体来说明: 

从0偏移量开始,

a为char类型占一个字节,默认对齐数位8,较小值为1,所以对齐数为1,从0偏移量的地方开始存放;

i为int类型,占四个字节,默认对齐数为8,较小值为4,所以对齐数为4,要放在偏移量为4的倍数的地方,从而就跳过了三个字节的空间;

b与a相同,如上图所示,直接放到8偏移量的位置。

存放完所有数据后,共占用的9个字节,该结构体最大的成员变量对齐数为4,而9并不是4的倍数,所以要多浪费3个字节,最终该结构体的大小就为12.

与上一个结构体成员一样,只不过成员的顺序不同,结果也不同;

这里也就不赘述了,规则都是一样的。

为什么存在内存对齐?

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

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

2. 性能原因:

数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

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

小tips:

在声明结构体时,将相同类型的数据放在一起,可以节省空间!

修改默认对齐数:

使用预指令#pragma来改变默认对齐数;

#include <stdio.h>
#pragma pack(2)//设置默认对齐数为8
struct S1
{
    char c1;
    int i;
    char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数为1
struct S2
{
    char c1;
    int i;
    char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
    //输出的结果是什么?
    printf("%d\n", sizeof(struct S1));//结果为8
    printf("%d\n", sizeof(struct S2));//结果为6
    return 0;
}

#pragma pack(n); //设置默认对齐数为n

#pragma pack(); //恢复默认对齐数

六、结构体传参

结构体传参可以传结构体,也可以传结构体地址;

struct S
{
	int data[1000];
	int num;
};
struct S s = { {1,2,3,4}, 1000 };
//结构体传参
void print1(struct S s)
{
	printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
	printf("%d\n", ps->num);
}
int main()
{
	print1(s);  //传结构体
	print2(&s); //传地址
	return 0;
}

函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。

所以一般结构体传参的时候要传结构体地址。性价比高。

七、结构体实现位段

结构体讲完就得讲讲结构体实现位段的能力。

7.1  什么是位段

       位段(或称“位域”,Bit field)为一种数据结构,可以把数据以位的形式紧凑的储存,并允许程序员对此结构的位进行操作。这种数据结构的好处:

  • 可以使数据单元节省储存空间,当程序需要成千上万个数据单元时,这种方法就显得尤为重要。
  • 位段可以很方便的访问一个整数值的部分内容从而可以简化程序源代码。

  而位域这种数据结构的缺点在于,其内存分配与内存对齐的实现方式依赖于具体的机器和系统,在不同的平台可能有不同的结果,这导致了位段在本质上是不可移植的

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

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

2. 位段的成员名后边有⼀个冒号和⼀个数字。

7.2  位段的声明和使用

虽然位段可以决定用多少位来储存数据,但是切不可认为位段就是可以自定义一个数据类型。位段是依赖结构体来实现的,我们可以认为位段是可以将一个盒子里面格子自定义大小。

位段的声明:

struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};

需要注意的是:

  • 这里面的数字代表的不是字节,是比特(bit)。
  • 位段成员的类型只能是整型家族的,例如:int, unsigned int, signed int, char。

位段的使用:

int main()
{
	struct A a;
	a._a = 2;
	a._b = 3;
	a._c = 5;
	a._d = 10;
	return 0;
}

相当于实例化后的a里面的不同大小的内存里放入了数据。 

7.3  位段的空间大小计算

因为不同平台上的规则都是不太一样的,计算出来的结果也会有些许差异,以下使用vs2022的x64环境下运行的
例如:

#include <stdio.h>
struct A
{
	int _a : 2;//二进制位
	int _b : 5;
	int _c : 10;
	int _d : 30;
};

int main()
{
	printf("%d", sizeof(struct A));
	return 0;
}

上面代码的输出结果是8。

  • 声明类型是int类型的,所以一开始先开辟4个字节的内存,也就是32bit。
  • _a用掉了2bit,还剩下30bit。
  • _b用掉了5bit,还剩下25bit。
  • _c用掉了10bit,还剩下15bit。
  • _d需要30bit的空间,但是预先开辟的空间只剩下15bit,所以我们还需要再开辟一个int大小的空间,之前剩下的15bit的空间选择不使用,_d的30bit全放在第二个空间内。
  • 结果为8

注意:

  大家有没有发现,我们在声明位段的时候,如果定义的是int,那么冒号后面跟上的数字不能超过32,如果定义的是char,那么冒号后面跟上的数字不能超过8。如果超过以后,就会报错。

       其实根据内存对齐原则,如果超出以后,处理器就需要访问两次才能完整的得到数据。所以在定义的时候,应该避免超出应有的内存大小。

7.4  位段的内存分配
  • 位段分配的内存中的比特位是从左向右使用的,还是从右向左使用的呢?
  • 如何证明内存分配剩余的比特位不够使用时,是继续使用还是浪费掉呢?

接下来我们分析:
用例代码:

#include <stdio.h>
struct A
{
	char _a : 3;
	char _b : 4;
	char _c : 5;
	char _d : 4;
};
int main()
{
	struct A a = {0};
	a._a = 10;
	a._b = 12;
	a._c = 3;
	a._d = 4;
	return 0;
}

我们假设:位段分配的内存中的比特位是从右向左使用的,分配剩余的比特位不够使用时,浪费掉剩余内存。
则:

  • 我们先定义位段,如下图: 

  • 执行程序:a._a = 10; 10的二进制为1010,放入_a中,由于_a只有3bit,需要截断,所以舍弃最高位1,放入010:

  • 执行程序:a._b = 12;,12的二进制为1100,刚好可以放入,如下图: 

 

  • 执行程序:a._c = 3;,3的二进制为11,由于_c有5bit,高位添0,放入00011,如下图: 

  • 执行程序:a._d = 4;,4的二进制为100,放入0100,如下图: 

  • 程序就基本执行完了,那么内存中是什么样的呢?根据上面分析,我们一开始给结构体初始化为0,我们可以得到: 

由于机器是小端存储,所以内存上应该是:62 03 04.
经过调试,可以看到: 

以上也证明了, 在VS2022上,位段分配的内存中的比特位是从右向左使用的,分配剩余的比特位不够使用时,浪费掉剩余内存,重新开辟新的空间。 

7.5  位段的跨平台问题
  1. int 位段被当成有符号数还是无符号数是不确定的。
  2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题)。
  3. 位段中的成员在内存中从左向右分配还是从右向左分配的标准尚未定义。
  4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳打一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。

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

7.6  位段的应用

位段由于跨平台的问题,真正的用途的其中一个是计网的IP数据报:

7.7  位段使用的注意事项 

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

wamp集成环境部署

Windows下Apache服务器搭建 第一步&#xff1a;下载Windows下的最新ZIP压缩包 推荐下载网址&#xff1a;http://www.apachelounge.com/download/ 为了让Apache服务器发挥更好的性能&#xff0c;请根据自己的系统选择下载&#xff0c;如果不清楚自己的系统是64位还是32位&am…

x-cmd pkg | frp - 内网穿透工具

简介 frp&#xff08;Fast Reverse Proxy&#xff09;是一个专注于内网穿透的高性能反向代理应用&#xff0c;可以将内网服务以安全、便捷的方式通过具有公网 IP 节点的中转暴露到公网。 它采用 C/S 模式&#xff0c;将服务端部署在具有公网 IP 的机器上&#xff0c;客户端部…

企业品牌推广方式,产品推广方法

如何有效推广平台和产品成为了企业不可忽视的重要问题。推广不仅仅是为了提升知名度&#xff0c;更是为了吸引目标受众、促进销售、建立品牌形象。 一、平台推广&#xff1a;构建线上线下双通道 建立专业网站&#xff1a; 企业的网站是线上推广的重要窗口&#xff0c;要确保网…

操作系统【OS】Ch2【大题】 PV题型分类

生产者-消费者问题&#xff1a;生产资源-消费资源理发师问题&#xff1a;服务-被服务读者-写者问题&#xff1a;同类进程不互斥、异类进程互斥哲学家进餐问题&#xff1a;只有一类进程&#xff0c;每个进程需要同时拥有多种资源才能运行单纯的同步问题&#xff1a;前驱后继图 生…

dubbo:服务暴露

节点角色说明&#xff1a; Provider:暴露服务的服务提供方。 Consumer::调用远程服务的服务消费方。 Registry:服务注册与发现的注册中心。 Monitor:统计服务的调用次调和调用时间的监控中心。 Container:服务运行容器。 调用关系说明&#xff1a; 0.服务容器负责启动&#xff…

ubuntu上创建ftp服务器

今天在linux电脑上安装了ftp服务器&#xff0c;中间碰到不少问题&#xff0c;参照各路攻略&#xff0c;修改多次配置后终于完成了服务器搭建 1&#xff1a;安装vsftp服务器 最简答的一步&#xff0c;直接&#xff1a;apt-get install vsftp 安装完成后&#xff0c;查看版本号…

蓝桥杯(C++ 最大开支 优先队列)

优先队列&#xff1a; 蓝桥杯&#xff08;C 整数删除 优先队列 &#xff09;-CSDN博客 思路&#xff1a; 1、每个人依此选择项目&#xff0c;每个人选项目时都&#xff08;选择当下花费增加最多的项目&#xff09;&#xff0c;若项目i的门票价格为kxb&#xff0c;那么增加一个…

机器学习 | 深入理解并掌握核心概念

在如今数字化时代的浪潮下&#xff0c;机器学习已经成为人工智能领域的璀璨明星。它像一面魔镜&#xff0c;赋予计算机系统学习和改进的能力&#xff0c;让机器能够从海量数据中提取规律、预测未来&#xff0c;甚至做出智能决策。本 专栏 将带您踏上机器学习的奇妙之旅&#xf…

机器人学论文——智能施药机器人调研报告

目录 摘 要 Abstract 第一章&#xff1a;引言 1.1研究背景 1.2 研究意义 1.3文章架构 第二章&#xff1a;智能施药机器人发展现状 2.1引言 2.2 大田智能施药机器人发展现状 2.3 果园智能施药机器人发展现状 2.4 设施农业智能施药机器人发展现状 第三章&#xff1a;智能施药机器…

短视频推广方案,新品推广攻略

短视频以其生动、直观的特点吸引了大量用户&#xff0c;成为品牌塑造、产品推广的有效手段。本文将深入解读短视频推广方法&#xff0c;帮助企业在这个充满创意和活力的平台上实现产品的成功推广。 一、抓住用户注意力的前奏&#xff1a;创意内容制作 引人入胜的开篇&#xff…

打家劫舍系列(三合一)(动态规划)

本篇博客讲解一下动态规划的打家劫舍系列&#xff0c;对应的力扣题目分别是198. 打家劫舍&#xff0c;213. 打家劫舍 II&#xff0c;337. 打家劫舍 III 198. 打家劫舍&#xff1a; 题目&#xff1a; 你是一个专业的小偷&#xff0c;计划偷窃沿街的房屋。每间房内都藏有一定的…

关于C#中的HashSet<T>与List<T>

HashSet<T> 表示值的集合。这个集合的元素是无须列表&#xff0c;同时元素不能重复。由于这个集合基于散列值&#xff0c;不能通过数组下标访问。 List<T> 表示可通过索引访问的对象的强类型列表。内部是用数组保存数据&#xff0c;不是链表。元素可重复&#xf…

【数据结构】快速排序,归并排序

快速排序 1.hoare版本 根据动图的演示&#xff0c;整理的思路如下&#xff0c; 1.定义left,right,key。key默认是左边第一个元素&#xff0c;像两个指针&#xff0c;左边找比key大的&#xff0c;右边找比k小的&#xff0c;找到的话&#xff0c;交换二者&#xff0c;往返这个过…

《移动通信原理与应用》——QAM调制解调仿真

目录 一、QAM调制与解调仿真流程图&#xff1a; 二、仿真结果&#xff1a; 三、Matlab仿真程序代码如下&#xff1a; 一、QAM调制与解调仿真流程图&#xff1a; QAM调制仿真流程图&#xff1a; QAM解调仿真流程图&#xff1a; 二、仿真结果&#xff1a; &#xff08;1&…

JOSEF约瑟 中间继电器JZ14-44Z/4 不带外罩和接线座

系列型号 JZ14-014Z/0中间继电器;JZ14-014Z/1中间继电器; JZ14-014Z/2中间继电器;JZ14-014Z/4中间继电器; JZ14-014J/0中间继电器;JZ14-014J/1中间继电器; JZ14-014J/2中间继电器;JZ14-014J/3中间继电器; JZ14-014J/4中间继电器;JZ14-140Z/0中间继电器; JZ14-140Z/1中间继…

Web06--JavaScript基础02

1、JS流程控制语句 JS与Java一样&#xff0c;也有三个流程控制语句&#xff1a; 顺序结构 选择结构 循环结构 1.1 选择结构 1.1.1 if结构 <script type"text/javascript">if (条件表达式) {代码块;} else if(条件表达式){代码块;} else {代码块;} </scr…

Flink中的容错机制

一.容错机制 在Flink中&#xff0c;有一套完整的容错机制来保证故障后的恢复&#xff0c;其中最重要的就是检查点。 1.1 检查点&#xff08;Checkpoint&#xff09; 在流处理中&#xff0c;我们可以用存档读档的思路&#xff0c;将之前某个时间点的所有状态保存下来&#xf…

MATLAB实现岭回归数学建模算法

岭回归&#xff08;Ridge Regression&#xff09;是一种线性回归的扩展&#xff0c;用于处理多重共线性&#xff08;multicollinearity&#xff09;的问题。多重共线性是指自变量之间存在高度相关性的情况&#xff0c;这可能导致线性回归模型的不稳定性和过拟合。 岭回归通过在…

风二西CTF流量题大集合-刷题笔记|基础题(4)

61.sql2 sql.pcapng flag{a3eb0ff8-e467-5036-7c9b-287f6848d5f3} 62.冰蝎2.0 swt1.pcapng flag{0867c25f69f0c62c970408ccefe29bb7} 64.gs哥斯拉流量4.0 gs.pcapng flag{0fffbfa87e5508955b397950502db0bd} 65.冰蝎web流量 webshell.pcapng flag{da2c30d9318a0d80b4bfa…

C++——IOStream

什么是IO&#xff1f; C语言和C&#xff0c;我们其实已经接触到了两个IO的概念 #include<stdio.h> #include<iostream> iostream&#xff0c;便是IO流&#xff0c;其中I表示in&#xff0c;O表示out&#xff0c;代表着用户的输入和终端的输出。在之前的C语法中&a…