自定义类型

在之前的博客中我们讲到了C语言有三种自定义类型:结构体(结构)、枚举和联合,在这篇博客中我们将更加深入地探讨这三种自定义类型。

结构体

1.结构体的声明

struct tag
{
	int a;
	char ch;
	int arr[3];
	double d;
	float f;
}t1,t2;

 如上,struct是结构体关键字,而tag是结构体命名,一般根据结构体的需要对其进行合适的命名。而大括号内部的是成员变量,它可以是不同类型的变量,成员变量之间用分号隔开。大括号外面的是变量列表,t1,t2是用这个结构体类型来创建的结构体变量。

特殊的结构体类型声明(不完全声明):匿名结构体类型

顾名思义,匿名结构体就是在声明的时候部队结构体命名,没有tag部分。这种结构体类型只能在声明的时候在变量列表创建变量,只能在这里使用一次。 匿名结构体我们一般用在函数内部,在函数内部进行声明并创建变量使用,而出了函数就不再需要的时候我们就可以用这种声明。

2.结构体的自引用

结构体声明时能不能在内部创建一个自己的变量?答案是不能的,因为如果一个结构体内部包含一个同类型结构体的变量时,通俗来说就是会无限套娃,我们是无法得知这种类型创建的变量的内存大小的,所以编译器不会让这种代码通过编译。而真正的结构体自引用是在一个结构体内部包含一个同类型结构体的指针,有了这样的想法,我们就能实现链表等数据结构。 结构体自引用不能用匿名结构体类型。

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

在这里我们用typedef关键字对这个结构体类型重命名了。这个结构体类型包含一个数据变量和一个该结构体类型的指针。

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

结构体有两种定义形式,一种是在声明时直接在变量列表中定义,另外一种是用这个结构体类型去创建变量,这两种多余的时候都能直接对变量初始化。

在对结构体初始化的时候要用一个大括号,与数组一样,对多个元素同时赋值要用大括号括起来。如果一个结构体内部包含另一个结构体,在初始化的时候,内部的结构体的初始化也要用一个大括号括起来。

struct Stu
{
	char name[20];
	int age;
}s1={"zhangsan",18};

struct School
{
	struct Stu s;
	int score;
}S1={{"zhangsan",18},100};

int main()
{
	struct Stu s2 = { "lisi",19 };
	struct School S1 = { {"lisi,19",85} };

	return 0;
}

4.结构体内存对齐

上面我们已经能够了解结构体的基本使用了,现在要深入讨论一个问题,结构体的大小。要计算结构体的大小就要知道结构体的内存对齐。

struct S1
{
	char ch1;
	char ch2;
	int n;
};

struct S2
{
	char ch1;
	int n;
	char ch2;
};

int main()
{
	int size1 = sizeof(struct S1);
	int size2 = sizeof(struct S2);
	printf("%d\n", size1);
	printf("%d\n",size2);

	return 0;
}

对于上面这一个代码,我们可能会不理解,为什么结构体的大小不是成员变量大小的和呢?为什么成员变量相同,只是位置换了结构体大小却不一样了呢?

下面我们先来讲一些结构体内存对齐的规则,相信看完之后我们就能很容易解决上面的两个问题、

(1)第一个成员在于结构体变量偏移量为0的地址处。偏移量就是距离结构体起始位置所隔的字节数,比如起始位置开始第一个字节偏移量为0,第二个字节偏移量为1;

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

    对齐数=编译器默认对齐数 与 该成员大小 的较小值。

vs编译器中默认对齐数为8,其他的编译器则没有默认对齐数,他们的对齐数就是自身的大小。

比如第二个成员为int类型的变量,它的起始位置就是偏移量为4的整数倍的位置。

(3) 结构体的大小为最大对齐数(每一个成员都有一个对齐数,最大对齐数就是他们之中的最大值)的整数倍

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

拿上面的struct S1举例,第一个成员char ch1 对齐到偏移量为0的位置,占一个字节.而ch2 自身大小为一个字节,默认对齐数是8,所以他的对齐数是1 ,可以放到偏移量为1的位置。n自身是4个字节,默认对齐数是8,所以n的对齐数是4,放到偏移量为4 的整数倍的位置,刚好可以放到偏移为4的位置

此时这三个元素所占的空间为8个字节,而这个结构体的最大对齐数为4,8是4的整数倍,所以这个结构体的大小就是8个字节。

以此类推,struct S2创建的变量的大小为12个字节。

为什么会存在内存对齐?

大部分的参考资料都是如是说的:

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

不是所有的硬件平台都能访问任意地址上的任意数据的。某些硬件平台只能在某些地址处去某些特定类型的数据,否则抛出硬件异常。假如某个硬件平台只能在地址为2的倍数的地址处去读取整型,这就需要内存对齐来保证可移植性。

(2)性能原因

数据结构(尤其是栈)应该尽可能地在自然边界上对齐。

原因在于,为了访问未对齐的内存,编译器需要做两次内存访问;而对齐的内存访问只需要一次内存访问。

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

在设计结构体的时候,怎么做才能尽可能节省空间?

我们可以把内存小的成员尽量集中放在一起,就比如上面的S1和S2,他们的成员顺序,内存也不同。

我们也可以对编译器的默认对齐数进行修改,可以用#progma这个预处理指令来修改默认对齐数。

#progma pack(4)  就是把默认对齐数修改为4;

#pragma pack()   就是取消设置的默认对齐数,还原为编译器的默认对齐数。

struct St1
{
	char ch1;
	double d;
	char ch2;
};

#pragma pack(4)
struct St2
{
	char ch1;
	double d;
	char ch2;
};
#pragma pack()

int main()
{
	printf("%d\n", sizeof(struct St1));
	printf("%d\n", sizeof(struct St2));
	return 0;
}

对于上面这两个结构体,当我们修改默认对齐数为4的时候,St2的大小就变成了16个字节。

如果我们不想要结构体内存对齐的话,可以把默认对齐数修设置为1;

当我们觉得某个结构体用默认对齐数的话内存浪费太大了或者不太合适,在声明这个结构体的时候对默认对齐数进行修改,声明完之后再改回来。

5.结构体传参

 结构体传参我们之前讲到过传值和传址的区别,传值调用的时候形参压栈会浪费空间和时间,增加系统开销,所以我们最好传结构体的时候传它的地址。如果我们不想在函数内部误操作改变其内容,我们可以在形参部分用const对其进行修饰。

6.结构体实现位段

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

(1)位段的成员必须是int 、unsigned int、signed int 或者char类型的数据

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

位段的位是比特位的意思,成员变量冒号后面的数字就是分配给成员的比特位的个数。因为我们写代码时有些变量的取值范围没有这么大,用不到这么多的比特位,而位段能节省内存空间。位段是一种节省空间的做法,位段是一种特殊的结构体,而结构体是以空间换时间的做法。那么位段的结构体大小怎么算?比如下面的struct A的类型的变量内存是多大。

struct A
{
	int a : 10;
	int b : 10;
	int c : 20;
	int d;
};
int main()
{
	printf("%d\n", sizeof(struct A));
	return 0;
}

 我们要先了解位段的内存分配:

1 首先要确定一点,位段的成员类型只有int 和char 以及他们的有符号和无符号类型。同时,我们在使用位段的时候成员都是同一个类型的,不会把不同的成员放在同一个位段里,这样做就搞得太复杂了。同时,成员后面的数字不能大于该类型原来的大小。

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

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

拿上面的struct A来举例,由于位段成员是int类型,首先开辟四个字节(32个比特位),低十个比特位存a,然后接下来十个比特位存b。这时候开辟的四个字节剩余的比特位只有12位了,不够c的空间,所以再开辟四个字节的空间,前面四个字节剩余的空间就不会用了。然后将c存进新开辟的四个字节的低20个比特位中。d是一个整型类型,所以我们需要再开辟四个字节来存储d,所以这个位段的内存大小为12个字节。

因为编译器分配内存的时候是按一个或者四个字节来分配的,所以还是会有空间浪费,但是这样可以减少空间的浪费,提高空间的利用率。

位段的跨平台问题:

1.int位段被当成有符号数还是无符号数是标准未定义的,在位段中的int是不确定是unsigned int还是signed int 的,其他的时候单独使用int都是默认为signed int ,而char类型是signed char 还是unsigned char不管在不在位段中都是C语言标准未定义的。

2.位段中数据的最大位数不能确定(比如int类型在十六位机器上是16个比特位,而在三十二位机器上是32个比特位,如果把位段中的int写成30个比特位,可以在三十二位机器上正常运行,但是在十六位机器上就会出问题)。

3.位段中的成员在内存中是从左向右分配还是从右向左分配(同一个字节实现使用低地址还是先使用高地址)是标准未定义的,我们上面举例的规则是从低地址开始分配,这只适用于vs编译器、

4.当一个结构体包含两个位段,第二个位段成员比较大,无法容纳与第一个位段剩余的位时,是舍弃剩余的位,还是使用这些剩余的位,然后将剩余的数据存储到新开辟的空间中,这是不确定的。就比如我们上面举例中的c变量,在vs编译器中是直接舍弃前一块空间剩余的位,将c的数据全部存在新的空间中,但是在其他的编译器中我们是不知道是不是这样的。



枚举

枚举顾名思义就是一一列举,把可能的值都列举出来。在我们生活中可以一一列举的东西也有很多,比如一周是从星期一到星期天,人的性别是男和女等等。

枚举类型的定义,一一周七天为例:

enum DAY
{
	Mon,
    Tues,
	Wed,
	Thur,
	Fri,
	Sat,
	Sun
};

如上图,枚举成员之间是用逗号隔开的,也没有类型之分。

当我们用枚举类型来创建变量时,只能用枚举的可能取值来给变量赋值。

比如

int main()
{
    enum DAY d = Fri;
    return 0;
}

我们之前提到过枚举的成员是一种枚举常量,而枚举常量其实也是有具体的值的,默认取值是从0开始,比如Mon的取值时0,Tues的取值是1。我们也可以更改枚举常量的取值,比如我们在声明时可以Thur=100,这样的话Thur的值就是100了,而它后面的值依次加一。

enum DAY
{
	Mon,
    Tues,
	Wed,
	Thur=100,
	Fri,
	Sat,
	Sun
};

int main()
{
	printf("%d\n", Mon);
	printf("%d\n", Tues);
	printf("%d\n", Wed);
	printf("%d\n", Thur);
	printf("%d\n", Fri);
	printf("%d\n", Sat);
	printf("%d\n", Sun);
	return 0;
}

我们可能想到之前提到过的#define也可以定义常量,那为什么要用枚举呢?

枚举的优点:

1.增加代码的可读性和可维护性。对于#define定义的标识符常量,在预编译期间就将标识符进行了替换,所以我们是无法调试观测#define定义的常量的,而枚举相当于一种类型,是可以调试的。

2.和#define相比,枚举有类型检查更加严谨,而#define是不会类型检查的,只是进行替换操作。

3.防止了命名污染。

4.便于调试。

5.使用方便,一次可以定义多个变量。

联合(共用体)

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

联合的声明与结构体相同,联合的关键字是union。

union Un
{
	int a;
	char c[5];
};

因为联合具有所有成员共用一块空间的特性,所以联合的成员的地址和整个联合的地址时相同的。当两个变量不会同时被使用的时候,就可以使用联合。在使用联合的时候,对其中一个成员进行赋值,会改变另一个成员的值,直接覆盖了。

联合的大小:

联合的所有成员共用一块内存,所以联合的大小至少是最大成员的大小。

联合也存在内存对齐,联合的内存大小也是成员最大对齐数的整数倍。

就比如上面声明的联合,char c[5]的对齐数是1,占用连续的五个字节的空间,a的对齐数是4,占用前四个字节,所以这个联合最大对齐数是4,所以整个联合的大小是8.

联合的特性也可以用来判断当前机器是大端存储还是小端存储。

int fun(void)
{
	union
	{
		char ch;
		int n;
	}Un;
	Un.n = 1;
	return Un.ch;
}


int main()
{
	int ret = fun();
	if (1 == ret)
	{
		printf("小端存储\n");
	}
	else
	{
		printf("大端存储\n");
	}

	return 0;
}

上面的代码中我们用了一个匿名联合,在声明的时候定义了一个联合变量U你。

因为联合的成员是共用一块空间的,而ch占用这块空间的第一个字节,n占用这块空间的四个字节,这个联合的大小也是四个字节。 当我们对n赋值为1时,如果是小端存储,低位字节放在低字节处,那么n的第一个字节存放01,高地址的三个字节存放00 00 00,这时ch的值也被改成01。如果是大端存储,低位字节被存放在高地址处,那么n的存储内容就是 00 00 00 01,那么ch的内容就是00 ,所以可以用这个特点来判断机器的字节序存储方式。

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

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

相关文章

2022 年甘肃省职业院校技能大赛 高职组 网络系统管理竞赛 网络构建模块试题

2022 年甘肃省职业院校技能大赛 高职组网络系统管理竞赛 网络构建模块试题 目 录 考试说明… 3 任务描述… 3 任务清单… 3 (一)基础配置… 3 (二)有线网络配置… 4 (三)无线网络配置… 6 (四&a…

【数据结构】双向奔赴的爱恋 --- 双向链表

关注小庄 顿顿解馋๑ᵒᯅᵒ๑ 引言:上回我们讲解了单链表(单向不循环不带头链表),我们可以发现他是存在一定缺陷的,比如尾删的时候需要遍历一遍链表,这会大大降低我们的性能,再比如对于链表中的一个结点我们是无法直接…

【探究图论中dfs记忆化,搜索,递推,回溯关系】跳棋,奶牛隔间, 小A和uim之大逃离 II

本篇很高能,如有错误欢迎指出,本人能力有限(需要前置知识记忆化dfs,树形dp,bfsdp,tarjan) 另外,本篇之所以属于图论,也是想让各位明白,dfs就是就是在跑图&am…

DNS 服务 Unbound 部署最佳实践

文章目录 安装unbound-control配置启动服务测试 参考: 官网地址:https://nlnetlabs.nl/projects/unbound/about/ 详细文档:https://unbound.docs.nlnetlabs.nl/en/latest/index.html DNS服务Unbound部署于使用 https://cloud.tencent.com/…

cryptography,一个神奇的 Python 库!

更多资料获取 📚 个人网站:ipengtao.com 大家好,今天为大家分享一个神奇的 Python 库 - cryptography。 Github地址:https://github.com/pyca/cryptography 在当今数字化时代,信息安全越来越受到重视。数据加密是保护…

海外媒体发稿:9种高效的媒体套餐内容发稿策略分析-华媒舍

海外媒体发稿:9种高效的媒体套餐内容发稿策略分析高效的媒体发布和营销推广策略对公司、本人的成就尤为重要。下面我们就对于媒体套餐内容发稿营销推广策略开展全面解析,帮助读者掌握并应用这9种合理的思路,进而获得更好的媒体营销效果。 1.媒…

基于react native的自定义轮播图

基于react native的自定义轮播图 效果示例图示例代码 效果示例图 示例代码 import React, {useEffect, useRef, useState} from react; import {Animated,PanResponder,StyleSheet,Text,View,Dimensions, } from react-native; import {pxToPd} from ../../common/js/device;c…

小目标检测篇 | YOLOv8改进之GSConv + Slim Neck提升小目标检测效果

前言:Hello大家好,我是小哥谈。在文章中,作者提出了一种新方法GSConv来减轻模型的复杂度并保持准确性。GSConv可以更好地平衡模型的准确性和速度。并且,提供了一种设计范式Slim Neck,以实现检测器更高的计算成本效益。实验过程中,与原始网络相比,改进方法获得了最优秀的…

GDAl 之绘制栅格图像的大致直方图和精准直方图(8)

gdal的绘制大致直方图是仅查看概览或者抽样像素的一个子集 import os from osgeo import gdal import matplotlib.pyplot as plt import numpy as np# Dont forget to change directory. os.chdir(rD:\DeskTop\learn_py_must\Learn_GDAL\osgeopy-data\osgeopy-data\Switzerlan…

基于Selenium+Python的web自动化测试框架!

简介: 本文将详细介绍如何运用Python结合Selenium WebDriver库搭建web自动化测试框架。 一、什么是Selenium? Selenium是一个基于浏览器的自动化测试工具,它提供了一种跨平台、跨浏览器的端到端的web自动化解决方案。Selenium主要包括三部分…

音视频处理 - 音频概念详解,码率,采样率,位深度,声道,编码

1. 音频采样 与视频不同,音频的最小单位不是一帧,而是一个采样。 采样是当前一刻声音的声音样本,样本需要经过数字转换才能存储为样本数据。 真实声音是连续的,但是在计算机中,声音是离散且均匀的声音样本。 2. 位深…

ER图与关系模型

1.设某商业集团数据库中有三个实体集。 “公司”实体集,属性有公司编号、公司名、地址等; “仓库”实体集,属性有仓库编号、仓库名、地址等; “职工”实体集,属性有职工编号、姓名、性别等。公司与仓库间存在“隶属…

《被讨厌的勇气》读书思考笔记 (好书推荐)

《被讨厌的勇气》是一本由日本心理学家岸见一郎和奥冈昌高合著的畅销心理成长书籍。这本书基于心理学家阿尔弗雷德阿德勒的思想,介绍了“自我决定心理学”的观点,探讨了人们如何克服害怕失败,勇敢追求自己真正想要的生活。这本书在心理学、自…

HCIP的学习(4)

GRE和MGRE VPN---虚拟专用网络。指依靠ISP(运营商)或其他公有网络基础设施上构建的专用的安全数据通信网络。该网络是属于逻辑上的。​ 核心机制—隧道机制(封装技术) GRE—通用路由封装 ​ 三层隧道技术,并且是属于…

Git基础(23):Git分支合并实战保姆式流程

文章目录 前言准备正常分支合并1. 创建两个不冲突分支2. 将dev合并到test 冲突分支合并1. 制造分支冲突2. 冲突合并 前言 Git分支合并操作 准备 这里先在Gitee创建了一个空仓库,方便远程查看内容。 正常分支合并 1. 创建两个不冲突分支 (1&#xf…

Tableau项目案例-网上超市运营分析

一、数据简要介绍 超市运营分析.xlsx 1、客户分析 交易次数统计 购买次数即购买频率,是指消费者在一定时期内购买某种或某类商品的次数。 用tableau打开excel文件 双击城市字段,会显出出一个地图 类别字段也拖到筛选器上,如上操作相同

AI论文速读 | 具有时间动态的路网语义增强表示学习

论文标题: Semantic-Enhanced Representation Learning for Road Networks with Temporal Dynamics 作者: Yile Chen(陈亦乐) ; Xiucheng Li(李修成); Gao Cong(丛高) ; Zhifeng Ba…

管理能力学习笔记四:团队发展四阶段

组建期 管理方式 动荡期 领导方式 规范期 管理方式 高产期 管理方式 高产期的注意点

FL Studio21.2.3最新中文编曲音乐制作软件新版本功能介绍

一、前言 随着科技的发展,越来越多的人开始尝试自己创作音乐。然而,传统的音乐制作过程复杂繁琐,需要昂贵的硬件设备和专业的知识技能。那么,有没有一款软件可以让普通人也能轻松地制作出专业级别的音乐作品呢?答案就…

什么是 ECMAScript,它与 JavaScript 有何不同

什么是 ECMAScript? 关于 JavaScript](https://cloudaffle.com/history-of-javascript/)的[历史以及它是如何产生的,有一个完整的故事。长话短说,ECMAScript 中的 ECMA 是指欧洲计算机制造商协会,早在 1997 年就向该协会提交了 JavaScript 1.1 进行标准化。创建了一个技术委员…