【进阶C语言】自定义类型

news2025/4/9 12:39:40

本节内容大致目录如下:

1.结构体

2.位段

3.枚举

4.联合(共用体)

以上都是C语言中的自定义类型,可以根据我们的需要去定义。

一、结构体

一些基础知识在初阶C语言的时候已经介绍过,在这里粗略概括;重点介绍前面没有提到过的。

1.结构体的声明

声明其实就是需要自己创造一个结构体(类型)。后面再拿这个结构体(类型)去创造变量。

(1)简单声明

(2*)特殊的声明

struct
{
	int a;
	char b;
	float c;
}x;//x为结构体创造出来的名字
struct
{
	int a;
	char b;
	float c;
}a[20], * p;

1.这种在结构体关键字前省略了名字,这种声明方式的结构体称为:匿名结构体类型。

2.因为省略了名字,后续没法再进行变量的创造,所以只能使用一次

3.两个相同的匿名结构体,属于两种不同的类型

(3*)结构体的自引用

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

1.在结构体内部,可以用自身结构体类型来创建的指针,称为结构体的自引用。

2.一般用来数据结构的链表中,因为需要类型相同。

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


在前面的时候,我们有介绍过在声明的时候创造的全局变量,接下来都一起介绍了。

第一种创造方式:

#include<stdio.h>
struct Stu
{
    char c;
    int arr[10];
};
int main()
{
    struct Stu A;//结构体变量A
    struct Stu B;//结构体变量B
    return 0;
}


这里创造的变量A和B都是局部变量。

第二种创造方式:

#include<stdio.h>
typedef struct Stu
{
    char c;
    int arr[10];
}Stu;//对结构体重命名
int main()
{
    //struct Stu A;//结构体变量A
    //struct Stu B;//结构体变量B
    Stu C;//结构体变量C
    return 0;
}


第三种方式:上面提到过的创造全局变量

#include<stdio.h>
struct Stu
{
    char c;
    int arr[10];
}D;//全局变量D
int main()
{
    //struct Stu A;//结构体变量A
    //struct Stu B;//结构体变量B
    //Stu C;//结构体变量C
    return 0;
}


在创建后变量后,就该对变量进行初始化了

初始化:

#include<stdio.h>
struct Stu
{
    char name[20];
    int age;
    double height;
};
int main()
{
    struct Stu s1 = {"zhangsan",20,182.8};//顺序初始化
    struct Stu s2 = {.age=18,.height=188.5};//指定成员初始化
    return 0;
}


在创建变量的时候就初始化:

struct Stu
{
    char name[20];
    int age;
    double height;
}s3 = {"lisi",19,150.6};//创造的全局变量并初始化

3.结构体的内存对齐(*)

这是本节的重点,所谓的内存对齐,就是要知道结构体类型的内存大小专门来的,我们该怎么计算。

(1)引例

我们先观察两个大体相同的结构体,为什么相同的变量不同的顺序,内存大小却不一样。

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

运行结果:

第一个结构体类型s1的内存是8字节

第二个结构体类型s2的内存是12字节

造成这种原因是:在结构体中,存在内存对齐这一规则。

(2)结构体内存对齐规则

 1)第一条规则:第一个成员在,与结构体变量偏移量为0的地址处

什么是偏移量:

把存储单元的实际地址与其所在段的段地址之间的距离称为段内偏移,也称为“有效地址或偏移量”。

图解:

第一个成员就从0位置开始存放,内存多大就占几个格子(字节)。

2)第二条规则:其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处(偏移量处)

 什么是对齐数:

对齐数=编译器默认的一个对齐数与该成员的内存大小的较小值

(vs的默认值为8)

图解:

 上述成员在内存中一共占据了8个字节的空间

第二个结构体:

 这些成员在内存中所占的字节大小就是结构体的最终内存大小了吗?还没完,还需要根据第三条规则来计算。

3)第三条规则:结构体的总大小为最大对齐数(每个成员变量都有一个对齐数) 的整数倍

如:char的大小为1,1就是对齐数;像int大小为4,4就是对齐数;还有一个编译器默认对齐数,需要变量与其对比得出。

对齐数图解:

现在我们来计算结构体的大小

第一个:

第二个:

 该结构体在内存中占9个字节,不是4的整数倍,需要增大(增大到离9最近的数字且是4的整数倍),所以该结构体的内存大小为12字节。

如果结构体嵌套又如何计算,我们看第四条规则

4)第四条规则:如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍

存放嵌套的结构体时,它也有自己的最大对齐数和内存大小,所以只需要把它存放到是它自己的最大对齐数的整数倍处即可。

 图解:

变量C在右图所占的内存大小为16个字节,该结构体的最大对齐数为4,所以16为最终的内存大小。

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

 

(3)内存对齐的原因

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

2)性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐;原因在于,为了访问未对齐的内存,处理器需要做两次内存访问;而对齐的内存访问仅需要一次访问(拿空间换时间的一种做法)

3)做法:既可以节省空间又能节约时间

在设计结构体的时候,我们既要满足对齐,又要节省空间,就要让占用空间小的成员尽量集中在一起。

例如:

struct s1//已集中
{
	char c1;
	char c2;
	int i;
};
struct s2//未集中
{
	char c1;
	int i;
	char c2;
};

像struct s1的结构体就做到了上述的要求,内存只占8,而struct s2却占了12

(4)修改默认对齐数

我们可以通过修改默认对齐数,使结构体有更好的对齐方式,这里用#pragma这个预处理指令来修改。

#pragma pack(1)//设置默认对齐数为1
struct s1
{
	char c1;
	char c2;
	int i;
};
#pragma pack()//恢复默认对齐数
struct s2
{
	char c1;
	char c2;
	int i;
};
#include<stdio.h>
int main()
{
	printf("%zd\n", sizeof(struct s1));
	printf("%zd\n", sizeof(struct s2));
	return 0;
}

1.一样的数据类型和排列方式,所占内存却是不一样

2.结构在对齐方式不合适的时候,我们可以自己更改默认对齐数

3.默认对齐数,修改的结果一般要求为2^n,n>=1;不可以为符合或奇数(1除外)。

二、位段

位段是基于结构体的基础上的,位段是一种特殊的结构体--也是为了节省空间

1.位段的定义

位段的声明和结构是类似的

(1)位段的成员必须是:int,unsigned int、char或signed int(c99之后也可以有其他的类型)

(2)位段的成员名后边有一个冒号和数字

(3)举例:

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

1. _a、_b这些只是为了更好知道这是位段才加的,也可以选择不加。

2.后面的冒号和数字才是位段的语法要求。

3.位段的“位”表示二进制位的意思,冒号后面的数字就是代表有多少二进制位。

4.数字表明该成员变量最大的二进制位,如_a:2,_a只有两个二进制位,能表示的二进制数字只有:00,01,10和11,范围就是0-3。

(4)位段的作用

 我们根据预知的数据内存,可以设置合理的二进制位,就可以达到节约空间的目的

2.位段的内存分配

(1)内存的计算

1.位段也是一种结构体,所以位段也遵守内存对齐的方式。

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

如:

下面的内存大小是多少呢?

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

我们通过代码的运行结果可知:该位段的内存大小为8个字节

 我们接上面第二点:如:该位段都是int,一上来会先分配一个字节的空间(一字节==32bit),我们前面的2+5+10刚好存放在第一个字节的空间里面,而_d需要30bit,所以只能再开辟一个字节的空间,32bit拿出30比特刚好存放_d这个数据。

(2)位段的内存分配规则

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

 比如上述第一个字节中的bit没有被用完,后续是否还用这是不确定的

(3)内存分配实例

看一段代码:

#include<stdio.h>
struct S
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};
int main()
{
	struct S s = {0};//初始化
	printf("%zd\n", sizeof(struct S));
	return 0;
}

先简单看这个位段会消耗多少字节?

该结构体共消耗3字节。他们所占的二进制有3+4+5+4=16位,不应该是只占2字节吗?

图解:

现在我们已经知道了内存的分配,接下来了解数据是怎么存入的。

代码:

#include<stdio.h>
struct S
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};
int main()
{
	struct S s = {0};//初始化
	printf("%zd\n", sizeof(struct S));
	//赋值
	s.a = 10;
	s.b = 12;
	s.c = 3;
	s.d = 4;
	return 0;
}

 内存分配图解:

这就是这些数据存入内存中的二进制形式,然后转化成16进制就是在调试窗口的展示形式,让我们看看是不是这样子呢?

可以清楚看到三个字节中数据的存储方式,因为只能存储有限位的bit,所以需要控制数据的大小范围,否则会造成数据的丢失。

3.位段的跨平台问题

(1)int被当成有符号数还是无符号数是不确定的

如上述代码的结果:

(2)位段中最大位的数目不能确定。(16位机器最大位16,32位机器最大32位,写成27,在16位平台的机器上会出现问题)

(3)位段中的成员在内存中是从左向右分配,还是从右向左分配标准尚未定义(大小端存储字节序)

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

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

三、枚举

所谓枚举,就是一一列举

1.枚举的定义

枚举,全名又称枚举常量,属于常量中的一种。定义枚举常量的时候需要用到enum的关键字。

(1)定义格式

enum Sex//性别常量
{
	Man,//男
	Woman,//女
	Sercet,//保密
};

1.enum Sex称为枚举类型,Man、Woman和Sercet称为枚举常量

2.格式与结构体相似,但是每个枚举常量后面是逗号。

3.枚举常量的常量值默认从0开始。类似#define定义的常量一样,在后续的使用中他就是一个数字。

4.枚举常量的名字一般首字母大写或者全部大写,便于识别。

(2)打印枚举常量

#include<stdio.h>
enum Sex//性别常量
{
	Man,//男
	Woman,//女
	Sercet,//保密
};
int main()
{
	printf("%d\n", Man);
	printf("%d\n", Woman);
	printf("%d\n",Sercet);
	return 0;
}

结果:

(3)修改默认值

第一种:修改起始值,后续的值会依次递增

#include<stdio.h>
enum Sex//性别常量
{
	Man=5,
	Woman,
	Sercet,
};
int main()
{
	printf("%d\n", Man);
	printf("%d\n", Woman);
	printf("%d\n",Sercet);
	return 0;
}

第二种:任意赋值

#include<stdio.h>
enum Sex//性别常量
{
	Man=5,
	Woman=3,
	Sercet=100,
};
int main()
{
	printf("%d\n", Man);
	printf("%d\n", Woman);
	printf("%d\n",Sercet);
	return 0;
}

注意:只能在定义的时候赋值,但是不能在后续的步骤中修改

2.枚举的的优点

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


(2)和#define定义的标识符比较枚举有类型检查,更加严谨。

枚举常量是有类型的(枚举类型),而#define定义的却没有
(3)防止了命名污染(封装)


(4)便于调试


(5)使用方便,一次可以定义多个常量

四、联合(共用体)

1.联合体的定义

(1)联合体是一种特殊的自定义类型,特征是这些变量公用一块空间。定义需要用到联合关键字union。

(2)代码格式

union Un
{
	char c;
	int i;
};
与结构体格式相同,但是关键字不一样。

联合变量的创建:

#include<stdio.h>
union Un
{
	char c;
	int i;
};
int main()
{
	union Un u1;//联合变量
	printf("%zd",sizeof(u1));//计算联合变量的大小
	return 0;
}

2.联合体的特点

(1)特点

联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联
合至少得有能力保存最大的那个成员)。

#include<stdio.h>
union Un
{
	char c;
	int i;
};
int main()
{
	union Un u1;
	printf("%p\n", &u1);
	printf("%p\n", &(u1.c));
	printf("%p\n",&(u1.i));
	return 0;
}

运行结果:

这三个的起始地址都一样,那在内存中是什么样的呢?

他们是公用一块内存的,如果修改其中一个,另一个是很有可能就被修改了。

(2)利用其特点解决的问题

利用联合体判断当前机器是小端还是大端字节序?

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

原理:

返回的u.c,拿到的是i的第一个字节;如果小端存储。第一个字节(低字节)就会存储在低地址处,也就是起始位置,反之一样。

 3.联合大小的计算

(1)联合的大小至少是最大成员的大小。(不一定就是最大的)
(2)当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。(蹲守结构体的内存对齐规则)

#include<stdio.h>
union Un
{
	char a[5];
	int i;
};
int main()
{
	printf("%zd\n",sizeof(union Un));
	return 0;
}

联合体一般用于有公用部分的时候,如果商场的购物系统,很多商品都有共同的属性,如:价格等。


本章完 

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

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

相关文章

C++基于Qt中QOpenGLWidget模块实现的画图板源码+可执行文件

基于Qt中QOpenGLWidget模块实现的画图板 一、系统概述 本系统拟完成一个画图板&#xff0c;对多种常见图形进行基本操作系统功能 二维图形的输入&#xff1a;可输入或全部清除直线、矩形、圆、椭圆、多边形、文本等二维图形的变换&#xff1a;在直线、矩形、圆、椭圆、多边形…

(七)Flask之路由转换器

引子&#xff1a; from flask import Flaskapp Flask(__name__)# 通过使用<int>转换器&#xff0c;可以捕获URL中的整数值&#xff0c;并将其作为参数传递给视图函数。 app.route(/index/<int:nid>, methods[GET, POST]) def index(nid):print(nid)return Indexi…

基于SpringBoot的知识管理系统

目录 前言 一、技术栈 二、系统功能介绍 用户管理 文章分类 资料分类 文章信息 论坛交流 资料下载 三、核心代码 1、登录模块 2、文件上传模块 3、代码封装 前言 随着信息互联网信息的飞速发展&#xff0c;无纸化作业变成了一种趋势&#xff0c;针对这个问题开发一个…

毛玻璃带有光影效果的卡片

效果展示 页面结构组成 从效果展示可以看到&#xff0c;页面的主要元素是卡片&#xff0c;卡片的内容呈现上都是比较常规的布局&#xff0c;只是卡片上带有光影效果。 CSS / JavaScript 知识点 transformVanillaTilt.js 使用 页面基础结构实现 <div class"contain…

斯坦福数据挖掘教程·第三版》读书笔记(英文版)Chapter 10 Mining Social-Network Graphs

来源&#xff1a;《斯坦福数据挖掘教程第三版》对应的公开英文书和PPT。 Chapter 10 Mining Social-Network Graphs The essential characteristics of a social network are: There is a collection of entities that participate in the network. Typically, these entiti…

HDR-ISP_unpack_depwl_01_20231002

https://github.com/JokerEyeAdas/HDR-ISP/tree/main 1.unpack&#xff1a;解析raw图 (1)unpack&#xff1a;2个字节1个像素 (2)mipi10&#xff1a;5个字节4个像素 [p1 9:2][p2 9:2][p3 9:2][p4 9:2][(p1 1:0)(p2 1:0)(p3 1:0)(p4 1:0)] (3)mipi12&#xff1a;3个字节2个像…

音频编辑软件Steinberg SpectraLayers Pro mac中文软件介绍

Steinberg SpectraLayers Pro mac是一款专业的音频编辑软件&#xff0c;旨在帮助音频专业人士进行精细的音频编辑和声音处理。它提供了强大的频谱编辑功能&#xff0c;可以对音频文件进行深入的频谱分析和编辑。 Steinberg SpectraLayers Pro mac软件特点 1. 频谱编辑&#xff…

基于or-tools的人员排班问题建模求解(JavaAPI)

使用Java调用or-tools实现了阿里mindopt求解器的案例&#xff08;https://opt.aliyun.com/platform/case&#xff09;人员排班问题。 这里写目录标题 人员排班问题问题描述数学建模编程求解&#xff08;ortoolsJavaAPI&#xff09;求解结果 人员排班问题 随着现在产业的发展&…

Spring(JavaEE进阶系列1)

目录 前言&#xff1a; 1.Servlet与Spring对比 2.什么是Spring 2.1什么是容器 2.2什么是IoC 2.3SpringIoC容器的理解 2.4DI依赖注入 2.5IoC与DI的区别 3.Spring项目的创建和使用 3.1正确配置Maven国内源 3.2Spring的项目创建 3.3将Bean对象存储到Spring&#xff08…

21.本地存储

目录 1 保存 wx.setStorageSync() 2 获取 wx.getStorageSync() 3 删除指定的 wx.removeStorageSync() 4 清空所有 wx.clearStorageSync() 1 保存 wx.setStorageSync() 第一个参数是键&#xff0c;第二个参数是值&#xff0c;我放在生命周期函数中了&#xff0c;所以一…

怒刷LeetCode的第17天(Java版)

目录 第一题 题目来源 题目内容 解决方法 方法一&#xff1a;过滤和排序 方法二&#xff1a;迭代 第二题 题目来源 题目内容 解决方法 方法一&#xff1a;回溯算法 方法二&#xff1a;动态规划 方法三&#xff1a;DFS剪枝 方法四&#xff1a;动态规划状态压缩 方…

基于SpringBoot的高校学科竞赛平台

目录 前言 一、技术栈 二、系统功能介绍 竞赛题库管理 竞赛信息管理 晋级名单管理 往年成绩管理 参赛申请管理 三、核心代码 1、登录模块 2、文件上传模块 3、代码封装 前言 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步…

【机器学习笔记】

文章目录 1.优化器讲解 1.优化器讲解

超详细的Pycharm.2023下载与安装教程

Pycharm.2023下载与安装教程 Pycharm下载Pycharm安装 Pycharm下载 Pycharm官网下载地址&#xff1a; https://www.jetbrains.com/pycharm/download/?sectionwindows#sectionwindows 进入如下界面&#xff1a;一般分为专业版本和社区版本&#xff0c;平时做深度学习的话社区版…

Ubuntu镜像源cn.arichinve.ubuntu.com不可用原因分析和解决

文章目录 Ubuntu查看系统版本Ubuntu更新系统不能更新Ubuntu查看APT更新源配置cn.archive.ubuntu.com已经自动跳转到清华镜像站Ubuntu变更镜像源地址备份原文件批量在VIM中变更 Ubuntu国内镜像站推荐推荐阅读 今天想要在Ubuntu环境下搭建一个测试环境&#xff0c;进入Ubuntu系统…

基于51单片机NEC协议红外遥控发送接收仿真设计( proteus仿真+程序+原理图+报告+讲解视频)

基于51单片机NEC协议红外遥控发送接收仿真设计 讲解视频1.主要功能&#xff1a;2.仿真3. 程序代码4. 原理图5. 设计报告6. 设计资料内容清单&&下载链接 基于51单片机NEC协议红外遥控发送接收仿真设计 51单片机红外发送接收仿真设计( proteus仿真程序原理图报告讲解视频…

查看react内置webpack版本的方法

yarn list --pattern webpack npm ls --pattern webpack

力扣-303.区域和检索-数组不可变

Idea 需计算数组nums在下标right 和 left-1 的前缀和&#xff0c;然后计算两个前缀和的差即可。 需要注意的是&#xff0c;当left为0的时候&#xff0c;如果还是left-1则会发生数组访问越界错误。 AC Code class NumArray { public:vector<int> sum;NumArray(vector<…

嵌入式学习笔记(41)SD卡启动详解

内存和外存的区别&#xff1a;一般是把这种RAM(random access memory,随机访问存储器&#xff0c;特点是任意字节读写&#xff0c;掉电丢失)叫内存&#xff0c;把ROM&#xff08;read only memory&#xff0c;只读存储器&#xff0c;类似于Flash SD卡之类的&#xff0c;用来存储…

基于YOLOv8的安全帽检测系统

1.Yolov8介绍 Ultralytics YOLOv8是Ultralytics公司开发的YOLO目标检测和图像分割模型的最新版本。YOLOv8是一种尖端的、最先进的&#xff08;SOTA&#xff09;模型&#xff0c;它建立在先前YOLO成功基础上&#xff0c;并引入了新功能和改进&#xff0c;以进一步提升性能和灵活…