自定义类型:【结构体】

我们知道C语言中有许多的类型,比如char,short,int等等类型。像是这些C语言本身就支持的类型叫做内置类型,但是有一些复杂对象,只有这些类型是完全不够的。比如人,或者一本书。那么我们就可以自己定义一些类型来实现。

一.结构体的基础知识

1.结构体的创建与初始化

结构是一些值的集合,这些值被称为成员变量。结构的每个成员可以是不同类型的变量。我们先来创建一个结构体类型,看一下是怎么用struct进行创建的。

struct student//student自己取的名字,是类型
{int age;//学生的年龄char name[30];//学生名字char sex[5];//学生性别。这三个都是成员变量。
}xiaoming;//xiaoming就是我创建的一个变量,这是其中的一种创建变量的方法,全局变量
int main()
{struct student xiaowang;//这也是创建变量的方法,是局部变量return 0;
}

关于结构体的初始化也是很简单的。比如还是上面我创建的学生类型。

	struct student xiaowang = {18,"小王","男"};//直接输入,按着顺序struct student xiaohong = {.age=20,.sex = "女",.name="小红"};//也可以乱序

这里的.是结构体引用操作符。它用于访问结构体的成员变量或成员函数。通过结构体变量名后面加上.,然后跟上成员的名称,就可以访问该成员。

匿名结构体类型

这里我单独介绍一下:匿名结构体类型。它有一个特点就是只能使用一次。

struct //关于匿名结构体,这里是没有名字的,我们想要创建变量就只能在这个结构体后面创建
{int age;char name[30];char sex[5];
}xiaowang = {18,"小王","男"};//就只能在这里创建,如果想在后面的函数里用这个类型,由于没有名字我们是用不了的。

这个东西不能再次使用就是因为没有名字,后面我们再次使用的话就没有办法去使用。

2.结构体自引用

提到结构体自引用就不得不要提到关于链表的内容了。关于链表,它是数据结构里的内容。而数据结构其实是数据在内存中的存储和组织的结构。链表就是其中之一。简单的说假如我要在内存中存储1,2,3,4,5.我们想到的有什么存储方式呢?

简单介绍一下关于链表的知识。像是图上的每一个“方块”,我们叫做节点。每一个节点都包含着数据域和指针域。数据域就是在这里的数字,指针域里面是指针,指向的是下一个节点的地址。

那么我们知道了上一个节点,就可以知道下一个节点地址。这就需要我们的结构体自引用了。

struct Node
{int date;//数据struct Node* next;//struct Node类型的指针,指向的就是下一个节点的地址
};

在结构体里包含了和自己类型一样的指针类型。这就实现了结构体的自引用,自己引用与自己同类型的对象。

二.结构体内存对齐

这个其实就是来探讨结构体大小的。

1.对齐规则

首先要了解结构体的对齐规则:

1.结构体的第一个成员对齐和结构体变量起始位置偏移量为0的地址处

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

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

-VS中默认的值为0。

-Linux中gcc没有默认对齐数,对齐数就是成员本身的大小。

3.结构体总大小为最大对齐数(结构体中的每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍

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

用代码来更深刻的了解对齐数的规则。

#include<stdio.h>
struct s
{char c1;int i;char c2;
};
int main()
{printf("%d", sizeof(struct s));return 0;
}

我们知道,c1是char类型,字节为1。i为int类型,字节为4。c2也是char类型,也是一个字节。注意这个可不是直接把他们相加,struct s的大小可不是6。

上面的代码运行出来的结果是12

为什么是12呢?这就是因为对齐数规则。来看这个图。

我们要先知道怎么要判断对齐数:对齐数=编译器默认的一个对齐数与该成员变量大小的较小值。

注意:这里我用的vs默认对齐数是8。而且Linux中gcc没有默认对齐数

struct s
{           //该成员大小    默认对齐数    对齐数char c1;//   1             8          1int i;  //   4             8          4 char c2;//   1             8          1
};

跟着我们步骤走一遍。

(1)根据对齐规则的第一条:第一个成员对齐和结构体变量起始位置偏移量为0的地址处。所以c1直接从0开始(就是黄色所占的地方)

(2)然后根据第二条规则:其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处,因为需要找4的倍数,所以我们就往下找,一直到偏移量为4的时候,找到了4的倍数。所以我们的i就从这里占领空间(深蓝色的部分)。

(3)然后还有一个char类型的变量,我们就继续找1的倍数,直接就是8就可以,c2在这里就开始占领空间(红色的部分)。

(4)到这里已经完成了大部分内容,但还没有结束。再根据第三条:结构体总大小为最大对齐数的整数倍。这里的最大对齐数就是上面我三个变量当中的最大对齐数,是4.所以我们就要找4的倍数。到红色部分的时候总共的数量才9,我们就继续往下找。直到到达11这个位置的时候。总共的数量才到12。所以这个结构体的大小就是12。

这几个步骤我们就成功的找到了结构体的大小,注意:我在上面画×的部分是浪费的内存。

还有一个重要的地方,就是结构体里嵌套结构体。这个要怎么样计算它的大小呢?这时我们就需要了解对齐规则的第四点。

struct s1
{char c1;//1   8   1char c2;//1   8   1int i;  //4   8   4
};
struct s2
{char c1;     //1   8   1struct s1 m; char c2;     //1   8   1
};
#include <stdio.h>
int main()
{printf("%d", sizeof(struct s2));return 0;
}

这里我就创建了两个结构体,在s2里嵌套了一个s1。其实这里我们就把struct s1当成一个普通的变量对待就行了。m的大小就是8个字节,vs默认对齐数是8,所以对齐数就是8。然后在根据我前面所说的前三个步骤。

注意一下第四条规则说的:

(1)前半句:嵌套的结构体的成员对齐到自己的成员中最大对齐数的整数倍处。这句话的意思代入到咱们的这个代码,就是找一下s1里成员的最大对齐数。这里就是4。那么我们就找4的倍数。找到了4.就从这里开始往后占领。一直占领struct s1的大小:8个字节。

(2)然后是后半句:结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。也就是说,我们不仅要找s2里的最大对齐数,还要找s1里的最大对齐数,结合起来找最大的对齐数,就是4。然后找4的倍数就行了。最后的结果是16。

 这就是我们的对齐规则。

2.为什么存在内存对齐

大部分参考资料是这么说的。

1.平台原因

不是所有的硬件平台都能访问任意地址上的任意数的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则输出硬件异常。

2.性能原因

数据结构应该尽可能的在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存仅仅需要一次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8个倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或写值了。否则我们可能需要进行两次内存访问,因为对象可能被分在两个8个字节内存块中。

下面我画图来看,为什么要对齐

struct s
{char c;int i;
};

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

虽然我们是拿空间换时间,但是我们也要尽量的节省空间,我在上面写的时候,其实大家也能发现这两个代码:

struct s
{           char c1;int i;  char c2;
};
struct s1
{char c1;char c2;int i;  
};

这两个结构体的成员虽然都一样,但是它们所占的字节大小确差了不少。所以我们可以总结一下这个

让占用空间小的成员尽量集中到一起

3.修改默认对齐数

这里我们就需要知道一个指令,#pragma,这是一个预处理指令,可以改变编译器默认对齐数。我来使用一下这个指令

#include<stdio.h>
#pragma pack(1)//把默认对齐数修改为1
struct s
{           char c1;//1   1   1int i;  //4   1   1char c2;//1   1   1
};
#pragma pack()//取消设置的对齐数,使对齐数恢复默认
int main()
{printf("%d", sizeof(struct s));return 0;
}

最终打印出来的结果是6

三.结构体传参

对于同一种功能的实现我用两种方法来写:

#include<stdio.h>
struct S
{int arr[100];int n;double d;
};
void print1(struct S tmp)
{int i = 0;for (i = 0; i < 5; i++){printf("%d ", tmp.arr[i]);}printf("%d ", tmp.n);printf("%lf\n", tmp.d);
}
void print2(struct S* ps)
{int i = 0;for (i = 0; i < 5; i++){printf("%d ", ps->arr[i]);}printf("%d ", ps->n );printf("%lf\n", ps->d );
}
int main()
{struct S s = { {1,2,3,4,5},10,1.22 };print2(&s);return 0;
}

这里我有两个函数,一个是print1一个是print2。一个是值传递的方式,一种是地址传递的方式。我们知道,在给函数传递参数的时候,这个函数会再另外开辟一个空间来存放传递过来的值。那么我们可以思考一下,我们是使用print1好还是print2好。答案当然是print2。因为指针不是4个字节就是8个字节,当我们传递参数的时候,只需要传递地址开辟的空间小。如果传递的是结构体,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降。

四.结构体实现位段

1.什么是位段

位段,位段。这个位其实指的就是二进制位,关于位段先介绍几点需要注意的地方。

1.位段的成员必须是int,unsigned int或signed int,在C99中位段成员的类型也可以选择其他类型。

2.位段的成员名后面有一个冒号和一个数字(这个数字指的就是有多少个二进制位,也就是多少bit位)。

struct S
{int a : 2;int b : 5;int c : 10;int d : 30;
};

我们知道一个整型是有四个字节的,32个比特位。当我们在结构体成员后面加上冒号和一个数字的时候,就相当于是改变了这个整型所占的bit位。比如我们想创建一个成员a=3;把3转化为2进制后是11,这里我们就只需要两个二进制位来表示就可以了。所以我们在这里加一个冒号和2,就只占了2个bit位,就足够来表达出这个3了。

所以,位段是专门来节省内存的。

2.位段的内存分配

1.位段的成员必须是int,unsigned int,signed int或者是char等类型。

2.位段的空间上是按照以4个字节或者1个字节来开辟的。(也就是假如我是int类型,我就一次开辟4个字节,如果是char就开辟1个字节,如果不够了再来开辟)。

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

来看一下位段到底是怎么分配空间的:

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

如图所示:

也就是说一次会开辟一个字节的大小,然后根据位段后面的数字来判断出a,b,c,d各自占多少个bit位。比如这里a是三个bit位先占3个bit位,然后b是有4个bit位,紧接着占取空间。然后这八个字节就只剩下了一个bit位,不够下一个c来占用了。所以我就再次开辟一个字节来继续让c占取。后面的d也是同样的原理。

注意:1.申请到同一块内存中,从左向右使用,还是从右向左使用的,不确定。这里我用的vs是从右向左使用的。

2.剩余的空间,不足以下一个成员使用的时候,是直接浪费掉了。

这就是位段的内存分配的知识了。

还有一个代码分享一下:

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

根据我刚才说的那些,大家应该也可以很轻松的就算出来这个值是多少了。但是我想说的点不再这里。这里我给每一个结构体成员都赋予了值,那么我给a赋予的值是10,它的二进制是1010,它足足有4位,但是我只给了a3个bit位。这里也很简单,直接取后面的三位010放进去就行了。也就是上面的黄色区域,蓝色区域放的是12,所以就是1100,刚好4位,直接放进去就好了。根据这个方式把我开辟的所有空间都放满的值就是

 0110 0010 0000 0011 0000 0100 

用16进制存放的话就是     6        2       0       3      0       4

在内存中就是这样:

3.位段的跨平台问题

虽然位段的好处很多,但是关于位段也有很大的弊端

1.int位段被当成有符号数还是无符号数不确定

2.位段中最大位的数目不能确定(比如16位机器最大是16,32位机器最大是32,我写一个17,在16位机器会出现问题)

3.位段中的成员在内存中从左向右还是从右向左分配,标准没有定义

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

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

4.位段的使用事项

位段的结构成员共同使用一个字节,这样有些成员的起始位置不是某个字节的起始位置,那么这些位置也是没有地址的。因为在内存中每个字节分配一个地址,一个字节内部的bit位是没有地址的。

因为没有地址,所以我们也就不能对这些位段的成员使用&操作符,只能是先输入放在一个变量中,然后赋值给位段的成员。

只有在结构体才能使用位段。

整个结构体在这里就基本上写完了,感谢大家的观看,如果有错误,还请大家多多指正。

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

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

相关文章

mysql--事务四大特性与隔离级别

事务四大特性与隔离级别 mysql事务的概念事务的属性事务控制语句转账示例 并发事务引发的问题脏读脏读场景 不可重复读幻读幻读场景 事务的隔离级别读未提交读已提交可重复读&#xff08;MySQL默认&#xff09; 总结 mysql事务的概念 事务就是一组操作的集合&#xff0c;他是一…

鸿蒙OS开发实例:【手撸服务卡片】

介绍 服务卡片指导文档位于“开发/应用模型/Stage模型开发指导/Stage模型应用组件”路径下&#xff0c;说明其极其重要。 本篇文章将分享实现服务卡片的过程和代码 准备 请参照[官方指导]&#xff0c;创建一个Demo工程&#xff0c;选择Stage模型 鸿蒙OS开发更多内容↓点击…

面试经典150题【111-120】

文章目录 面试经典150题【111-120】67.二进制求和190.颠倒二进制位191.位1的个数136.只出现一次的数字137.只出现一次的数字II201.数字范围按位与5.最长回文子串97.交错字符串72.编辑距离221.最大正方形 面试经典150题【111-120】 六道位运算&#xff0c;四道二维dp 67.二进制…

PCB损耗来源

信号经过PCB板会产生损耗&#xff0c;主要包括导体损耗&#xff0c;介电损耗和辐射损耗 导体损耗&#xff1a;导体损耗是由于电流流动过程中产生电阻损耗而发热。 介电损耗&#xff1a;介电损耗是由于电场通过介质时分子的交替极化和晶格碰撞造成的。 辐射损耗&#xff1a;辐…

YOLOv9 实战指南:打造个性化视觉识别利器,从零开始训练你的专属测试集

论文地址&#xff1a;YOLOv9: Learning What You Want to Learn Using Programmable Gradient Information GitHub&#xff1a;WongKinYiu/yolov9: Implementation of paper - YOLOv9: Learning What You Want to Learn Using Programmable Gradient Information (github.com)…

Linux---多线程(下)

前情提要&#xff1a;Linux---多线程(上) 七、互斥 临界资源&#xff1a;多线程执行流共享的资源就叫做临界资源临界区&#xff1a;每个线程内部&#xff0c;访问临界资源的代码&#xff0c;就叫做临界区互斥&#xff1a;任何时刻&#xff0c;互斥保证有且只有一个执行流进入临…

PL/SQL的词法单元

目录 字符集 标识符 分隔符 注释 oracle从入门到总裁:​​​​​​https://blog.csdn.net/weixin_67859959/article/details/135209645 PL/SQL块中的每一条语句都必须以分号结束。 一个SQL语句可以跨多行&#xff0c;但分号表示该语句的结束:一行中也可以有多条 SQL语句&…

3.28(迭代搜索算法 + java学习总结)

迭代加深搜索 迭代加深算法是一在DFS的基础上添加搜索深度限制的搜索方法&#xff1b; 其核心思想是从深度为0的地方开始搜索&#xff0c;然后逐步加深搜索深度&#xff0c;重新搜索一遍&#xff1b;这对于那些已知答案在浅层&#xff0c;但整个树或图存在极多分支的情况&#…

【前端Vue】HR-saas中台项目开发md文档第1篇:vuex基础-介绍,vuex基础-初始化功能【附代码文档】

HR-saas中台管理项目开发完整教程&#xff08;附代码资料&#xff09;主要内容讲述&#xff1a;vuex基础-介绍,vuex基础-初始化功能,vuex基础-state,vuex基础-mutations,vuex基础-actions,vuex基础-getters。项目课设计&#xff0c;人力资源的环境搭建vue-element-admin的了解和…

[flask]http请求//获取请求头信息+客户端信息

在网站中查询请求头信息&#xff0c;可以通过以下操作进行 右键然后选择检查 进入改页面后选择文档&#xff0c;刷新一下页面就好了 获取所有的请求头信息 print(request.headers, type(request.headers)) 在flask模块中&#xff0c;使用上面的输出函数就可以查看到有关于请求…

Qt 窗口MainWindow(上)

Qt 窗口是通过 QMainWindow 类来实现的。 QMainWindow 是一个为用户提供主窗口程序的类&#xff0c;继承自 QWidget 类&#xff0c;并且提供了⼀个预定义的布局。QMainWindow 包含一个菜单栏&#xff08;menubar&#xff09;、多个工具栏(toolbars)、多个浮动窗口&#xff08;…

第十四届蓝桥杯JavaA组省赛真题 - 特殊日期

解题思路&#xff1a; 暴力秒了 public class Main {public static void main(String[] args) {int cnt 0;for (int i 1900; i < 9999; i) {for (int j 1; j < 12; j) {for (int k 1; k < days(i, j); k) {if (sum(i) sum(j) sum(k)) cnt;}}}System.out.print…

安防监控视频汇聚平台EasyCVR启用图形验证码之后如何调用login接口?

视频综合管理平台EasyCVR视频监控系统支持多协议接入、兼容多类型设备&#xff0c;平台可以将区域内所有部署的监控设备进行统一接入与集中汇聚管理&#xff0c;实现对监控区域的实时高清视频监控、录像与存储、设备管理、云台控制、语音对讲、级联共享等&#xff0c;在监控中心…

【Vue】可拖拽侧边栏实现

在本篇博客中&#xff0c;我们将探讨如何在 Vue.js 项目中实现一个可拖拽的侧边栏。此功能可以通过修改 HTML 和 Vue 组件的脚本来实现。 首先&#xff0c;我们需要在 HTML 文件中定义侧边栏的容器和用于拖拽的元素。在 Vue 组件中&#xff0c;我们将使用 Vue 的响应式系统来追…

力扣73. 矩阵置零

Problem: 73. 矩阵置零 文章目录 题目描述思路复杂度Code 题目描述 思路 思路1&#xff1a;利用一个等大的矩阵判定 复制一个与原始矩阵一样大的矩阵temp&#xff0c;遍历temp时若temp[i][j] 0&#xff0c;则将martix对应的行与列均设置为0 思路2&#xff1a;利用两个一维矩阵…

【Linux】UnixBench介绍、分数调优思路以及测试2D3D的方法

一.简介 unixbench是一个用于测试unix系统性能的工具&#xff0c;也是一个比较通用的benchmark&#xff0c; 此测试的目的是对类Unix 系统提供一个基本的性能指示&#xff0c;很多测试用于系统性能的不同方面&#xff0c;这些测试的结果是一个指数值&#xff08;index value&am…

幻兽帕鲁服务器价格表_阿里云/腾讯云/京东云/华为云报价大全

2024年全网最全的幻兽帕鲁服务器租用价格表&#xff0c;阿里云幻兽帕鲁游戏服务器26元1个月、腾讯云32元一个月、京东云26元一个月、华为云24元1个月&#xff0c;阿腾云atengyun.com整理最新幻兽帕鲁专用4核16G、8核16G、8核32G游戏服务器租用价格表大全&#xff1a; 阿里云幻…

C++类的六个默认成员函数(详细解析与总结)

目录 前言&#xff1a; 一、构造函数 a.特点 b.注意事项 1.首先明确什么是默认构造函数 2.默认构造函数对内置类型与自定义类型的处理 c.总结 二、析构函数 a.特点 b.注意事项 1.什么时候写析构函数&#xff1f; 2.析构函数对内置类型与自定义类型的处理 c.总结 …

pythonselenium自动化测试实战项目

说明&#xff1a;本项目采用流程控制思想&#xff0c;未引用unittest&pytest等单元测试框架 一.项目介绍 目的 测试某官方网站登录功能模块可以正常使用 用例 1.输入格式正确的用户名和正确的密码&#xff0c;验证是否登录成功&#xff1b; 2.输入格式正确的用户名和不…

【面试经典 | 150】单词拆分

文章目录 Tag题目来源解题思路方法一&#xff1a;动态规划 写在最后 Tag 【动态规划】【字符串】 题目来源 139. 单词拆分 解题思路 方法一&#xff1a;动态规划 定义状态 定义 dp[i] 表示字符串 s 前 i 个字符组成的字符串&#xff08;s[0, ..., i-1]&#xff09;是否能被…