初识C语言·自定义类型(2)

目录

1 结构体的声明和定义

2 结构体的自引用

3 结构体成员访问操作符

4 内存对齐

4 结构体传参

5 位段


1 结构体的声明和定义

什么是结构?结构也就是元素的集合,在C语言里面,结构体里面的可以有多个变量,类似于集合中的元素,结构体里面的元素被叫做成员变量,成员变量可以是不同类型的多个变量,那么创建好结构体之后,定义的就是结构体变量,那么结构体的创建用到的是关键字struct,创建如下:
 

struct teg
{
	member list;//成员变量
}variable-list;//结构体变量

variable-list在创建的时候是不用写的,这种写法是定义结构体的同时创建好结构体变量,需要注意的是分号不能丢,tag标签一般都要写,不然就是匿名结构体了。

匿名结构体,匿名结构体是结构体的一种特殊声明,匿名声明,特点是这种结构体只能使用一次,如下代码:

struct
{
	int age;
	char name[20];
}x;//匿名结构体 只能使用一次
struct
{
	int age;
	char name[20];
}*p;
int main()
{
	p = &x;//类型不兼容 只用一次 不常用
	return 0;
}

当我们创建了两个成员变量是一样的匿名结构体变量的时候,使用结构体指针想要取地址就会发现系统报警告说类型不兼容,这是匿名结构体的特点。当然,用了一次之后不想丢弃这个结构体也是有办法的,使用重命名typedef:

typedef struct
{
	int age;
	char name[20];
}x;

这样结构体就重新拥有名字了,就和平常创建的结构体变量是一样的了,创建结构体变量的时候像

x St =  ……; 就可以了,但是实际写代码的时候用的时候一般不用匿名结构体,毕竟它的特点就只有一个——只能用一次。

比如我们要创建一个结构体表示学生的一些信息,像这样:

struct Stu
{
	char name[20];//名字
	int age;//年龄
	float score;//成绩
	char id[20];//学号
};

创建好了之后我们使用的时候要进行初始化,初始化可以分为两种情况

一是按照成员变量的顺序进行初始化:

struct Stu s1 = { "zhangsan",18,99.9,"20240123" };

二是使用操作符按照自己的想法进行初始化:

struct Stu s2 = { .score = 98.9,.age = 18,.id = "20240124",.name = "lisi" };

当然,如果你嫌完整初始化太麻烦了的话,直接给0也是可以的:

struct Stu s3 = { 0 };

2 结构体的自引用

套娃知道吧?main里面有个main知道吧?会崩溃知道吧?结构体里面有个自己程序也是会崩溃的,像这样:

struct Stu
{
	char name[20];//名字
	int age;//年龄
	double score;//成绩
	char id[20];//学号
	struct Stu next;
};

如果想不明白就想这个问题:sizeof(struct Stu)等于多少?

在数据结构里面,会涉及到链表的内容,我们也是通过结构体实现的,但是不是这种套娃的形式,我们确实可以通过结构体的自引用找到下一个结构体,但停不下来,所以我们把结构体分为数据域和指针域:

struct Stu
{
	char name[20];//名字
	int age;//年龄
	double score;//成绩
	char id[20];//学号
	struct Stu* next;
};

是的,就是加个*的事儿,结构体存下一个结构体的地址,这样循环往复,我们就可以像链条一样,挨个挨个找到我们需要使用的数据。

在结构体自引用的过程中也包括了typedef的重命名过程,像这样:

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

有错误吗?当然是有的,在重命名好之前,Node就已经是成员变量了,但是实际上此时的结构体还没有真正拥有名字,所以这段代码是错误的。


3 结构体成员访问操作符

创建好结构体之后我们就需要访问,使用该结构体了,那么结构体成员访问操作符有哪些呢?

有两个,点操作符和箭头操作符:

struct Stu
{
	int age;
}*p;
int main()
{
	struct Stu stu = { 18 };
	p = &stu;
	printf("%d\n", stu.age);
	printf("%d\n", p->age);
	return 0;
}

综上:点操作符的右边是成员变量,左边是结构体变量,箭头操作符的右边是成员变量,左边是结构体变量指针。


4 内存对齐

上文提到的,内存对齐是一大考点,也是热门的话题,内存对齐是用来计算结构体的大小的。

那么内存对齐的规则有:

i) 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处

ii) 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。

iii) 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的整数倍

iv) 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构 体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。

第一条规则的意思就是计算大小的时候,是从0开始计算的,偏移量从0开始,以1递增,那么什么是对齐数呢?
对齐数就是系统默认的对齐数和成员变量大小中的较小的值,在VS里面默认对齐数是8,但是在Linux环境下的gcc编译器是没有默认最大对齐数的,对齐数就是成员变量的大小。

代码1:

struct Stu
{
	char a;
	char b;
	int c;
};
int main()
{
	printf("%zd\n", sizeof(struct Stu));
	return 0;
}

运行结果是8,偏移量从0开始,那么char 类型占一个字节,比8小,所以对齐1个字节,那么0对应的字节就被char a占了,同理,1对应的字节被 char b占去了,那么接下来的偏移量是2 3 4 5 6 ,int c占4个字节,比8小,所以对齐数是4,那么根据第二条规则,int c应该从4开始对齐,所以占去了 4 5 6 7四个字节,那么0 - 7一共是8个字节,所以sizeof(struct Stu)的结果就是8。

代码2:

struct Stu
{
	char a;
    int b;
	char c;
};
int main()
{
	printf("%zd\n", sizeof(struct Stu));
	return 0;
}

运行结果是12,偏移量从0开始,那么char a占0对应的字节,1 2 3 都不是4的整数倍,所以int b占去了4 5 6 7 四个字节,char c就占去了8对应的字节,那么现在的字节数是0 -  8,一共9个字节,那答案是9吗?指定不是,根据第三条规则,最大对齐数是4,9不是4的整数倍,所以就继续浪费空间,直到12,因为12是4的整数倍,所以运行结果是12。

代码3:

struct S3
{
	double a;
	char c;
	int i;
};
struct S4
{
	char a1;
	struct S3 s3;
	int i;
};
int main()
{
	printf("%zd\n", sizeof(struct S4));
	return 0;
}

运行结果是32,根据123规则我们可以得出s3的大小是16,那么char a1占0对应的字节,根据第四条规则,s3对齐不是根据16对齐,是根据成员变量最大的大小来对齐,那么就是8,所以8 - 23是s3占用的字节,24 - 27是int对应的字节,总字节数是28,不是8的整数倍,一直浪费到32,ok了就。

为什么存在内存对齐?

第一个原因:
平台原因(硬件原因),不是所有的硬件都能访问所有地址的数据的,有些硬件只能访问特定地址的数据,所以如果没有对齐好,可能就会访问失败。

第二个原因:

为了提高访问效率,因为有些硬件只能访问特定地址的位置,那么如果数据的位置不是在特定地址,是在特定地址的左右两边,这样就会导致原本只需要访问一次就可以获取的数据需要进行多次访问,就会降低性能。

故可以将内存对齐的存在理解为是空间换取时间的作法。

实际写代码的时候,我们着重将同一类型的放一起,浪费的空间就没有那么多。

内存对齐中涉及到的是偏移量,比如我们想要直到某一个成员变量对于起始地址的偏移量是多少我们就可以使用offsetof函数,这个函数就是专门计算起始偏移量的:

struct S4
{
	char a1;
	struct S3 s3;
	int i;
};
int main()
{
	printf("%zd\n", offsetof(struct S4,a1));
	printf("%zd\n", offsetof(struct S4,s3));
	printf("%zd\n", offsetof(struct S4,i));
	return 0;
}

具体细节可以去cplusplus网站查看。

我们知道默认对齐数有的编译器有的编译器没有,而默认对齐数是可以自行修改的。

#pragma pack(1)
struct S5
{
	char i;
	int a;
	char j;
}s5;
int main()
{
	printf("zd\n", sizeof(s5));
	return 0;
}

运行结果就是6,那么取消就在后面加一个:

#pragma pack()

就行了。


4 结构体传参

struct S
{
	int data[1000];
	int num;
}s = { { 1,2,3,4,5 },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;
}

打印都是没有问题的,探讨的是结构体传参是传值好还是传地址好?

我们说形参是实参的一份临时拷贝,也就是说调用该函数的时候会有两个结构体,可能你会觉得没有什么,可是如果结构体一大,浪费的空间不仅很大,内存还要为了该函数压栈,就很浪费时间,所以结构体传参,传地址优于传值。


5 位段

struct A
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};

位段的写法就是char a : 任意数字,当然这里的char 也是可以换成其他的,那么关于位段:

i) 使用位段只能用int unsigned int  signed int  char

ii) 位段不具有可移植性,因为不同平台的位段使用规则不一样,所以要避免跨平台使用

iii) 位段在内存中的开辟是以4个字节(int)或1个字节(char)开辟的

但是实际使用的时候有许多需要注意的点,例如:
1: int 是unsigned int 还是 signed int是未知的

2:位段中的最大位数和机器有关,32位机器最大32,16位机器最大16

3:当一个结构体包含两个位段的时候是,其中一个位段占据了较大的空间导致剩余的位不够下一个位段使用的时候,是继续使用剩余位数还是舍弃剩下的位数在新开辟一个空间这是不确定的。

4 :位段中的成员在内存中的分配是从右往左还是从左往右这是不确定的

5:位段中可能多个成员共用一个字节,但是取地址的时候都是从首地址开始的,所以不能对位段成员进行取地址,这是错误示范:

正确做法是先赋值给一个临时变量,然后赋值给位段中的成员:

struct A
{
	int a : 2;
	int b : 2;
	int c : 10;
	int d : 30;
};
int main()
{
	struct A s = { 0 };
	int b1 = 0;
	scanf("%d", &b1);
	s.b = b1;
	printf("%d", s.b);
	return 0;
}

Tips:位段在计算的时候也要考虑内存对齐


感谢阅读!

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

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

相关文章

LabVIEW准分子激光器控制系统

LabVIEW准分子激光器控制系统是为了实现准分子激光光源在工业、医疗和科研领域的应用集成及其功能的扩展。系统由PC端和激光器端两部分构成,通过光隔离的RS232通讯连接,以实现稳定可靠的控制与通信。 系统主要由微控制单元(MCU)主…

Python解释器的启动方式

Python解释器的启动方式 Python 解释器是一个运行 Python 代码的程序。它读取并执行写成 Python 语言的指令。由于 Python 是一种解释型语言,所以它的代码不需要编译成机器语言就可以直接运行。这就是为什么我们需要一个解释器来逐行读取 Python 代码,将…

linux centos 查看端口是否打开与打开端口

查看端口是否打开 talnet talnet ip 端口linux查看防火墙开放情况 firewall-cmd --list-all打开端口 其中permanent表示永久生效,public表示作用域,443/tcp表示端口和类型,执行规则的重载 firewall-cmd --zonepublic --add-port443/tcp …

Shell脚本——循环语句(for、while和until循环)

一、命令 1.echo命令 echo -n 表示不换行输出 echo -e 输出转义字符,将转义后的内容输出到屏幕上 常见转义字符: \b 相当于退格键 转义后相当于退格键(backspace),但是前提是“\b”存在字符。“\b”表示删除前一个…

按条件自动搜索文件

在计算机的某个文件夹中,假如有一大堆不同格式的文件,如下图: 我们的目的:快速查找出文件名中包含某文字内容的指定格式的文件,看看它们都放在哪里?通过分析,可能在当前文件夹中也可能在某个子…

LabVIEW探测器CAN总线系统

介绍了一个基于FPGA和LabVIEW的CAN总线通信系统,该系统专为与各单机进行系统联调测试而设计。通过设计FPGA的CAN总线功能模块和USB功能模块,以及利用LabVIEW开发的上位机程序,系统成功实现了CAN总线信息的收发、存储、解析及显示功能。测试结…

FinBert模型:金融领域的预训练模型

文章目录 模型及预训练方式模型结构训练语料预训练方式 下游任务实验结果实验一:金融短讯类型分类实验任务数据集实验结果 实验二:金融短讯行业分类实验任务数据集实验结果 实验三:金融情绪分类实验任务数据集实验结果 实验四:金融…

RT-Thread: STM32 SPI使用流程

1.添加驱动 ①点开设置界面 ②勾选看门 SPI 驱动 ③点击保存 ④查看添加的驱动文件 drv_spi.c 2.打开驱动头文件定义 ①打开配置文件 ②打开定义 3.打开需要开启的SPI总线 打开 drivers 目录下的 board.h 用SPI搜索,找到如下文字,打开对应的宏。 /*-…

非官方 Bevy 作弊书07-09

源自 网页 Working with 2D - Unofficial Bevy Cheat Book 个人用 有道 翻译,希望能够帮助像我一样的 英语不好 的 bevy 初学者 非官方 Bevy 作弊书 7 使用 bevy 2D 本章涵盖与使用 Bevy 制作 2D 游戏相关的主题。 2D Camera Setup - Unofficial Bevy Cheat Book 非…

架构篇26:高可用存储架构-集群和分区

文章目录 数据集群数据分区小结上一篇我们讨论了高可用存储架构中常见的双机架构,分别为主备复制、主从复制、双机切换和主主复制,并分析了每类架构的优缺点以及适应场景。 今天我们一起来看看另外两种常见的高可用存储架构:数据集群和数据分区。 数据集群 主备、主从、主…

金额格式化,三位数逗号分隔 vue2(借鉴)

在main.js全局注册 import Vue from vue;Vue.filter(currencyFormat, function(value) {if (!isNaN(parseFloat(value))) { // 判断输入值是否为有效数字value parseFloat(value);const parts value.toFixed(2).split(.); // 将小数点后两位转换为字符串并按小数点切割parts…

什么是servlet

什么是servlet 什么是servlet Servlet(Server Applet)是 Java Servlet 的简称,称为小服务程序或服务连接器,用 Java 编写的服务器端程序,具有独立于平台和协议的特性,主要功能在于交互式地浏览和生成数据…

如何使用Flutter构建高质量的用户界面

Flutter 是一种比较流行的移动应用开发框架,可以让开发者使用一个代码库构建高质量的 iOS 和 Android 应用。Flutter 以其快速、美观、高度可定制等优点吸引了开发社区的广泛关注。但如何使用 Flutter 构建高质量的用户界面呢?下面分为以下几个部分简单的…

安全防御第三次作业

作业:拓扑图及要求如下图 注:server1是ftp服务器,server2是http服务器 lsw1: 其中g0/0/0口为trunk 实现 1,生产区在工作时间内可以访问服务器区,仅可以访问http服务器 验证: 2,办公…

使用python写一个比Windows系统自带浏览器更好用的计算器

【介绍】 比Windows系统自带的还好用的计算器,感兴趣的可以试用一下。 1.支持括号优先级运算和平方、立方计算; 2.支持计算历史记录功能; 3.支持界面缩放和拖动; 4.支持钉在界面(界面最前置顶)&#xff0c…

Higress 开源一周年:新版本,新标准,新工具,新征程

作者:Higress 团队 历程回顾 Higress 开源一年时间,一共发布了 18 个 release 版本,收获了 40 多位社区贡献者和 1800 star,上图是这一年过来达成的一些关键的里程碑。 前面半年通过集成开源生态,打磨开源版本稳定性…

【技术分享】Ubuntu 20.04如何更改用户名

产品简介 本文适用于所有RK3568/RK3588平台产品在Ubuntu 20.04系统上如何更改用户名,本文以IDO-EVB3588开发板为例,在ubuntu20.04系统上修改用户名industio为usernew。 IDO-EVB3588开发板是一款基于RK3588平台的产品。该开发板集成了四核Cortex-A76和四…

使用自有数据集微调ChatGLM2-6B

1 ChatGLM2-6B介绍 ChatGLM是清华技术成果转化的公司智谱AI研发的支持中英双语的对话机器人。ChatGLM基于GLM130B千亿基础模型训练,它具备多领域知识、代码能力、常识推理及运用能力;支持与用户通过自然语言对话进行交互,处理多种自然语言任…

线性代数基础【6】二次型

第一节、二次型的基本概念及其标准型 一、基本概念 ①二次型 含n个变量x1,x2,…,xn,且每项都是2次的齐次多项式 ②标准二次型 只含有平方项不含交叉项的二次型称为标准二次型 ③二次型的标准化 设f(X)X^TAX 为一个二次型,经过可逆的线性变换XPY(即P为可逆矩阵)把二次型…

简述云原生基础定义及关键技术

云原生是什么 云原生是面向“云”而设计的应用,因此技术部分依赖于传统云计算的 3 层概念,基础设施即服务(IaaS)、平台即服务(PaaS)和软件即服务(SaaS)。 例如,敏捷的不可变基础设施交付类似于 IaaS,用来提供计算网络存储等基础资源,这些资源是可编程且不可变的,直…