C中自定义类型——结构体

一.前言

在C语言中,不仅有int、char、short、long等内置类型,C语言还有一种特殊的类型——自定义类型。该类型可以由使用者自己定义,可以解决一些复杂的个体。

二.结构体

2.1结构体的声明

我们在利用结构体的时候一般是用于描述一些有多种因素的对象。比如我们要描述一个学生,那么学生就有他的姓名,性别,年龄,如果我们使用内置类型的话,非常麻烦,我们就可以定义一个结构体类型用来描述一个学生:

//定义了一个学生类型
struct student
{
	char name[15];//学生姓名
	char sex[5];//性别
	int age;//年龄
};

 我们创建了类型之后还得依靠此类型创建变量。

2.2结构体变量的创建和初始化

我们要时刻记住,我们创建的是一个类型,而不是一个变量,我们要根据此类型来创建我们需要的变量。 下来,我们依据上面创建的学生类型来创建变量并进行初始化:

#include <stdio.h>

//定义了一个学生类型
struct student
{
	char name[15];//学生姓名
	char sex[5];//性别
	int age;//年龄
};

int main()
{
	//在创建变量的同时进行初始化
	//该初始化必须得按照结构体内部成员的顺序进行初始化
	struct student stu1 = { "zhangsan","nan",18 };

	printf("%s\n", stu1.name);
	printf("%s\n", stu1.sex);
	printf("%d\n", stu1.age);

	//利用访问操作符可实现自定义顺序进行初始化
	struct student stu2 = { .age = 21,.name = "lisi",.sex = "nv" };

	printf("%s\n", stu2.name);
	printf("%s\n", stu2.sex);
	printf("%d\n", stu2.age);

	return 0;
}

我们除了在主函数内创建结构体变量外,还可以在定义结构体的同时进行创建变量:

#include <stdio.h>

struct student
{
	char name[15];
	char sex[5];
	int age;
}stu1;

int main()
{
	struct student stu2 = { 0 };

	return 0;
}

stu1和stu2都是结构体类型变量,区别是stu1是全局变量存放在静态区,而stu2是局部变量存放在栈上。

2.3结构体访问操作符

结构体访问操作符有.(句点操作符)和->(箭头操作符)。句点操作符用于结构体变量的访问,而箭头操作符用于结构体指针来访问结构体变量。该操作符我在之前已经介绍过了,大家可以看我之前的博客。结构体中的访问运算符-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/xsc2004zyj/article/details/136722599?spm=1001.2014.3001.5502

 2.4结构体的特殊声明

结构体除了正常的声明外,还有一种特殊的声明方式,叫做匿名结构体。

匿名结构体就是在声明结构体的时候不给结构体标识符,而直接创建一个变量。

//匿名结构体,该结构体没有标识符
struct
{
	char name[15];
	char sex[5];
	int age;
}stu;

而对于匿名结构体类型来说,该结构体基本上只能使用一次,因为该结构体没有标识符,后续没法再对此来创建变量。

下面提出一个问题:

#include <stdio.h>


struct
{
	int a;
	char b;
	float c;
}x;

struct
{
	int a;
	char b;
	float c;
}*p;

int main()
{
	p = &x;
	return 0;
}

主函数中的p = &x;是否合法?

答案是:非法,因为以上两个结构体的成员变量完全一样,并且都使用匿名的方式,所以编译器会把两个声明当成两个完全不同的类型,所以是非法的。

2.5结构体的自引用 

如果我们在描述某个对象的时候,需要用到结构体的自引用,我们应该如何写?

struct stu
{
	int age;
	struct stu s;
};

上面代码正确么?如果正确,那么sizeof(struct stu)的大小是多少呢?上面的代码其实是错误的,如果这样进行自引用,那么一个结构体变量的空间将会无穷大,因为一个结构体里面永远还有一个结构体。

正确的应该是存放一个该结构体的指针,因为一个指针的大小不是四个字节,就是八个字节。而且该指针也指向了一个该类型的变量,所以也实现了结构体的自引用。

struct stu
{
	int age;
	struct stu *s;
};

2.6利用typedef重命名结构体类型

我们在创建结构体变量的时候每次都要写出struct tag x;这样写起来非常麻烦,并且有人可能粗心大意而忘记struct。所以,我们在声明结构体的时候,可以利用typedef关键字给该结构体重新起个名字,用该名字来创建变量。

#include <stdio.h>

typedef struct student
{
	char name[14];
	int age;
}stu;

int main()
{
	stu s;
	struct student s2;
	return 0;
}

此时,在声明时分号前面就不是创建变量了,还是该结构体的一个新名字——stu。在创建变量的时候,stu和struct student是一个意思。

 我们也可以利用typedef来解决匿名结构体的问题,我们只需要给匿名结构体重新起一个名字,利用该名字创建变量就行了。这时,该匿名结构体与正常声明的结构体没有区别。

#include <stdio.h>

typedef struct
{
	char a;
	int b;
}X;

int main()
{
	X x;
	X s;
	return 0;
}

这样就解决了匿名结构体只能使用一次的局限。我们可以利用重命名的结构体进行创建变量,初始化等操作

 三.结构体内存对齐

我们已经基本了解了结构体的内容,下来我们来讨论结构体中的热门话题:结构体的大小。这也是最近热门的考点:结构体内存对齐。

3.1对齐规则

在理解内存对齐之前,我们得先了解结构体内存对齐的规则。

  1. 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数 = 编译器默认的一个对齐数 与 该成员变量大小间的较小值。VS上的默认对齐数 = 8。Linux中gcc没有默认对齐数,对齐数就是成员自身的大小
  3. 结构体的总大小等于最大对齐数(结构体的每一个成员变量都有对齐数,所有对齐数中最大的一个)的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。

 下来我来一一介绍结构体的对齐规则。我们通过一个练习来引出:

struct S1
{
	char c1;
	int i;
	char c2;
};
int main()
{
	printf("%d\n", sizeof(struct S1));

	return 0;
}

我们利用该结构体来进行解说,判断该结构体的大小。有的人会想,该结构体的成员两个char,一个int,那就占6个字节,是这样么?

我们看到,结果是12个字节,与我们的猜测不符。这就是因为结构体内部存在内存对齐规则。

在上图,我借助练习题详细介绍了结构体对齐规则如何理解,以及内存是如何进行对齐的。大家先仔细理解上图,后进行下面几个结构体大小的练习。

3.1.1练习1

#include <stdio.h>

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

int main()
{
	printf("%zd\n", sizeof(struct S2));
	return 0;
}

 

3.1.2练习

#include <stdio.h>

struct S3
{
	double d;
	char c;
	int i;
};

int main()
{
	printf("%zd\n", sizeof(struct S3));
	return 0;
}

 

3.1.3练习3

#include <stdio.h>
//结构体嵌套

struct S3
{
	double d;
	char c;
	int i;
};

struct S4
{
	char c1;
	struct S3 s3;
	double d;
};

int main()
{
	printf("%zd\n", sizeof(struct S4));
	return 0;
}

3.2为什么存在内存对齐

大部分的参考资料是这样说的:

3.2.1平台原因(移植原因):

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

3.2.2性能原因:

数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用⼀个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。

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

 3.2.3那么如何尽可能节省空间的浪费呢?

我们来分析一下,这两个结构体:

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

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

S1和S2的成员类型都一样,只是存储的顺序不同,那这两个结构体的大小是否一样大呢?

我们看到S1和S2的成员虽然相同,但是两者的大小却不同,我们来分析一下。

 那么,我们在设计结构体的时候,既要满足对齐,又要节省空间,如何做到:

让占用空间小的成员尽量集中到一起。

 3.2.4修改默认对齐数

#pragma这个预处理指令可以修改编译器的默认对齐数

//利用#pragma修改默认对齐数

#include <stdio.h>
#pragma pack(1)//设置默认对⻬数为1 
struct S
{
	char c1;
	int i;
	char c2;
};
#pragma pack()//取消设置的对⻬数,还原为默认 

int main()
{
	//输出的结果是什么? 
	printf("%d\n", sizeof(struct S));
	return 0;
}

我们在上面已经分析该结构体的大小为12个字节(在VS默认8为对齐数的情况下),而我们利用#pragma已经将编译器的默认对齐数修改为1,结果还是12个字节么?

 我们分析后得出,修改默认对齐数后该结构体的大小变成了6字节。

四.结构体传参

我们创建好结构体变量后,可能需要把该结构体变量的某个成员变量传给某个函数。而我们到现在由两种传参的方式:值传递地址传递。那我们应该选择哪一种方式对结构体变量进行传参呢?

//结构体传参

#include <stdio.h>

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

//值传递
void test1(struct S s)
{
	printf("%c\n", s.c1);
	printf("%c\n", s.c2);
	printf("%d\n", s.i);
}

//地址传递
void test2(struct S* ps)
{
	printf("%c\n", ps->c1);
	printf("%c\n", ps->c2);
	printf("%d\n", ps->i);
}

int main()
{
	struct S s = { 'a','b',10 };
	test1(s);
	test2(&s);
	return 0;
}

我们看到,无论是值传递,还是地址传递,都可以达到我们的目的。那我们到底应该选那种方式呢?

我们首先要知道,值传递中的形参是实参的一份临时拷贝,会在栈上存储其拷贝内容,也就意味着会消耗内存,那如果该结构体的大小非常大的话,我们在栈上就会消耗很多内存。

无论是何种传参方式,都会进行压栈操作,而值传递在压栈过程中机会在时间和空间上有大量的系统开销。而地址传递的话,指针的大小不是8个字节就是4个字节,在压栈过程中不会有太多空间和时间上的开销。 

所以,在结构体传参的时候,要传结构体的地址

 五.结构体实现位段

利用结构体实现位段。位段这个概念大家可能没听过,但段位大家肯定不陌生。位段是一种特殊的结构体类型,位段必须得依靠结构体来实现。

5.1什么是位段?

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

  1. 位段的成员必须是int 、unsigned int、或者signed int,在C99中位段成员的类型也可以选择其他类型。
  2. 位段的成员后面有一个冒号和一个数字。

 比如:

struct S
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _e : 30;
};

 通常在创建位段式结构体的时候习惯在每个变量前面加上下划线,以此来说明这是位段式结构体。

我们看到结构体和位段式结构体的区别就在于位段的每个成员后面多了一个冒号和一个数字,这是什么意思呢?

这每个成员后面的数字表示该成员占的bit位,_a占2个bit位,_b占5个bit位,_c占10个bit位。为什么要这样设置变量呢?

我们想,如果_a里面只会存1,2,3这三个数,1二进制就是01,2二进制就是10,3二进制就是11,所以存这三个数用两个bit位就够了。以此类推,_b就是存只需5个bit位就能表示的数,_c就是只存10个bit位就能表示的数。这样就可以大大减少空间的浪费。那位段式结构体是怎样储存数据的呢?是如何达到节省空间的?下面我们来了解位段的内存对齐规则。

5.2位段的内存对齐

位段的成员最好都是同一种类型的,位段会根据成员类型来开辟空间,比如成员是int型,就一次开辟4个字节,如果是char型,就一次开辟一个字节。下面我们举一个例子。

 那该位段的大小是不是跟我的结论一样呢?

 我们看到,该位段的大小与我们的推测相同。我们再来看一下该位段是如果写入数据的。

 我们分析完之后,在VS上调试一下,发现s的内存如下,和我们分析的一样,占三个字节,存储的是0a 0c 05。

 5.3位段的跨平台问题

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

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

 5.4位段的应用

下图是网络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要几个bit位就能描述,这里使用位段,能够实现想要的效果,也节省了空间,这样网络传输的数据报大小也会较小⼀些,对网络的畅通是有帮助的。

 5.5位段使用的注意事项

位段的几个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的。

 所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输入值,只能是先输入放在⼀个变量中,然后赋值给位段的成员。

#include <stdio.h>
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/535248.html

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

相关文章

代码随想录算法训练营第三十一天| 455.分发饼干、376.摆动序列、53.最大子序和

系列文章目录 目录 系列文章目录455.分发饼干贪心算法大饼干喂胃口大的&#xff08;先遍历胃口&#xff09;胃口大的先吃大饼干(先遍历饼干&#xff09;小饼干先喂胃口小的&#xff08;先遍历胃口&#xff09;胃口小的先吃小饼干&#xff08;先遍历饼干&#xff09; 376. 摆动序…

Claude使用教程

claude 3 opus面世后&#xff0c;网上盛传吊打了GPT-4。网上这几天也已经有了许多应用&#xff0c;但竟然还有很多小伙伴不知道国内怎么用gpt&#xff0c;也不知道怎么去用这个据说已经吊打了gpt-4的claude3。 今天我们想要进行的一项尝试就是—— 用claude3和gpt4&#xff0c…

2024-04-11最新dubbo+zookeeper下载安装,DEMO展示

dubbozookeeper下载安装 下载zookeeper&#xff1a; 下载地址 解压&#xff0c;并进入bin目录&#xff0c;启动 如果闪退可以编辑脚本&#xff0c;在指定位置加上暂停脚本 报错内容说没有conf/zoo.cfg&#xff0c;就复制zoo_sample.cfg重命名为zoo.cfg 再次启动脚本&#x…

HarmonyOS开发实例:【手势截屏】

介绍 本篇Codelab基于手势处理和截屏能力&#xff0c;介绍了手势截屏的实现过程。样例主要包括以下功能&#xff1a; 根据下滑手势调用全屏截图功能。全屏截图&#xff0c;同时右下角有弹窗提示截图成功。根据双击手势调用区域截图功能。区域截图&#xff0c;通过调整选择框大…

Excel 记录单 快速录入数据

一. 调出记录单 ⏹记录单功能默认是隐藏的&#xff0c;通过如下如图所示的方式&#xff0c;将记录单功能显示出来。 二. 录入数据 ⏹先在表格中录入一行数据&#xff0c;给记录单一个参考 ⏹将光标至于表格右上角&#xff0c;然后点击记录单按钮&#xff0c;调出记录单 然后点…

百元不入耳运动耳机哪个品牌好?五款业内顶尖品牌推荐

在追求舒适与健康的运动中&#xff0c;不入耳式&#xff08;开放式耳机&#xff09;运动耳机逐渐成为了许多运动爱好者的首选&#xff0c;它们不仅避免了长时间佩戴耳机带来的不适&#xff0c;还能在享受音乐的同时保持对环境的警觉&#xff0c;确保运动安全&#xff0c;市场上…

Python中同时调用多个列表

如果你有多个列表&#xff0c;想要同时迭代它们&#xff0c;可以使用zip()函数。zip()函数可以将多个可迭代对象合并成一个元组的迭代器&#xff0c;然后你可以在循环中使用它。 问题背景 当需要在Python脚本中避免重复相同任务时&#xff0c;可以使用for循环来遍历列表。但是…

Volatility-内存取证案例1-writeup--xx大赛

题目提示&#xff1a;flag{中文} 按部就班 &#xff08;1&#xff09;获取内存镜像版本信息 volatility -f 文件名 imageinfo 通过上述可知&#xff0c;镜像版本为Win7SP1X64。 &#xff08;2&#xff09;获取进程信息&#xff1a; volatility -f 镜像名 --profile第一步获取…

面壁智能完成新一轮数亿元融资,继续面向AGI的高效大模型征程

近日&#xff0c;面壁智能完成新一轮数亿元融资&#xff0c;由春华创投、华为哈勃领投&#xff0c;北京市人工智能产业投资基金等跟投&#xff0c;知乎作为战略股东持续跟投支持。本轮融资完成后&#xff0c;面壁智能将进一步推进优秀人才引入&#xff0c;加固大模型发展的底层…

6.12物联网RK3399项目开发实录-驱动开发之UART 串口的使用(wulianjishu666)

嵌入式实战开发例程【珍贵收藏&#xff0c;开发必备】&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1tkDBNH9R3iAaHOG1Zj9q1Q?pwdt41u UART 使用 简介 AIO-3399J 支持 SPI 桥接/扩展 4 个增强功能串口&#xff08;UART&#xff09;的功能&#xff0c;分别为 UA…

LeetCode 面试题 02.07.链表相交(判断两个结点是否相同)

给你两个单链表的头节点 headA 和 headB &#xff0c;请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点&#xff0c;返回 null 。 图示两个链表在节点 c1 开始相交&#xff1a; 题目数据 保证 整个链式结构中不存在环。 注意&#xff0c;函数返回结果后&#x…

Qt中的网络通信

C没有封装专门的网络套接字的类&#xff0c;因此C只能调用C对应的API&#xff0c;而在Linux和Windows环境下的API都是不一样的 Qt作为一个C框架提供了相关封装好的套接字通信类 在Qt中需要用到两个类&#xff0c;两个类都属于network且都是属于IO操作&#xff0c;只不过这两个类…

ArcGIS Desktop使用入门(三)图层右键工具——缩放至图层、缩放至可见

系列文章目录 ArcGIS Desktop使用入门&#xff08;一&#xff09;软件初认识 ArcGIS Desktop使用入门&#xff08;二&#xff09;常用工具条——标准工具 ArcGIS Desktop使用入门&#xff08;二&#xff09;常用工具条——编辑器 ArcGIS Desktop使用入门&#xff08;二&#x…

Java 怎么捕捉 Windows 中前台窗口的改变?

在Java中捕捉Windows中前台窗口的改变通常需要使用JNI&#xff08;Java Native Interface&#xff09;来调用Windows API。Windows API提供了一系列函数来获取有关窗口和进程的信息&#xff0c;通过使用这些函数&#xff0c;我们可以实现在Java程序中监视和捕捉Windows前台窗口…

抖音爬虫——点赞量

该爬虫模拟了一个get请求来得到返回json里面的点赞量信息 下面介绍如何使用&#xff1a; 首先&#xff0c;我们找一个浏览器打开抖音搜索具体的关键词 接着我们点击键盘的F12建 就会出现如下的界面&#xff0c;接着我们点击网络&#xff08;可能再一些浏览器是叫network&…

JavaSE:this关键字(代码和内存图讲解)

this的含义 this代表当前对象&#xff0c;谁调用this所在的方法&#xff0c;this就代表谁 这句话非常重要 demo 以这段代码为例&#xff0c;setNum方法内部的this&#xff0c;setStr方法内部的this&#xff0c;还有构造方法ThisKeyword(int num, String str)内部的两个this…

软件库V1.2版本开源-首页UI优化

iAppV3源码&#xff0c;首页的分类更换成了标签布局&#xff0c;各位可以参考学习&#xff0c;界面名称已经中文标注&#xff01; 老版本和现在的版本还是有较大的区别的&#xff0c;建议更新一下&#xff01; 新版本改动界面如下&#xff1a; 1、首页.iyu&#xff1a;分类按…

基于javassm实现的幼儿教育管理系统

开发语言&#xff1a;Java 框架&#xff1a;ssm 技术&#xff1a;JSP JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09; 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;eclipse/myeclip…

晶核职业选择:六大角色技能揭秘,成为战斗高手!

在晶核的世界中&#xff0c;每一位玩家都扮演着不同角色&#xff0c;组成多样的团队&#xff0c;共同踏上探索未知的征程。而每个角色都有其独特的技能和特点&#xff0c;下面将为你详细介绍每个角色的技能搭配和操作技巧&#xff0c;让你在战斗中游刃有余&#xff0c;一展自己…

MPT - 原理及应用

前文回顾 Merkle原理及应用Merkle代码实现Patricia原理及应用Patricia代码实现 什么是MPT&#xff08;Merkle Patricia Tree&#xff09;树 MPT树是一种数据结构&#xff0c;用于在以太坊区块链中高效地存储和检索账户状态、交易历史和其他重要数据。MPT树的设计旨在结合Merk…