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

news2024/11/17 19:44:20

目录

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:位段在计算的时候也要考虑内存对齐


感谢阅读!

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

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

相关文章

下载并安装nacos 2.3 for arm64

客户组织安全测试,我们系统测出了好几个高危问题,其中大部分是关于nacos的。 原先的nacos版本太低了,是1.3的。现在(2024.01)已经是2.3了,应该装个新的。我们使用docker安装nacos,原本很简单的…

程序员必备的20个学习网站

今天好学编程小编整理了20个程序员必备的学习网站,此篇对于新手程序员比较有用,技术老鸟们也可以查缺补漏。话不多说,纯纯干货呈上,赶紧点个赞收藏,以后会用得上! 技术网站类 1、博客园 一个面向开发者的…

基于SSM的蛋糕甜品店管理系统(有报告)。Javaee项目。ssm项目。

演示视频: 基于SSM的蛋糕甜品店管理系统(有报告)。Javaee项目。ssm项目。 项目介绍: 采用M(model)V(view)C(controller)三层体系结构,通过Spring…

【网站项目】基于SSM的251国外摇滚乐队交流和周边售卖系统

🙊作者简介:拥有多年开发工作经验,分享技术代码帮助学生学习,独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。🌹赠送计算机毕业设计600个选题excel文件,帮助大学选题。赠送开题报告模板&#xff…

opencv012 滤波器04 中值滤波,双边滤波

中值滤波 取中位数,可以处理椒盐噪音 CV自带medianBlur函数dst cv2.medianBlur(src, ksize) 参数说明:1.src: 需要滤波的图片;2.ksize:核大小,必须是比1大的奇数【举个例子:3,5,7……

司铭宇老师:企业销售培训:企业培训销售效果评估与质量提升

企业销售培训:企业培训销售效果评估与质量提升 随着市场竞争的日益激烈,企业越来越重视员工培训,希望通过高质量的培训提高员工的技能和素质,进而提升企业的竞争力和业绩。然而,在实践中,很多企业的培训销售…

Redis——list以及他的应用场景

介绍 :list 即是 链表。链表是一种非常常见的数据结构,特点是易于数据元素的插入和删除并且且可以灵活调整链表长度,但是链表的随机访问困难。许多高级编程语言都内置了链表的实现比如 Java 中的 LinkedList,但是 C 语言并没有实现…

安泰电子ATA-3080功率放大器在雷达系统无线电能传输中的具体应用

雷达技术在现代通信和导航系统中起着至关重要的作用。而功率放大器作为一种关键的电子器件,在雷达无线电能传输中扮演着不可或缺的角色。功率放大器负责将来自雷达发射机的低功率无线电信号放大到足够高的水平,以便能够穿透大气层,传输到远距…

江大白 | 万字长文图解Numpy教程,看这一篇就够了!

本文来源公众号“江大白”,仅用于学术分享,侵权删,干货满满,有超级详细的图解。 原文链接:万字长文图解Numpy教程,看这一篇就够了! (qq.com) 以下文章来源于博客:Medium 作者&…

周鸿祎回应坚定支持华为:因为 360 也被制裁了

在昨天的华为鸿蒙生态千帆启航仪式上,360集团创始人兼CEO周鸿祎发表演讲表示,360坚定地支持华为的决定源于双方都曾遭到制裁。周鸿祎在演讲中提到:“在华为最早被制裁的时候,我们是少数几个公开站出来坚定支持华为的公司。其实也很…

transformer详解

transformer详解 1.从全局角度概括transformer2.位置编码详细解读3.注意力机制4.残差连接5.Batch Normal6.layer normal7.decode 1.从全局角度概括transformer 一个典型的编码器-解码器的结构,类似于sequence-to-sequence 这6(可以自己定)个…

接口文档swagger2的使用

Spring-接口文档swagger2 1、swagger/knife4j 接口文档配置 ​ knife4j是swagger的增强版本&#xff0c;更加的小巧、轻量&#xff0c;功能也是更加的完善&#xff0c;UI也更加的清晰&#xff1b;可以从swagger到knife4j无缝切换。 1.1 引入相关依赖 <!--接口文档的开发:…

FastDeploy项目简介,使用其进行(图像分类、目标检测、语义分割、文本检测|orc部署)

FastDeploy是一款全场景、易用灵活、极致高效的AI推理部署工具&#xff0c; 支持云边端部署。提供超过 &#x1f525;160 Text&#xff0c;Vision&#xff0c; Speech和跨模态模型&#x1f4e6;开箱即用的部署体验&#xff0c;并实现&#x1f51a;端到端的推理性能优化。包括 物…

Django从入门到精通(三)

目录 七、ORM操作 7.1、表结构 常见字段 参数 示例 7.2、表关系 一对多 多对多 第一种方式 第二种方式 7.3、连接MYSQL 7.4、数据库连接池 7.5、多数据库 读写分离 分库&#xff08;多个app ->多数据库&#xff09; 分库&#xff08;单app&#xff09; 注意…

坚持刷题 | 平衡二叉树

文章目录 题目考察点代码实现实现总结对实现进一步改进扩展提问 坚持刷题&#xff0c;老年痴呆追不上我&#xff0c;今天继续二叉树&#xff1a;平衡二叉树 题目 110.平衡二叉树 考察点 递归能力&#xff1a; 能否使用递归来解决问题。树的基本操作&#xff1a;能否正确地访…

10.Elasticsearch应用(十)

Elasticsearch应用&#xff08;十&#xff09; 1.为什么需要聚合操作 聚合可以让我们极其方便的实现对数据的统计、分析、运算&#xff0c;例如&#xff1a; 什么品牌的手机最受欢迎&#xff1f;这些手机的平均价格、最高价格、最低价格&#xff1f;这些手机每月的销售情况如…

常用电子器件学习——光耦

光耦介绍 光耦合器一般由三部分组成&#xff1a;光的发射、光的接收及信号放大。 输入的电信号驱动光发射源&#xff0c;使之发光&#xff0c;被光探测器接收而产生光电流&#xff0c;再经过进一步放大后输出。这就完成了电—光—电的转换&#xff0c;从而起到输入、输出、隔离…

Ubuntu 22.04安装Nginx负载均衡

君衍. 一、编译安装Nginx二、轮询算法实现负载均衡三、加权轮询算法实现负载均衡四、ip_hash实现负载均衡 一、编译安装Nginx 这里我们先将环境准备好&#xff0c;我使用的是Ubuntu22.04操作系统&#xff1a; 这个是我刚安装好的&#xff0c;所以首先我们进行保存快照防止安装…

APP出海广告变现对接Admob

AdMob成立于2006年&#xff0c;并于2009年被Google收购。从那以后&#xff0c;AdMob在游戏及应用广告变现的重要性不断上升。凭借Google的血统&#xff0c;AdMob广告网络拥有其他广告平台不具备的优势&#xff1a;它可以访问无数的Google广告客户数据库。不仅如此&#xff0c;A…

线性代数速通

二---矩阵 逆矩阵 抽象矩阵求逆 数字型矩阵求逆 二阶矩阵求逆秒杀 解矩阵方程 方阵 伴随矩阵 三---向量组的线性相关性 线性表示 数字型向量组 线性相关性判断 抽象型向量组 线性相关性判断 向量组的秩与极大无关组 四---线性方程组 齐次方程组 基础解系 通解 非齐…