C语言自定义类型(上)

news2025/2/23 17:24:01

大家好,我们又见面了,这一次我们来学习一些C语言有关于自定义类型的结构。
在这里插入图片描述

目录

1.结构体
2位段

1.结构体

前面我们已经学习了一些有关于结构体的知识,现在我们进行深入的学习有关于它的知识。

结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。

结构体的声明

struct tag
{
member-list;
}variable-list;

我们要注意的是结构体的关键字是struct,后面的就是我们自己定义的,而括号里面的叫做成员变量,它可以是任意类型的。例如我们自己创造一个学生结构体:stu就是我们创建的结构体变量,而名字,年龄,性别,学号就是成员变量。

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

匿名结构体

struct 
{
	char neme[20];
	int age;
	char sex[5];//一个汉字2个字符
	float score;
}s1,s2;

如上代码所示,可以去掉结构体的名字 匿名结构体类型,但只能用一次,后面再想定义变量不可以(只可以使用s1和s2)

结构体的自引用

就是在结构中包含一个类型为该结构本身的成员

让我们看到下面这一段代码:

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

如果我们要算结构体的大小这段代码能够实现吗?答案是否定的,因为我们这个结构体中包含着下一个结构体,我们要计算结构体大小的时候不仅是整型的大小还要加上下一个结构体的大小,那么下一个结构体中又包含了一个结构体是无法计算的,编译器会报错。那么正确的自引用是什么的样的呢?

正确的自引用:

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

这个代码可以运行,用指针的话,指针存放下一个节点的地址,指针本身的大小也是固定的,所以可以计算出来。

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

结构体的定义:

struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1

struct Point p2; //定义结构体变量p2

结构体的初始化:

struct Point p3 = {x, y};


struct Stu //类型声明
{
char name[15];//名字
int age; //年龄
};
struct Stu s = {"zhangsan", 20};//初始化

结构体内存对齐

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

这里我们定义两个结构体变量:

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

struct S2
{
	char c1;
	char c2;
	int i;
};
int main()
{
	/*printf("%d\n", offsetof(struct S2, c1));
	printf("%d\n", offsetof(struct S2, c2));
	printf("%d\n", offsetof(struct S2, i));*/

	printf("%d\n", sizeof(struct S1));
	printf("%d\n", sizeof(struct S2));

	return 0;
}

我们想知道这两个结构体的大小,那么一个整型是4个字节,一个字符型是一个字节,那么这两个结构体的大小是不是6个字节呢?

在这里插入图片描述
很显然是不对的,那为什么会造成这个结果呢?那是因为其中成员对于起始位指定的偏移量造成的,这里我们就要了解一个宏,offsetof这个宏就是专门来计算偏移量的。

#include<stdio.h>
#include <stddef.h>
struct S1
{
	char c1;
	int i;
	char c2;
};

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

int main()
{
	printf("%d\n", offsetof(struct S1, c1));
	printf("%d\n", offsetof(struct S1, c2));
	printf("%d\n", offsetof(struct S1, i));
	printf("%d\n", offsetof(struct S2, c1));
	printf("%d\n", offsetof(struct S2, c2));
	printf("%d\n", offsetof(struct S2, i));
	/*printf("%d\n", sizeof(struct S1));
	printf("%d\n", sizeof(struct S2));*/

	return 0;
}

在这里插入图片描述
在这里我们看到每个成员变量对于初始位置的偏移量都是不同的,这就是因为内存对齐导致的。那么我们有什么办法来计算结构体的大小呢?
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

这里以s2为例我们看到c1是一个字节和vs默认的八个对齐数,所以c1的对齐数就是1,第一个成员的偏移量为0,所以c1就在下标为0的位置,而i是四个字节和八个字节相比,对齐数是4,因为下标4才是4的倍数,所以我们的i从下标为4的位置开始储存,而c2的对齐数为1,所以它的偏移量为i的倍数就直接放在i的后面,因为结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍,最大是4,所以是12.

那么我们为什么要存在内存对齐呢?

  1. 平台原因(移植原因):
    不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  2. 性能原因:
    数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
    总体来说:
    结构体的内存对齐是拿空间来换取时间的做法。那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:让占用空间小的成员尽量集中在一起。

修改默认对齐数

#pragma 这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数。

看到我们的代码:

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

#pragma pack(1)//设置默认对齐数为1
struct S2
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
//输出的结果是什么?
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));

在这里插入图片描述
这里我们将默认对齐数改了之后所得出来的结果就明显的不同了,根据我们使用的方法计算结果就是12和6。

结构体传参

struct S
{
int data[1000];
int num;
};
struct S s = {{1,2,3,4}, 1000};
//结构体传参
void print1(struct S s)
{
printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
printf("%d\n", ps->num);
}
int main()
{
print1(s); //传结构体
print2(&s); //传地址
return 0;
}

函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。所以我们要首选传址。

位段

1.位段的成员必须是 int、unsigned int 或signed int 。
2.位段的成员名后边有一个冒号和一个数字。

例如:

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

那么我们位段的大小是多少呢?

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

int main()
{
	printf("%d\n", sizeof(struct A));

	return 0;
}

在这里插入图片描述
这里我们看到程序运行的结果是8,而我们根据自己的计算得到结构体占了47个比特位,而每个字节占了8个比特位,我们换算出来就是将近6个字节,那么为什么不是6个字节呢?这就要了解它的内存分布了。

位段的内存分配

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

在这里插入图片描述

我们定义的位段它的类型的是char型,而我们位段的内存分配是一个字节一个字节申请的,我们先拿到一个字节的空间,首先我们要给它三个比特位,而且是从低地址到高地址分配的,我们的a是等于10的,它的二进制的前三位是010放到这个分配的字节中,而后面也是一样b在这个字节中占了4个比特位,然后这里只剩下一个比特位就放不下了,就再申请一个字节的空间,这个里面就拿来存放c的二进制,而我们看到这个字节的空间就剩下了三个比特位了,而d还要四个比特位的空间,所以我们得再申请一个字节的空间。所以这个位段的大小就是三个字节,我们在给二进制换算的十进制,所以这三个字节的内存就是62 03 04。

位段的跨平台问题:

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

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

好了今天的学习就到这里,我们下次再见了。

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

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

相关文章

.Net6与Framework不同方式获取文件哈希值的性能对比

算法&#xff1a;MD5、SHA1、SHA256、SHA384、SHA512文件数&#xff1a;200平台对比&#xff1a;.NET 6 vs .NET Framework 4.7.2 关键代码 // 读取文件夹&#xff0c;获取MD5值 var hashs new HashAlgorithm[] { MD5.Create(), SHA1.Create(), SHA256.Create(), SHA384.Cre…

【C++】C++ 类中的 this 指针用法 ( C++ 类中的 this 指针引入 | this 指针用法 | 代码示例 )

文章目录 一、C 类中的 this 指针1、C 类中的 this 指针引入2、C 类中的 this 指针用法3、完整代码示例 一、C 类中的 this 指针 1、C 类中的 this 指针引入 在 C 类中 , this 指针 是一个特殊的指针 , 由系统自动生成 , 不需要手动声明定义 , 在类中的每个 非静态成员函数 中 …

【机器学习】期望最大算法(EM算法)解析:Expectation Maximization Algorithm

【机器学习】期望最大算法&#xff08;EM算法&#xff09;&#xff1a;Expectation Maximization Algorithm 文章目录 【机器学习】期望最大算法&#xff08;EM算法&#xff09;&#xff1a;Expectation Maximization Algorithm1. 介绍2. EM算法数学描述3. EM算法流程4. 两个问…

C++核心编程——P25-拷贝构造函数调用时机

拷贝构造函数调用时机 C中拷贝构造函数调用时机通常有三种情况 使用一个已经创建完毕的对象来初始化一个新对象值传递的方式给函数参数传值以值方式返回局部对象 #include<iostream> using namespace std; class Person { public:Person(){cout << "Person…

深入理解Linux中信号处理过程

&#x1f525;&#x1f525; 欢迎来到小林的博客&#xff01;&#xff01;       &#x1f6f0;️博客主页&#xff1a;✈️林 子       &#x1f6f0;️博客专栏&#xff1a;✈️ Linux       &#x1f6f0;️社区 :✈️ 进步学堂       &#x1f6f0…

联想笔记本怎么关闭/开启自带键盘

搜索&#xff0c;命令提示符&#xff0c;以管理员身份运行在弹出的窗口中将下面这段代码输入进去&#xff0c;并且回车。 sc config i8042prt startdisabled&#xff0c;提示成功即可然后重启&#xff0c; 笔记本自带键盘就会关闭。如果想恢复&#xff0c; 只要以同样方法输入下…

开发板TFTP调试

问题描述 开发板和host(此处指虚拟机linux)可以平通&#xff0c;但是通过uboot tftp下载请求时一直显示T T T, 即超时 使用wireshark抓包也显示超时 措施 关闭windows和linux的防火墙 重新进行下载成功

智慧公厕,公共厕所数字化促进智慧城市管理的成效

随着科技的不断进步和城市化的快速发展&#xff0c;城市管理也面临着新的挑战和机遇。而智慧公厕作为基层配套设施&#xff0c;通过数字化提升城市管理的效能&#xff0c;成为了现代智慧城市建设的重要一环。本文以智慧公厕领先厂家广州中期科技有限公司&#xff0c;大量项目案…

MySQL学习笔记11

MySQL日期类型&#xff1a; ###㈠ DATE类型&#xff08;年-月-日&#xff09; The DATE type is used for values with a date part but no time part. MySQL retrieves and displays DATE values inYYYY-MM-DD format. The supported range is 1000-01-01 to 9999-12-31. ##…

RASP hook插桩原理解析

javaagent技术&#xff0c;实现提前加载类字节码实现hook&#xff0c;插桩技术 javassist技术ASM字节码技术 像加载jar&#xff0c;有两种方式 premain启动前加载&#xff1a;每次变动jar包内容&#xff0c;都需要进行重启服务器利用java的动态attch加载原理&#xff0c;采用pr…

查询统计当前日期往前推近七天每天的记录数

1、查询统计当前日期往前推近七天每天的记录数。 并且如果某一天没有数据&#xff0c;则该天不会显示在结果集中&#xff0c;也不会用零值补充 SELECT date_format(create_time, %Y-%m-%d), count(*) FROM your_table WHERE create_time > date_sub(curdate(), interval 6…

恒合仓库 - 采购单管理模块

采购单管理模块 文章目录 采购单管理模块一、添加采购单(核心)1.1 采购流程1.2 采购单实体类1.3 添加采购单1.3.1 Mapper1.3.2 Service1.3.3 Controller1.3.4 效果图 二、采购单管理模块2.1 仓库数据回显2.1.1 Mapper2.1.2 Service2.1.3 Controller2.1.4 效果图 2.2 采购单列表…

Docker - Docker启动的MySql修改密码

基于上篇文章《Docker - Docker安装MySql并启动》&#xff0c;在Docker中启动了mysql服务&#xff0c;但是密码设置成了123456&#xff0c;想起来学生时代数据库被盗走&#xff0c;然后邮箱收到被勒索BTC的场景还历历在目&#x1f62d;&#xff0c;密码不能再设置这么简单了啊&…

【prometheus+grafana】快速入门搭建-服务监控各插件及企业微信告警

目录 1. 安装qywechat_webhook插件通知企业微信 1.1. 新建目录/opt/prometheus/qywechathook/conf 1.2. 新建编辑wx.js文件 1.3. 运行启动容器 1.4. 查看容器启动情况 1.5 企业微信通知地址为&#xff1a; 2. 安装altermanager 2.1. 下载altermanager 2.2. 解压alterm…

Linux 远程登录(Xshell7)

为什么需要远程登录Linux&#xff1f;因为通常在公司做开发的时候&#xff0c;Linux 一般作为服务器使用&#xff0c;而服务器一般放在机房&#xff0c;linux服务器是开发小组共享&#xff0c;且正式上线的项目是运行在公网&#xff0c;因此需要远程登录到Liux进行项日管理或者…

LeetCode算法二叉树—二叉树的中序遍历

目录 94. 二叉树的中序遍历 - 力扣&#xff08;LeetCode&#xff09; 代码&#xff1a; 运行结果&#xff1a; 给定一个二叉树的根节点 root &#xff0c;返回 它的 中序 遍历 。 示例 1&#xff1a; 输入&#xff1a;root [1,null,2,3] 输出&#xff1a;[1,3,2]示例 2&am…

Linux--线程 创建、等待、退出

Linux上线程开发API概要 多线程开发的最基本概念主要包含&#xff1a;线程&#xff0c;互斥锁&#xff0c;条件。   线程 3 种操作&#xff1a;线程的创建&#xff0c;退出&#xff0c;等待。   互斥锁 4 种操作&#xff1a;创建&#xff0c;销毁&#xff0c;加锁和解锁。…

iterm2 配置自动登录跳板机,无需输入密码和google验证码

1、准备&#xff1a;编写Python脚本计算生成google身份验证码&#xff0c;参考python3 实现 google authenticator 认证/校验_我要买GTR45的博客-CSDN博客 脚本拿来就可以用&#xff0c;只需要替换脚本中的secret字段的值为自己的密钥即可 2、在~/.ssh/目录下编写expect脚本 …

多台群晖实现按计划WOL网络自动唤醒数据冷备份

几年前买了2盘位的DS218&#xff0c;但是随着照片的增加已经不够用。年中购入了4盘位的群晖DS923、2块16T西数数企业级硬盘、1块2T intel企业级 SSD 1.什么是冷备份 冷备是离线备份&#xff0c;备份好的数据可以单独存取&#xff0c;定期冷备可以保证数据安全&#xff0c;适合…

怎样快速打开github.com

1访问这个网站很慢是因为有DNS污染&#xff0c;被一些别有用心的人搞了鬼了&#xff0c; 2还有一个重要原因是不同的DNS服务器解析的速度不一样。 1 建议设置dns地址为114.114.114.114.我觉得假设一个县城如果有一个DNS服务器的话&#xff0c;这个服务器很可能不会存储…