构造类型详解及热门题型结构体大小的计算

news2024/9/23 1:35:11

在编写程序时,简单的变量类型已经不能满足程序中各种复杂数据的需求,因此c语言还提供了构造类型的数据,构造数据是有基本数据按照一定的规则组成的。

目录

结构体类型的概念

结构体变量的定义

结构体变量的初始化

结构体变量的引用

结构体数组

结构体指针

结构体传参

共用体

枚举类型

结构体内存对齐

为什么存在内存对齐?

结构体大小计算

修改默认对齐数求大小

嵌套结构体计算大小


结构体类型的概念

结构体是一种由若干个成员组成的构造类型,成员可以是几种基本类型数据,也可以是另外的构造类型。既然结构体是一个构造类型,就需要先对其进行构造,我们称这个操作为声明一个结构体。

例如,一个学生包含学号,性别,分数等特点,一个老师有性别,年龄,教学科目等类别。

这两种类型就不能用普通的变量类型来表示,我们就可以自己定义一个结构体。

声明结构体的关键字为struct,一般形式如下

struct 结构体名

{

        成员列表;

};

大括号后边的分号切记不能忘记。

结构体名可以为teacher student,等等等等都可以,大括号里面的就是你所创建出的结构体具有哪些特征等。当然也可以放另一个结构体变量,我们后边会谈到。

声明上边的两种类型的结构体

如下

struct student
{
	char name[10];//名字
	int grade;//分数
	int num;//学号
	char sex;//性别
};

struct teacher
{
	char name[10];
	int age;
	char sex;
	char project[10];//任课科目
};

结构体变量的定义

前边已经介绍了如何声明一个结构体,如何使用构造的结构体才是我们的真正目的。

定义结构体变量的方法有两种

我们已经声明了student结构体,可以在程序中直接用定义一个小明,也可以再定义一个小红,要注意,声明一个结构体是创建一种新的类型名,要用新的类型名再定义变量,student的类型名为struct student。

定义如下:

struct student xiaoming;

struct student xiaohong;

结构体也可以使用typedef重命名,可以使定义变量时更加便捷。

例如

typedef struct student
{
	char name[10];//名字
	int grade;//分数
	int num;//学号
	char sex;//性别
}Student;

定义该结构体类型名为Student,再次定义一个新的变量就可以这样写

Student xiaogang;

是不是有很方便。

这种定义的方式可以写在函数内,也可以写在函数外部,写在函数外部就是全局变量。

这个时候就要说一说另一种定义变量的方式

struct student
{
	char name[10];//名字
	int grade;//分数
	int num;//学号
	char sex;//性别
}xiaoming,xiaogang;

可以看到,这种形式将定义的变量直接放在声明结构体的末尾处,需要注意的是要放在分号的前边且可以定义多个变量。

但是,这种定义方式不好的缺点是,如果这个结构体在头文件中存放,那么定义出的结构体变量都是全局变量,全局变量使用起来很危险,所以这种定义方式我们不推荐。

结构体变量的初始化

上边说过,这种直接在声明后边定义的方式并不推荐,但还是要知道,可以在声明后定义时就初始化,有点绕?

声明就是一栋房子的建造图纸,可以用这个图纸来造很多相似的房子,定义就是用图纸盖一栋房子,我们有了房子就可以在房子里放东西,布置布置,初始化就是粉刷房子,为房子内的房间装饰。

是不是懂啦

上边说边声明边定义边初始化,就是下边这样

struct student
{
	char name[10];//名字
	int grade;//分数
	int num;//学号
	char sex[20];//性别
}xiaoming = { "xiaoming",98,666,"男" };

定义的变量后边使用等号,然后将初始化的值放在大括号里,每一个数据要和结构体成员列表的顺序一样。

还有一种发方法就是定义初始化和声明分离

typedef struct student
{
	char name[10];//名字
	int grade;//分数
	int num;//学号
	char sex;//性别
}Student;

int main()
{
	Student xiaohong = { "xiaohong",99,6666,"女" };
	return 0;
}

是不是很简单就可以实现?

结构体变量的引用

如果我们想要修改结构体成员变量的值呢?

printf("%s %d %d %s",xiaohong);   ??????XXXXXX错误的哦

如果想要对结构体成员进行操作,我们就要拿出这个结构体中的变量,这个过程就叫做引用

一般形式如下

结构体变量名.成员名

例如,小红的分数更改为100

更改完成之后直接打印验证

如果是结构体里面套着结构体呢?

给Student加上birthday结构体储存其生日,如果记错了,如何修改其生日呢?

让Student结构体里有一个Birthday结构体变量,然后修改

直接公布答案

typedef struct birthday
{
	int month;
	int day;
}Birthday;

typedef struct student
{
	char name[15];//名字
	int grade;//分数
	int num;//学号
	char sex[10];//性别
	Birthday data;
}Student;



int main()
{
	Student xiaohong = { "xiaohong",99,6666,"女" ,{4,4} };
	xiaohong.data.day = 3;
	xiaohong.data.month = 3;
	xiaohong.grade = 100;

	printf("%d ", xiaohong.grade);
	printf("%d %d\n", xiaohong.data.day,xiaohong.data.month);
	return 0;
}

看见没看见没,如果是结构体里面有结构体变量,那么初始化内部的结构体变量的成员也要用大括号括起来。

结构体成员变量可以像普通变量一样进行各种运算。

结构体数组

我们已经知道了数组可以装好多种类型,当然结构体数组也不奇怪。结构体变量可以放好多组数据,结构体数组可以存放好几组结构体变量,就像一个小孩(结构体变量)有很多特征(结构体内部成员变量),一个班级(结构体数组)可以装好多小孩一样。

我们可以定义一个结构体数组

代码如下

typedef struct student
{
	char name[15];//名字
	int grade;//分数
	int num;//学号
	char sex[10];//性别
	Birthday data;
}Student;

int main()
{
	Student stu[3] = { {"dingding",66,22222,"女",{6,6}},{"shuaishuai",77,22223,"男",{6,6}} ,{"dengquan",88,22224,"男",{6,6}} };
	for (int i = 0; i < 3; i++)
	{
		printf("%s %d %d %s %d %d", stu[i].name, stu[i].grade, stu[i].num, stu[i].sex, stu[i].data.month, stu[i].data.day);
		printf("\n");
	}
	return 0;
}

运行后代码如下

        因为shuaishuai的名字长度为10,创建10个字符的数组,就无法存储结束标志\0,所以这里将名字的数组扩大为15,如果大家用汉字会报错的话,采取以下步骤

右击

高级->字符集->无

就可以正常使用汉字了,一个汉字两个字节。

        回归上边的操作,要记住的是,初始化结构体数组,每个结构体变量初始化内容要用大括号扩住,访问还是用.引用操作符,如果结构体套结构体的话,就多引用一次即可。

结构体指针

        指针可以指向整形,浮点型,甚至还可以指向他自己,变成二级指针,一个指向变量的指针表示该变量的起始地址,那么结构体指针就指向结构体变量的起始地址。

        既然指针指向结构体变量的地址,那么我们就可以通过结构体指针来访问结构体内的成员。

定义结构体指针的格式如下:

结构体类型 *指针名;

例如,定义一个Student结构类型的指针如下

Student *pstu;

重点来啦,使用结构体指针访问结构体成员有两种方法

第一种就是解引用在用.引用操作符进行引用

typedef struct birthday
{
	int month;
	int day;
}Birthday;

typedef struct student
{
	char name[15];//名字
	int grade;//分数
	int num;//学号
	char sex[10];//性别
	Birthday data;
}Student;

int main()
{
	Student xiaohong = { "xiaohong",99,6666,"女" ,{4,4} };
	Student* ptr = &xiaohong;
	(*ptr).grade = 100;
	printf("%d ", xiaohong.grade);

	return 0;
}

这里一定要注意,解引用结构体指针一定要用括号括住,这是因为.引用操作符的优先级比*解引用操作符优先级高,我们要的是先解引用结构体指针,再引用其成员变量。

还有一种方式

使用指向操作符引用结构体成员

代码如下(结构体声明部分省略)

int main()
{
	Student xiaohong = { "xiaohong",99,6666,"女" ,{4,4} };
	Student* ptr = &xiaohong;
	//(*ptr).grade = 100;
	ptr->grade = 101;
	printf("%d ", xiaohong.grade);

	return 0;
}

运行结果如图

结构体传参

结构体变量可以作为函数的参数,但是要记住哦,传参传过去的都是形参,形参的改变不影响实参,所以我们传结构体变量就只能访问其内部成员变量的值,想要在该函数里修改结构体变量,就要传结构体指针过去。

传参访问,要注意接收函数的参数类型要相同

typedef struct birthday
{
	int month;
	int day;
}Birthday;

typedef struct student
{
	char name[15];//名字
	int grade;//分数
	int num;//学号
	char sex[10];//性别
	Birthday data;
}Student;

void PrintStu(Student A)//这里给什么名字都可以,形参的名字
{
	printf("%s %d %d %s %d %d", A.name, A.grade, A.num, A.sex, A.data.month, A.data.day);
}

int main()
{
	Student xiaohong = { "xiaohong",99,6666,"女" ,{4,4} };
	Student* ptr = &xiaohong;
	//(*ptr).grade = 100;
	PrintStu(xiaohong);
	return 0;
}

我们可以访问,打印结构体成员的信息。

我们尝试着修改一个变量

void PrintStu(Student A)//这里给什么名字都可以,形参的名字
{
	printf("%s %d %d %s %d %d", A.name, A.grade, A.num, A.sex, A.data.month, A.data.day);
	A.grade = 100;
}

将分数改为100,在主函数再打印一次。

可以发现两次打印的数据没有变化,形参改变不影响实参。

传入结构体指针

  

再次运行

  

共用体

共用体和结构体很相似,只不过关键字由struct变为了union,区别在于:结构体为所有成员变量开独立的内存,而共用体定义了一块能容纳所有数据成员共享的内存。这也就确定了

声明形式如下

union 共用体名

{

        成员列表;

};

定义一个结构体

union Data

{
        int i;

        char c;

        double d;

}

他和结构体的引用和初始化一模一样,不再赘述。

有必要谈一谈的是共用体类型的数据特点

1,同一段内存可以用来存放几种不同类型的成员,但是每次只能够存放他们其中的一种,而不能同时存放所有的类型,这也代表在共用体中,同事只能有一个成员起作用,其他成员不起作用。

2,共用体初始化后起作用的成员是最后一次存放的成员,再存入一个新的值后,前边的所有成员就失去作用,如果要调用其中的某个成员,那么该成员就起作用,另外的成员不起作用。

3,共用体变量的地址和他各个成员的地址相同。

4,因为共用体的地址和各成员地址一样,不能对共用体变量名赋值,也不可以企图引用变量来得到一个值,违反了程序的确定性。

枚举类型

利用关键字enum可以声明枚举类型,这也是一种数据类型,使用枚举类型可以定义枚举类型变量,几个枚举变量为一组同类型的标识符,每个标识符都对应一个整数值,称为枚举常量。

定义一个枚举类型变量

enum Colors{

        RED,

        GREEN,

        BLUE

};

在括号中,第一个标识符就对应1,第二个对应2,以此类推。

每个标识符都必须是独特嘚!

也可以为某个标识符设置其对应得整形值,后边的标识符依次加一。

例如:

enum Colors{

        RED=1,

        GREEN,

        BLUE

};

此时GREEN就是2,BLUE就是3。

枚举类型通常和switch配合使用,case后边只能是整形数字,使用枚举就解决了这一问题,让代码功能更加清晰。

代码如下

typedef enum Colors
{
	RED = 1,
	BLUE,
	GREEN
}color;
int main()
{
	int icolor;
	scanf("%d", &icolor);
	switch (icolor)
	{
	case RED:
		printf("RED\n");
		break;
	case BLUE:
		printf("BLUE\n");
		break;
	case GREEN:
		printf("GREEN\n");
		break;
	default:
		break;
	}
	return 0;
}

结构体内存对齐

结构体内存对齐是一个十分热门的考题,这里一定要正确记住内存对齐的规则。

结构体的的大小不是里面变量类型的大小累加得到的,而是通过默认的结构体对齐规则,再通过计算得到的。

要记住的是

1,第一个成员在偏移量为0处。

2,第一个后边的成员对齐到变量大小与最小对齐数中小的那个的整数倍处。

        对齐数:编译器默认的一个最小对齐数和该成员变量大小的较小值

        VS:最小对齐数默认为8

        可以用#pragma pack(4)更改默认对齐数

        Linux:没有默认对齐数,对齐数就是成员函数本身。

3,结构体总大小为最大对齐数的整数倍

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

为什么存在内存对齐?

1,平台原因

不是所有的硬件都能访问任一地址上的任意数据,某些平台只能在某些地址处取出某些特定类型的数据,不然会报错,为了互容,出现了内存对齐。

2,性能原因

数据的结构应该尽可能在自然边界上对齐,原因在于为了访问没有对齐的内存,处理器需要两次内存访问,而对齐的内存只需要访问一次即可。也就是用空间来换时间。

结构体大小计算

看下边两种结构体

struct S1
{
	char c1;
	int i;
	char c2;
};
struct S2
{
	char c1;
	char c2;
	int i;
};

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

打印结果

一定要记住,内存对齐是从0开始的,所以所占内存是对齐到的位置加1。

第一个结构体:

不信的话我们可以通过offsetof函数来查看结构体成员,不要忘记包含头文件stddef.h

 代码如下

#include <stddef.h>
struct S1
{
	char c1;
	int i;
	char c2;
};
struct S2
{
	char c1;
	char c2;
	int i;
};
int main()
{
	printf("%d\n", offsetof(struct S1, c1));
	printf("%d\n", offsetof(struct S1, i));
	printf("%d\n", offsetof(struct S1, c2));
	printf("%d ", sizeof(struct S1));
	printf("%d ", sizeof(struct S2));
	return 0;
}

运行结果如图所示

验证了我们的猜想。

第二个结构体:

可以发现,结构体内部装有相同的数据,只不过排布顺序不一样,就产生了4个字节的浪费,这只不过是两个小小的结构体,才四个字节,然而万一是一个链表呢?成千上万个节点,每个节点浪费4个字节,那就开销很大了,所以我们也要注意结构体的排布问题。

修改默认对齐数求大小

#pragma pack(4)
//更改默认对齐数
struct A
{
	char c1;
	int i;
    char c2;
	double d;	
};

这个结构体的大小是多少呢?

要注意的是,默认对齐数已经被更改了,double类型的数据不会对齐到8的倍数,而是修改后的VS提供的默认对齐数。

在VS里运行一下看一看

没有问题!如果没有修改默认对齐数的话,就会对齐至16的位置,从16往后走8个字节,23-16+1=8(包含16,所以停在23的位置),23-0+1=24(从零开始,故-0+1),刚好是8的倍数,将更改默认对齐数的代码注释再次运行

结果如我们所料。

嵌套结构体计算大小

再建造一个结构体,嵌套后观察大小,上边的结构体大小没有修改默认对齐数的话大小为24。

代码如下

struct A
{
	char c1;
	int i;
	char c2;
	double d;
};
struct B
{
	char c;
	struct A a;
	int k;
};
int main()
{
	printf("%d ", sizeof(struct A));
	printf("%d ", sizeof(struct B));

	return 0;
}

结果是多少呢?

我们来推导一下:

在VS里跑一下验证结果是否正确

结构体的内存对齐规则和大小计算你学费了吗?

联合体的大小计算

union Un
{
	short s[7];
	int n;
};
int main()
{
	printf("%d ", sizeof(union Un));

	return 0;
}

联合体只会开辟最大的一个成员的内存,第一个成员的内存为14,int的内存为4,所以选择第一个成员,默认对齐数为8,最后的结果为8的倍数,故最终联合体的大小为16。

运行代码后结果如下

ok.今天的文章就结束啦,欢迎大家一起交流进步!

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

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

相关文章

老年少女测试媛入职感想

作为一枚从事通信行业测试的老年少女测试媛&#xff0c;入职离职也有两三次了。现在又在一家企业入职了。虽然心里也清楚离职和入职&#xff0c;无非也就是从一个公司的坑里跳出来&#xff0c;再跳到另外一个公司的坑里罢了&#xff0c;明明知道老东家的坑是填不完的了&#xf…

【Java 进阶篇】Java Request 获取请求头数据详解

在Java Web开发中&#xff0c;获取HTTP请求的请求头数据是一项常见任务。HTTP请求的请求头包含了客户端发送给服务器的额外信息&#xff0c;这些信息对于服务器来说很重要&#xff0c;因为它们可以包含用户代理、授权信息、Cookies等内容。在Java中&#xff0c;可以使用HttpSer…

二分查找法(查找左右端点)

前言 本文将会向您介绍二分查找法&#xff08;查找左右端点&#xff09;&#xff0c;关于朴素的二分查找法已经在之前讲过了朴素二分查找您可以点此超链接 查找右端点 如果您仅仅是想要参考如何查找左右端点&#xff0c;可以直接跳转到下文的模板处 ps&#xff1a;以下是本…

Linux常用的指令(2023.10.27)

文章目录 查看目录下文件的大小虚拟环境相关删除虚拟环境多版本的cuda切换修改虚拟环境名称 文件的移动、删除和复制文件的复制文件的删除文件的移动 查看目录下文件的大小 du&#xff08;disk usage 磁盘使用率&#xff09;命令查看当前目录和子目录文件夹、文件大小情况 du …

YOLOv7优化:渐近特征金字塔网络(AFPN)| 助力小目标检测

💡💡💡本文改进:渐近特征金字塔网络(AFPN),解决多尺度削弱了非相邻 Level 的融合效果。 AFPN | 亲测在多个数据集能够实现涨点,尤其在小目标数据集。 收录: YOLOv7高阶自研专栏介绍: http://t.csdnimg.cn/tYI0c ✨✨✨前沿最新计算机顶会复现 🚀🚀🚀…

开源投票系统源码至尊版带礼物道具 无限多开 盈利模式超丰富

网络投票系统已经成为各种活动、比赛、评选等场景中不可或缺的一部分。春哥团队开源投票系统源码至尊版是一种功能强大、灵活可定制的投票系统&#xff0c;不仅具有高度的安全性和稳定性&#xff0c;还支持多种盈利模式&#xff0c;含完整版代码包&#xff0c;支持投票礼物道具…

设备的分配与回收(考虑因素,数据结构,分配步骤)

目录 1.设备分配时应考虑的因素1.设备的固有属性2.设备分配算法3.设备分配中的安全性1.安全分配方式2.不安全分配方式 2.静态分配与动态分配3.设备分配管理中的数据结构1.“设备、控制器、通道”之间的关系2.设备控制表&#xff08;DCT)3.控制器控制表(COCT)4.通道控制表&#…

P1868 饥饿的奶牛

根据题意可以知道是一个动态规划&#xff0c;看完数据范围之后可以知道是一个线性DP。 解决方法有点类似于背包问题&#xff0c;枚举背包的每一个空间。 如果把坐标轴上每个点都看成一个块儿&#xff0c;只需要按顺序求出前 i 个块儿的最大牧草堆数&#xff0c;f[i] 就是前i的…

基于机器视觉的火车票识别系统 计算机竞赛

文章目录 0 前言1 课题意义课题难点&#xff1a; 2 实现方法2.1 图像预处理2.2 字符分割2.3 字符识别部分实现代码 3 实现效果最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 基于机器视觉的火车票识别系统 该项目较为新颖&#xff0c;适合作为竞赛…

BUUCTF zip伪加密 1

BUUCTF:https://buuoj.cn/challenges 题目描述&#xff1a; 下载附件&#xff0c;得到一个zip压缩包。 密文&#xff1a; 解题思路&#xff1a; 1、刚开始尝试解压&#xff0c;看到了flag.txt文件&#xff0c;但需要解压密码。结合题目&#xff0c;确认这是zip伪加密&#…

【0基础学Java第一课】-- 初始Java

目录 1. 初识java1.1 Java是什么1.2 Java应用领域1.3 Java语言发展简史1.4 Java语言特性1.5 JRE与JDK1.6 Java开发环境1.6.1 安装JDK1.6.2 配置环境变量 1.7 初始Java中main函数1.7.1 JDK、JRE、JVM之间的关系 1.8 注释1.9 标识符1.10 关键字 1. 初识java 1.1 Java是什么 Jav…

AXI-Stream协议详解(3)—— AXI4-Stream IP核原理分析

一、前言 在之前的文章中&#xff0c;我们介绍了AXI-S协议的一些基础知识&#xff0c;这是我们进行本文学习的前置基础&#xff0c;因此建议在开始本文章的学习前&#xff0c;完整阅读以下两篇文章&#xff1a; AXI-Stream协议详解&#xff08;1&#xff09;—— Introduction…

泛微OA之获取每月固定日期

文章目录 1.需求及效果1.1需求1.2效果 2. 思路3. 实现 1.需求及效果 1.1需求 需要获取每个月的7号作为需发布日期&#xff0c;需要自动填充1.2效果 自动获取每个月的七号2. 思路 1.功能并不复杂&#xff0c;可以用泛微前端自带的插入代码块的功能来实现。 2.将这需要赋值的…

Python:一个函数可以被多个装饰器装饰

理解&#xff1a; 规律&#xff1a; 一个函数可以被多个装饰器装饰. wrapper1 wrapper2 def target():print(我是目标)规则和规律 wrapper1 wrapper2 TARGET wrapper2 wrapper1def wrapper1(fn): # fn: wrapper2.innerdef inner(*args, **kwargs):print("这里是wrapper1 …

EtherCAT主站SOEM-- 0 SOEM下载编译及文件功能介绍

0 介绍EtherCAT主站SOEM文件及主要功能函数 1. soem介绍&#xff1a;2 soem主要功能文件说明&#xff1a;3 soem下载链接4 编译soem4.1 Windows (Visual Studio)&#xff1a;4.2 Linux & macOS&#xff1a; 该文档修改记录&#xff1a;总结 1. soem介绍&#xff1a; SOEM&…

BUUCTF 刮开有奖 1

这题使用IDA反汇编的话有windows编程基础会好些&#xff0c;看不懂跟着思路来也行 文章目录 一、基本分析二、代码分析第一处判断疑问 第二个判断第三处判断第四处判断第五处判断第五处判断 三、flag四、最后 一、基本分析 运行后 然后就什么都没有了 IDA反汇编 紫色颜色的函…

异常---

目录 认识异常 自定义异常 认识异常 1.异常是什么&#xff1f; 2&#xff0e;异常的代表是谁&#xff1f;分为几类&#xff1f; Error &#xff1a;代表的系统级别错误&#xff08;属于严重问题&#xff09;&#xff0c;也就是说系统一旦出现问题&#xff0c; s u n 公司会把…

RDMA概览

RDMA(Remote Direct Memory Access&#xff0c;远程直接内存访问)&#xff0c;指能够访问(读写)远程机器的内存。有多种支持RDMA的网络协议&#xff0c;包括&#xff1a;Infiniband、RoCE和iWAPP。具体的API定义包含在内核文件linux/include/rdma/ib_verbs.h reference: 【精选…

02【Git分支的使用、Git回退、还原】

上一篇&#xff1a;01【Git的基本命令、底层命令、命令原理】 下一篇&#xff1a;03【Git的协同开发、TortoiseGit、IDEA的操作Git】 文章目录 02【Git分支的使用、Git回退、还原】一、分支1.1 分支概述1.1.1 Git分支简介1.1.2 Git分支原理 1.2 创建分支1.2.1 创建普通分支1.…

如何理解my_map.yaml中origin的含义

当然可以。首先,我们先了解一下2D地图的基本构成。2D地图实际上是一个网格系统,其中每个单元格(或像素)代表现实世界中的一个区域。当我们谈论origin时,我们实际上是在描述这个网格如何在真实的3D空间中放置。 让我们通过一个简单的示意图来解释: 假设上面的矩形表示一个…