当前位置: 首页 > news >正文

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

目录

结构体

结构体类型的声明

匿名结构体

结构的自引用

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

结构体成员变量的访问

结构体内存对齐

结构体传参

位段

位段类型的声明

位段的内存分配

位段的跨平台问题

位段的应用

枚举

枚举类型的定义

枚举的优点

联合体(共用体)

联合类型的定义

联合体的特点

联合体的大小

联合体的应用


结构体

结构体类型的声明

之前学习到的数据类型我们称之为内置类型,如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.lryc.cn/news/495223.html

相关文章:

  • Bert+CRF的NER实战
  • 永久停用PostgreSQL 归档功能
  • 《数字图像处理基础》学习07-图像几何变换之最近邻插值法放大图像
  • pip安装库时报错(请求超时)
  • XPath表达式详解及其在Web开发中的应用
  • Qt中Socket网络编程
  • 【05】Selenium+Python 两种文件上传方式(AutoIt)
  • Python网络编程
  • openssl生成ca证书
  • Oracle RAC 环境下数据文件误建在本地目录的处理过程
  • 新质驱动·科东软件受邀出席2024智能网联+低空经济暨第二届湾区汽车T9+N闭门会议
  • windows11 使用体验记录
  • 202页MES项目需求方案深入解读,学习MES系统设计规划
  • 前端css实例
  • YOLO的框架及版本迭代
  • PotPlayer 最新版本支持使用 Whisper 自动识别语音生成字幕
  • JavaScript零基础入门速通(中)
  • 【Yarn Bug】 yarn 安装依赖出现的网络连接问题
  • 字节青训Marscode_5:寻找最大葫芦——最新题解
  • MySQL —— MySQL 程序
  • LLamafactory API部署与使用异步方式 API 调用优化大模型推理效率
  • 不玩PS抠图了,改玩Python抠图
  • 三维渲染中顺序无关的半透明混合(OIT)(一Depth Peeling)
  • Linux零基础入门--Makefile和make--纯干货无废话!!
  • vim编辑器的一些配置和快捷键
  • 电子应用设计方案-31:智能AI音响系统方案设计
  • 【设计模式】【结构型模式(Structural Patterns)】之装饰模式(Decorator Pattern)
  • 【AI】JetsonNano启动时报错:soctherm OC ALARM
  • QT:生成二维码 QRCode
  • 【LeetCode刷题之路】120:三角形最小路径和的两种解法(动态规划优化)