自定义类型: 结构体、枚举 、联合

目录

结构体

结构体类型的声明

匿名结构体

结构的自引用

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

结构体成员变量的访问

结构体内存对齐

结构体传参

位段

位段类型的声明

位段的内存分配

位段的跨平台问题

位段的应用

枚举

枚举类型的定义

枚举的优点

联合体(共用体)

联合类型的定义

联合体的特点

联合体的大小

联合体的应用


结构体

结构体类型的声明

之前学习到的数据类型我们称之为内置类型,如int, double, char, float等,后续还学习了数组,数组是一组相同类型元素的集合,但描述一个事物通常需要用到不同的类型,比如要描述一个学生,有年龄,姓名,学号等,就会出现各种类型的字段,这些叫做结构体的成员,结构体的每个成员额可以是不同的变量类型!

struct Stu是一个自定义的结构体类型, struct是结构体关键字,Stu是结构体标签(tag)

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

在使用时我们感觉结构体类型太长了,可以typedef进行类型重定义

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

匿名结构体

在声明结构体类型的时候,可以不完全的声明,也就是省略掉结构体标签, 称之为匿名结构体

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

建议不要使用匿名结构体,可读性不是很好,就按照最标准的 struct + 结构体标签 来创建结构体

结构的自引用

在结构中包含一个类型为该结构本身的成员是否可以呢?

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

这样写是不可以的,因为sizeof(struct Node)是无法计算的,用该结构体类型创建变量时开辟的空间大小也就是未知的,因此是错误的写法

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

这样写就是可以的,第二个成员变量是一个结构体指针,固定大小是4/8个字节, 这也是数据结构中链表的每个节点的结构体定义方式

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

这样写是不可以的,因为定义结构体类型时第二个成员变量用到了typedef后的类型,而此时结构体还没有创建完成,而结构体要创建完成,第二个成员变量就应该定义完成了,这就是先有鸡还是先有蛋的问题了!

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

这样写就是可以的,第二个成员变量定义时使用的是 struct Node, 已经有该类型了!

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

●定义全局变量并初始化

#include <stdio.h>//声明结构体类型的同时初始化变量(定义+赋初值)
struct Stu
{char name[20];//名字int age;//年龄char sex[5];//性别char id[20];//学号
}s1 = { "zhangsan", 18, "mail", "20221931" };   //全局变量struct Stu s2; //全局变量int main()
{s2 = { "lisi", 20, "femail", "31931313"};return 0;
}

●定义局部变量并初始化

#include <stdio.h>
struct Stu
{char name[20];//名字int age;//年龄char sex[5];//性别char id[20];//学号
};
int main()
{struct Stu s = { "wangmazi", 23, "mail", "39133113" };return 0;
}

● 结构体的嵌套定义

#include <stdio.h>
struct score
{int x;char ch;
};
struct Stu
{char name[20];//名字int age;//年龄char sex[5];//性别char id[20];//学号struct score s; //嵌套结构体
};
int main()
{struct Stu s = { "wangmazi", 23, "mail", "39133113", {10, 'q'}};return 0;
}

结构体成员变量的访问

● 结构体变量.成员变量

● 结构体指针变量->成员变量

● (*结构体指针变量).成员变量

#include <stdio.h>
struct score
{int x;char ch;
};
struct Stu
{char name[20];//名字int age;//年龄char sex[5];//性别char id[20];//学号struct score sc; //嵌套结构体
};
int main()
{struct Stu s = { "wangmazi", 23, "mail", "39133113", {10, 'q'}};//1.结构体变量.成员变量printf("%s %d %s %s %d %c\n", s.name, s.age, s.sex, s.id, s.sc.x, s.sc.ch);//2.结构体指针变量->成员变量struct Stu* p = &s;printf("%s %d %s %s %d %c\n", p->name, p->age, p->sex, p->id, p->sc.x, p->sc.ch);//3.*(结构体指针变量).成员变量printf("%s %d %s %s %d %c\n", (*p).name, (*p).age, (*p).sex, (*p).id, (*p).sc.x, (*p).sc.ch);return 0;
}

结构体内存对齐

现在我们来讨论一下结构体的大小,结构体中包含了若干个成员变量,结构体的大小是所有成员变量大小相加吗???

#include <stdio.h>
struct s1
{char c1;int i;char c2;
};
int main()
{printf("%d\n", sizeof(struct s1)); //12return 0;
}

显然不是,结构体中的成员变量并不是挨着连续存放的,而是要遵守一定的对齐规则!

结构体内存对齐规则

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

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

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

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

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

画图解释上面结构体的大小是12:

结构体嵌套计算大小:

#include <stdio.h>
struct S3
{double d;char c;int i;
};
struct S4
{char c1;struct S3 s3;double d;
};
int main()
{printf("%d\n", sizeof(struct S3)); //16printf("%d\n", sizeof(struct S4)); //32return 0;
}

计算出 struct S3 的所有成员的最大对齐数是8,因此struct S3 的起始位置就是8的整数倍,然后strcut s3 内部的成员变量存放规则依旧遵守前三条规则,最后检查整体结构体的大小是所有成员(包括嵌套结构体成员)大小的整数倍,也就是32个字节

为啥存在结构体内存对齐?

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

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

2. 性能原因:

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

举个例子: 对于下列结构体:

struct S
{char c; int i;
};

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

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

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

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

s1结构体和s2结构体的成员变量是完全一样的,但是s2结构体的两个char类型变量在一起,所以同样遵守结构体内存对齐规则前提下,s2是更加节省空间的!

修改默认对齐数

● 使用 #pragma pack() 预处理指令修改默认对齐数

#include <stdio.h>
#pragma pack(1) //修改默认对齐数
struct S1
{char c1;int i;char c2;
};
#pragma pack() //恢复默认对齐数int main()
{printf("%d\n", sizeof(struct S1)); //6return 0;
}

结构体传参

#include <stdio.h>
#include <stddef.h>
struct S
{int data[1000];int num;
};void print1(struct S ss)
{for (int i = 0; i < 3; i++){printf("%d ", ss.data[i]);}printf("%d\n", ss.num);
}void print2(const struct S* ps)
{for (int i = 0; i < 3; i++){printf("%d ", ps->data[i]);}printf("%d\n", ps->num);
}int main()
{struct S s = { {1, 2, 3}, 100 };print1(s);print2(&s);return 0;
}

代码中有两种传参方式,可以采用代码一(传值传参),也可以采用代码二(传址传参),那么使用哪一个好呢???

建议使用传址传参,理由如下:

1. 传值传参,参数需要压栈,会有时间和空间上的系统开销

2. 如果结构体对象过大,参数压栈的系统开销比较大,导致性能下降

3. 如果要修改外部的结构体,就只能传址传参了; 如果不想修改,传址传参的形参加上const即可

位段

位段类型的声明

1.位段的成员必须是 int、unsigned int 、signed int 、char 等等,  总之,必须属于整形家族

2.位段的成员名后边有一个冒号和一个数字(表示成员占几个比特位)

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

可以看到,位段是一种节省空间的方式,int是占据4个字节,32个比特位,但是比如int flag 变量,用来标识真假,我们只需要两种状态,01 / 10, 只需要两个比特位就够了,也有很多其他类似的场景,因此使用位段可以节省空间

位段的内存分配

1. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。

#include <stdio.h>
//位段
struct A
{int _a : 2;int _b : 5;int _c : 10;int _d : 30;
};
int main()
{printf("%d\n", sizeof(struct A)); //8return 0;
}

上述代码中,struct A中的成员都是 int 类型的,因此先开辟4个字节,先把前三个成员(17个比特位)存下来,还剩余了15个比特位,不够存储_d,于是再开辟4个字节,_d就能存下了! 于是最终结构体的大小就是8个字节, 32个比特位

问题是_d的空间如何分配,是先使用剩余的15个比特位,再使用15个比特位呢? 还是直接使用新开辟的4个字节(32个比特位)中的30个比特位呢?? 答案是 不确定!

2. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。


#include <stdio.h>
//位段
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(s)); //3return 0;
}

假设:

1.每一个字节的空间存放时从右向左存放

2.当这1个字节不够下一个位段成员存储时就从下一个字节开始存

根据上面两点假设,得到如下结果:

 经过vs2022调试观察,发现vs2022的位段存储就是基于上面两点假设

位段的跨平台问题

1. int 位段被当成有符号数还是无符号数是不确定的。

2. 位段中最大位的数目不能确定。

(比如 int 整数, 16位机器最大16,32位机器最大32,写成27,在16位机器会出问题)

3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。

4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。

总结:

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

位段的应用

网络中常用到,后期网络部分的博客会有介绍

枚举

枚举类型的定义

enum Day//星期
{Mon,Tues,Wed,Thur,Fri,Sat,Sun
};enum Sex//性别
{MALE,FEMALE,SECRET
};enum Color//颜色
{RED,GREEN,BLUE
};

枚举类型中包含的成员就是一个个常量,叫做枚举常量,枚举常量是有取值的,默认从0开始,依次递增一

#include <stdio.h>
enum Day//星期
{Mon,Tues,Wed,Thur,Fri,Sat,Sun
};
int main()
{printf("%d\n", Mon); //0printf("%d\n", Tues); //1printf("%d\n", Wed); //2 printf("%d\n", Thur); //3printf("%d\n", Fri); //4printf("%d\n", Sat); //5printf("%d\n", Sun); //6return 0;
}

当然我们在定义枚举变量的时候可以赋初值,从被赋初值的枚举常量开始往后的值都是递增1

#include <stdio.h>
enum Day//星期
{Mon,Tues,Wed = 3,Thur,Fri,Sat,Sun
};
int main()
{enum Day d = Fri;printf("%d\n", Mon); //0printf("%d\n", Tues); //1printf("%d\n", Wed); //3printf("%d\n", Thur); //4printf("%d\n", Fri); //5printf("%d\n", Sat); //6printf("%d\n", Sun); //7return 0;
}

枚举的优点

1. 增加代码的可读性和可维护性

比如switch - case 进行分支判定时,可以用枚举常量代替0,1, 2等数字,可以很直观的看出某个分支的含义!
2. 和#define定义的标识符比较枚举有类型检查,更加严谨。

C语言对类型的检查不是很严格,但是C++对类型的检查更加严格,

#include <stdio.h>
enum Day//星期
{Mon,Tues,Wed = 3,Thur,Fri,Sat,Sun
};
int main()
{enum Day d = 5; //errreturn 0;
}

3. 防止了命名污染(封装)
4. 便于调试

#define定义的标识符和宏都是在编译阶段就完成替换的,而调试是将代码已经编译成了二进制程序,此时都完成了替换,调试起来的代码和最开始的就不一样了!
5. 使用方便,一次可以定义多个常量

联合体(共用体)

联合类型的定义

联合也是一种特殊的自定义类型 这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)

#include <stdio.h>
//定义一个联合体类型
union Un
{char c;int i;
};
int main()
{union Un u; //定义一个联合体变量printf("%d\n", sizeof(u)); //4printf("%p\n", &u);  //00AFFB78printf("%p\n", &u.c); //00AFFB78printf("%p\n", &u.i); //00AFFB78return 0;
}

注:取地址永远取出的是最低一个字节的地址

联合体的特点

● 由于联合成员公用一块空间,因此同一时刻只能使用其中一个联合成员

● 对一个联合成员的修改可能会影响另一个联合成员

联合体的大小

● 联合的大小至少是最大成员的大小

● 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍

● 对齐数 = 编译器的默认对齐数 和 联合体成员大小 的较小值

#include <stdio.h>
union Un1
{char arr[5];int i;
};
int main()
{printf("%d\n", sizeof(union Un1)); //8return 0;
}

联合体的大小至少是最大成员的大小,也就是数组大小是5,arr虽然是数组,但其实被看成一个一个的char,  所以对齐数是1,i 的对齐数是4,所以最大对齐数就是4,因此最终联合体的大小不是5,应该是8

联合体的应用

判断机器大小端

●大端: 数据的高字节保存在内存的低地址中, 数据的低字节保存在内存的高地址中

●小端: 数据的低字节保存在内存的低地址中, 数据的高字节保存在内存的高地址中

#include <stdio.h>
int check_sys()
{union Un{char c;int i;}u;u.i = 1;//小端: 01 00 00 00//大端: 00 00 00 01return u.c;
}
int main()
{int ret = check_sys();if (ret == 1)printf("小端\n");elseprintf("大端\n");return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【WPS】【EXCEL】将单元格中字符按照分隔符拆分按行填充到其他单元格

问题&#xff1a;实现如下图的效果 解答&#xff1a; 一、函数 IFERROR(TRIM(MID(SUBSTITUTE($A$2,",",REPT(" ",LEN($A$2))),(ROW(A1)-1)*LEN($A$2)1,LEN($A$2))),"") 二、在单元格C2中填写如下函数 三、全选要填充的单元格并且按CTRLD 函数…

BiGRU:双向门控循环单元在序列处理中的深度探索

一、引言 在当今的人工智能领域&#xff0c;序列数据的处理是一个极为重要的任务&#xff0c;涵盖了自然语言处理、语音识别、时间序列分析等多个关键领域。循环神经网络&#xff08;RNN&#xff09;及其衍生结构在处理序列数据方面发挥了重要作用。然而&#xff0c;传统的 RN…

卸载 Archiconda

一、卸载创建的虚拟环境 # 1.查看所创建的虚拟环境 conda env list# 2.一 一删除创建的虚拟环境&#xff0c;name 替换为自己创建的虚拟环境的名字 conda remove --name name --all二、卸载archidonda rm -rf ~/archiconda3三、删除conda的环境变量 外链图片转存失败,源站可…

【Java基础面试题001】Java中序列化和反序列化是什么?

在Java中&#xff0c;序列化和反序列化是用于将对象的状态保存和恢复的重要机制。 序列化 是将Java对象转换为字节流的过程&#xff0c;这样Java对象才可以网络传输、持久化存储还有缓存。Java提供了java.io.Serializable接口来支持序列化&#xff0c;只要类实现了这个接口&a…

前端学习week8——vue.js

Vue.js 基础 Vue 核心概念&#xff1a;了解 Vue 的响应式系统、组件、指令&#xff08;如 v-if、v-for、v-model 等&#xff09;。Vue 项目管理&#xff1a;学习 Vue CLI 或 Vite&#xff0c;掌握项目创建、管理和打包。推荐学习顺序&#xff1a;Vue 基础 → 组件化开发 → Vu…

Excel如何限制单元格内可选择的下拉框内容?

先选择想要的表格区域&#xff1a; 如果想要选中如下所示&#xff1a;C2格子及其下面所有的格子&#xff08;则&#xff1a;点击一下C2格子&#xff0c;然后按一下键盘&#xff1a;SHIFT CTRL ↓&#xff09; 然后在【sheet2】表&#xff0c;先填写好下拉框可选择的内容&am…

uniapp实现列表页面,实用美观

咨询列表页面 组件 <template><view><view class"news_item" click"navigator(item.id)" v-for"item in list" :key"item.id"><image :src"item.img_url"></image><view class"righ…

Linux学习笔记11 系统启动初始化,服务和进程管理(下)

前文 前文介绍了系统启动初始化程序&#xff0c;介绍了systemd的基础知识。这里主要看一下我们systemd的单元管理和常用的命令以及示例。 Linux学习笔记10 系统启动初始化&#xff0c;服务和进程管理&#xff08;上&#xff09;-CSDN博客 systemd单元管理 启动服务 这很常…

哈希表,哈希桶的实现

哈希概念 顺序结构以及平衡树中&#xff0c;元素关键码与其存储位置之间没有对应的关系&#xff0c;因此在查找一个元素 时&#xff0c;必须要经过关键码的多次比较。顺序查找时间复杂度为O(N)&#xff0c;平衡树中为树的高度&#xff0c;即 O(logN)&#xff0c;搜索的效率取决…

Maven install java heap space

Maven install java heap space 打包报错 Maven install java heap space 解决&#xff1a; vm option: -Xms1024m -Xmx1024m如果 vm配置了&#xff0c;还是一样报错&#xff0c;就重新选择JRE看看是否正确&#xff0c;idea会默认自己的环境&#xff0c;导致设置vm无效&…

aws(学习笔记第十五课) 如何从灾难中恢复(recover)

aws(学习笔记第十五课) 如何从灾难中恢复 学习内容&#xff1a; 使用CloudWatch对服务器进行监视与恢复区域(region)&#xff0c;可用区(available zone)和子网(subnet)使用自动扩展(AutoScalingGroup) 1. 使用CloudWatch对服务器进行监视与恢复 整体架构 这里模拟Jenkins Se…

【Maven】依赖管理

4. Maven的依赖管理 在 Java 开发中&#xff0c;项目的依赖管理是一项重要任务。通过合理管理项目的依赖关系&#xff0c;我们可以有效的管理第三方库&#xff0c;模块的引用及版本控制。而 Maven 作为一个强大的构建工具和依赖管理工具&#xff0c;为我们提供了便捷的方式来管…

go语言的成神之路-筑基篇-中间件

目录 单个Gin中间件 中间件简要概述 一、中间件的定义&#xff1a; 二、中间件的使用&#xff1a; 效果展示 多个Gin中间件 示例 Abort阻止后续处理函数 执行流程图 return直接返回 执行流程图 全局注册中间件 注意事项 单个Gin中间件 中间件简要概述 在 gin 框架中…

Xilinx PCIe高速接口入门实战(一)

引言&#xff1a;本文对Xilinx 7 Series Intergrated Block for PCI Express PCIe硬核IP进行简要介绍&#xff0c;主要包括7系列FPGA PCIe硬核资源支持、三IP硬核差异、PCIe硬核资源利用等相关内容。 1. 概述 1.1 7系列FPGA PCIe硬件资源支持 7系列FPGA对PCIe接口最大支持如…

【第三讲】Spring Boot 3.4.0 新特性详解:增强的配置属性支持

Spring Boot 3.4.0 版本在配置属性的支持上进行了显著增强&#xff0c;使得开发者能够更灵活地管理和使用应用程序的配置。新的特性包括对配置属性的改进、类型安全增强、以及对环境变量的更好支持。这些改进旨在提升开发效率和代码可读性&#xff0c;同时简化配置过程。本文将…

如何使用 Chrome 无痕浏览模式访问网站?

无痕浏览&#xff08;Incognito Mode&#xff09;是 Google Chrome 浏览器提供的一种隐私保护功能&#xff0c;它允许用户在一个独立的会话中浏览网页&#xff0c;而不会记录用户的浏览历史、下载历史、表单数据等。这对于希望保护个人隐私或进行临时性匿名浏览的用户来说非常有…

拥抱 OpenTelemetry:阿里云 Java Agent 演进实践

作者&#xff1a;陈承 背景 在 2018 年的 2 月&#xff0c;ARMS Java Agent 的第一个版本正式发布&#xff0c;为用户提供无侵入的的可观测数据采集服务。6 年后的今天&#xff0c;随着软件技术的迅猛发展、业务场景的逐渐丰富、用户规模的快速增长&#xff0c;我们逐渐发现过…

AI数据分析工具(二)

豆包-免费 优点 强大的数据处理能力&#xff1a; 豆包能够与Excel无缝集成&#xff0c;支持多种数据类型的导入&#xff0c;包括文本、数字、日期等&#xff0c;使得数据整理和分析变得更加便捷。豆包提供了丰富的数据处理功能&#xff0c;如数据去重、填充缺失值、转换格式等…

C/C++ 数据结构与算法 【时间复杂度和空间复杂度】【日常学习,考研必备】

一、时间复杂度 定义&#xff1a;时间复杂度描述了算法运行时间随输入大小增长而增长的趋势。它主要关注的是算法中最耗时的部分&#xff0c;并忽略常数因子、低阶项等细节。表示方法&#xff1a;通常使用大O符号&#xff08;Big O notation&#xff09;来表示时间复杂度。例如…

linux 文件权限,修改权限,c库调用

参考chmod 777 到底是啥 ???看完这个你就完全懂了&#xff01;-CSDN博客 ls -l 查看当前目录文件的权限 会有一个十位的东西 分别为 d:这是一个文件夹 后面3*3位分别表示所有者用户&#xff0c;同组用户&#xff0c;其他用户的读(r)&#xff0c;写(w)&#xff0c;执行(x)…