【C语言】自定义类型:结构体

【C语言】自定义类型:结构体

前言

在C语言中有许多内置的数据类型,如int、char、short等,我们可以直接拿来用。但是在有些情况下,比如描述一个人,这个人的基本数据有名字,性别和年龄等,内置的类型并不能满足我们的使用需求,那我们就需要自定义类型。这篇文章会简单介绍一下自定义类型中的结构体。

一、结构体是什么

简单来说,结构体就是一些值的集合,这些值被称为成员变量,成员变量可以是不同类型
结构体和数组相似,数组是一些类型相同的元素的集合

二、结构体的声明

struct tag
{
member-list;
}variable-list; //分号不可以丢

struct 是关键字,tag 是我们自定义的名字。struct tag 连在一起是类型名,使用时不要把 struct 丢掉了 ,比如创建一个结构体变量 a :struct tag a

member - list 是成员变量,它们可以是不同类型,如 int a char b

variable-list 是结构体变量名,相当于 int a中的 a ,我们可以在声明结构体时创建结构体变量,也可以不创建,使用时再创建

这里我们描述一个学生:

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

三、特殊的声明

结构体的声明,可以不完全声明

struct
{
	int a;
	char b;
	float c;
}x;
struct
{
    int a;
    char b;
    float c;
}a[20], *p;

如上代码,不完全声明的结构没有名字,因此当我们创建变量时,只能在声明结构时创建

这两个结构虽然成员相同,但在编译器眼中,就是两个不同的结构体类型
因此以下代码是非法的

p = &x;

四、结构体的自引用

说到结构体的自引用,就不得不提一下数据结构中的链表了

那什么是链表呢?

链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的

如图,共有四个节点,每个节点都是一个结构体
每个节点在存储数据的同时还能指向下一个节点,形成了一个链式结构

image-20231004205701756

那我们为了完成上述功能,声明一个结构体

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

这样写看似没什么问题,但是当我们计算结构体大小时就会发现问题

这个结构体的大小是 data 的大小加上 next 的大小,data 大小为 4 个字节,next 的大小呢?next 的大小又是 data 大小加上 next 大小…如此循环,这个结构体的大小就无法计算。

为了解决这个问题,我们将下节点的地址放到上一个节点,这样就可以通过上个节点找到下个节点,而指针的大小是固定的,结构体的大小也就可以计算出来了

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

image-20231004212054615

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

定义

我们可以在声明结构体的同时进行结构体变量定义,也可以先声明,等用的时候再定义结构体变量

struct Point
{
	int x;
	int y;
}p1;//p1为全局变量

int main()
{
	struct Point p2;//p2为局部变量
	return 0;
}

初始化

结构体的初始化要用 {} ,成员之间用逗号隔开

struct Point
{
	int x;
	int y;
}p1 = { 1, 2 };

struct Student
{
	char name[20];
	int age;
}s1 = { "张三", 18 };

int main()
{
	struct Point p2 = { 2,3 };
	struct Student s2 = { "李四", 19 };
	return 0;
}

同时,结构体支持嵌套初始化

struct Point
{
	int x;
	int y;
}p1 = { 1, 2 };

struct Node
{
	int data;
	struct Point p;
	struct Node* next;
}n1 = { 10, {4,5}, NULL };

六、结构体成员的访问

结构体变量访问成员

访问形式:变量名.成员名

struct Stu
{
	char name[20];
	int age;
};

int main()
{
	struct Stu s = { "张三",18 };
	printf("%s\n", s.name);
	printf("%d\n", s.age);
}

结构体指针变量访问成员

结构体指针变量想访问结构体成员需要先解引用再正常进行访问,或者直接 结构体指针变量->成员名

struct Stu
{
	char name[20];
	int age;
};

int main()
{
	struct Stu s = { "张三",18 };
	struct Stu* p = &s;
	printf("%s\n", (*p).name);
	printf("%d\n", p->age);
}

七、结构体内存对齐

1.引出

结构体的基本使用我们已经了解,下面我们来探讨如何计算结构体的大小

结构体的大小并不是单纯地把成员的大小相加,我们来看下面的代码

struct s1
{
	char c1;
	char c2;
	int i;
};

struct s2
{
	char c1;
	int i;
	char c2;
};

int main()
{
	printf("s1的大小:%d\n", sizeof(struct s1));
	printf("s2的大小:%d\n", sizeof(struct s2));
	return 0;
}

s1 和 s2 这两个结构的成员相同,只是成员的排列顺序不同,那么它们的大小如何?
image-20231004215921771

可以看到,s1 和 s2 的大小并不相同,这就涉及到结构体的内存对齐了

在内存对齐之前,我们先引入一个宏 offsetof,这个宏是干什么的?

offsetof 可以计算结构体成员相较于起始位置的偏移量
参数:结构体类型,结构体成员名
头文件:<stddef.h>

image-20231004220934969

下面我们计算一下 s2 的成员的偏移量

struct s1
{
	char c1;
	char c2;
	int i;
};

struct s2
{
	char c1;
	int i;
	char c2;
};

int main()
{
	//printf("s1的大小:%d\n", sizeof(struct s1));
	//printf("s2的大小:%d\n", sizeof(struct s2));
	printf("%d\n", offsetof(struct s2, c1));
	printf("%d\n", offsetof(struct s2, i));
	printf("%d\n", offsetof(struct s2, c2));
	return 0;
}

运行结果
image-20231004221903389

什么意思呢?我们画图表示一下,假设内存中的存储如下:

c1 是起始位置,所以它的偏移量是 0
image-20231004222358910

而 i 的偏移量是 4 ,这表示它跳过了 3 个字节,直接存到了 4 的位置
image-20231004222614598

c2 的偏移量是 8 ,存到了 8 的位置
image-20231004222813065

此时三个数据都存了起来,s2 大小是 9 个字节,但是上面我们通过 sizeof 计算 s2 的大小是 12,这说明 s2 在 9 字节的基础上,又额外占用了 3 字节,这样一算直接浪费了 6 个字节
image-20231004223209069

这就是内存对齐,那么内存对齐是怎么实现的?又为什么要浪费空间进行内存对齐?

2.如何对齐

对齐规则

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

2.其他成员存放在 对齐数 的整数倍的地址处
对齐数 = 编译器默认对齐数 与 该成员大小的 较小值
例如:vs默认对齐数为 8 ,一个 int 成员大小为 4,那么对齐数就是 4

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

4.如果嵌套了结构体,那么嵌套的结构体存放在自己最大对齐数的整数倍处,结构体的总大小为所有最大对齐数(包括嵌套结构体的对齐数)的整数倍

详细步骤

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

image-20231004230618130

2.其他成员存放在 对齐数 的整数倍的地址处

第二个成员是 i ,自身大小为 4 ,默认对齐数为 8 ,取较小值 4 为对齐数,所以 i 存放在 4 的整数倍的地址处
image-20231004230840038

第三个成员 c2,自身大小为 1,默认对齐数为 8 ,取较小值 1 为对齐数,所以 c2 存放在 1 的整数倍的地址处
image-20231004231107746

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

此时的结构体总大小为 9 ,而所有对齐数为 1 ,4 ,1 ,最大对齐数为 4,所以总大小为 4 的倍数 12
image-20231004231705384

4.如果嵌套了结构体,那么嵌套的结构体存放在自己最大对齐数的整数倍处,结构体的总大小为所有最大对齐数(包括嵌套结构体的对齐数)的整数倍

在此声明结构体 s3,s3 嵌套了 s2

struct s2
{
	char c1;
	int i;
	char c2;
};

struct s3
{
	char c;
	struct s2 s2;
};

成员 c 依然存放在偏移量为 0 的地址处,而结构体 s2 存放在自己最大对齐数,即 4 的整数倍处
image-20231004232439063

此时结构体总大小为 16,符合条件:结构体的总大小为所有最大对齐数(包括嵌套结构体的对齐数)的整数倍
所以最终大小为 16
image-20231004232634381

3.为什么对齐

我们已经学习了如何进行内存对齐,可以发现,内存对齐是存在空间浪费的,那为什么还要进行内存对齐呢?

1.平台原因

并不是所有硬件都可以访问任意地址的任意类型数据的,某些硬件平台只能访问特定地址的特定类型数据

2.性能原因

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

对齐
访问 i ,只需要访问一次就可以
image-20231004234054224

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

八 、结构体传参

我们知道函数传参时,如果是传值,那么会形成形参,会有时间和空间上的消耗
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的
下降。

struct S
{
	int data[1000];
	int num;
};

//传值
void print1(struct S s)
{
	printf("%d\n", s.num);
}

//传址
void print2(struct S* s)
{
	printf("%d\n", s->num);
}

int main()
{
	struct S s = { {1,2,3},1000 };
	//传值
	print1(s);
	//传址
	print2(&s);
	return 0;
}

上述代码,打印函数优先选择 print2 ,具有更高的效率

总结:结构体传参的时候,要传结构体的地址。

九、位段

位段的出现就是为了节省空间

1.什么是位段

位段与结构类似,但又不同

位段的成员类型必须是 int、unsigned int 或signed int
位段的成员名后边有一个冒号和一个数字

例:

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

表示 _a 大小为 2 个比特位,_b 的大小为 5 比特位,以此类推

那我们就可以算出 A 的大小为 47 比特位,也就是 6 字节

那到底是不是呢?我们来验证一下

struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};
int main()
{
	printf("%d", sizeof(struct A));
	return 0;
}

结果:
image-20231005003153402

结果与我们预料的并不一样,这涉及到位段的内存分配

2.位段的内存分配

  1. 位段的成员可以是int unsigned int signed int 或者是char (属于整形家族)类型
  2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
  3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

例:

struct S
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};

我们推测结构体类型 S 的内存分配可能是这样的:

先开辟一个字节的空间,分配存放 a 和 b 的空间,C语言标准并未规定位段是从左向右分配还是从右向左分配,我们这里假设从右向左分配
image-20231005004157420

分配完 a、b之后,还剩下一个比特位,给 c 分配肯定是不够了,只能再开辟一字节空间,那么剩下的一个比特位是接着用还是浪费掉不用了?我们猜测是浪费了
image-20231005004452455

剩下 3 个比特位,不够给 d,只能再开一字节,浪费这 3 个比特位
image-20231005004703748

至此,空间分配完毕

如果没浪费空间,那结构体类型就是 2 个字节
如果浪费了,那结构体类型就是 3 字节

验证:

struct S
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};
int main()
{
	printf("%d\n", sizeof(struct S));
}

image-20231005004946923

结果是3,说明浪费了空间

我们假设位段是从右向左分配空间,现在来验证

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

10的二进制位是 1010,由于 a 只有三个比特位的空间,所以只能存下 010;
12的二进制位是 1100,b 有四个空间,存下1100
以此类推,c 存 00011,d 存 0100

image-20231005010248193

换算为十六进制:

image-20231005010601052

大概在内存中存的就是这么个数

验证:
image-20231005010734090

结果一致,至少说明在vs中,位段分配空间就是从右向左分配

3.位段的跨平台问题

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

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

4.位段的应用

位段在网络中应用很多
image-20231005011503842

我们日常发消息时,不是单纯地把要发的消息发到网上,而是封装成一个数据包
数据表包中的信息可以把消息准确发送给别人

而位段可以减小包体体积,避免数据包过大,造成网络拥堵

结束,再见:D

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
1 目标检测的定义 目标检测(Object Detection)的任务是找出图像中所有感兴趣的目标(物体),确定它们的类别和位置,是计算机视觉领域的核心问题之一。由于各类物体有不同的外观、形状和姿态,加上成像时光照、遮挡等因素的干扰,目标检测一直是计算机视觉领域最具有挑战性的问题。 目标检测任务可分为两个关键的子任务,目标定位和目标分类。首先检测图像中目标的位置(目标定位),然后给出每个目标的具体类别(目标分类)。输出结果是一个边界框(称为Bounding-box,一般形式为(x1,y1,x2,y2),表示框的左上角坐标和右下角坐标),一个置信度分数(Confidence Score),表示边界框中是否包含检测对象的概率和各个类别的概率(首先得到类别概率,经过Softmax可得到类别标签)。 1.1 Two stage方法 目前主流的基于深度学习的目标检测算法主要分为两类:Two stage和One stage。Two stage方法将目标检测过程分为两个阶段。第一个阶段是 Region Proposal 生成阶段,主要用于生成潜在的目标候选框(Bounding-box proposals)。这个阶段通常使用卷积神经网络(CNN)从输入图像中提取特征,然后通过一些技巧(如选择性搜索)来生成候选框。第二个阶段是分类和位置精修阶段,将第一个阶段生成的候选框输入到另一个 CNN 中进行分类,并根据分类结果对候选框的位置进行微调。Two stage 方法的优点是准确度较高,缺点是速度相对较慢。 常见Tow stage目标检测算法有:R-CNN系列、SPPNet等。 1.2 One stage方法 One stage方法直接利用模型提取特征值,并利用这些特征值进行目标的分类和定位,不需要生成Region Proposal。这种方法的优点是速度快,因为省略了Region Proposal生成的过程。One stage方法的缺点是准确度相对较低,因为它没有对潜在的目标进行预先筛选。 常见的One stage目标检测算法有:YOLO系列、SSD系列和RetinaNet等。 2 常见名词解释 2.1 NMS(Non-Maximum Suppression) 目标检测模型一般会给出目标的多个预测边界框,对成百上千的预测边界框都进行调整肯定是不可行的,需要对这些结果先进行一个大体的挑选。NMS称为非极大值抑制,作用是从众多预测边界框中挑选出最具代表性的结果,这样可以加快算法效率,其主要流程如下: 设定一个置信度分数阈值,将置信度分数小于阈值的直接过滤掉 将剩下框的置信度分数从大到小排序,选中值最大的框 遍历其余的框,如果和当前框的重叠面积(IOU)大于设定的阈值(一般为0.7),就将框删除(超过设定阈值,认为两个框的里面的物体属于同一个类别) 从未处理的框中继续选一个置信度分数最大的,重复上述过程,直至所有框处理完毕 2.2 IoU(Intersection over Union) 定义了两个边界框的重叠度,当预测边界框和真实边界框差异很小时,或重叠度很大时,表示模型产生的预测边界框很准确。边界框A、B的IOU计算公式为: 2.3 mAP(mean Average Precision) mAP即均值平均精度,是评估目标检测模型效果的最重要指标,这个值介于0到1之间,且越大越好。mAP是AP(Average Precision)的平均值,那么首先需要了解AP的概念。想要了解AP的概念,还要首先了解目标检测中Precision和Recall的概念。 首先我们设置置信度阈值(Confidence Threshold)和IoU阈值(一般设置为0.5,也会衡量0.75以及0.9的mAP值): 当一个预测边界框被认为是True Positive(TP)时,需要同时满足下面三个条件: Confidence Score > Confidence Threshold 预测类别匹配真实值(Ground truth)的类别 预测边界框的IoU大于设定的IoU阈值 不满足条件2或条件3,则认为是False Positive(FP)。当对应同一个真值有多个预测结果时,只有最高置信度分数的预测结果被认为是True Positive,其余被认为是False Positive。 Precision和Recall的概念如下图所示: Precision表示TP与预测边界框数量的比值 Recall表示TP与真实边界框数量的比值 改变不同的置信度阈值,可以获得多组Precision和Recall,Recall放X轴,Precision放Y轴,可以画出一个Precision-Recall曲线,简称P-R
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿洵Rain

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值