C中自定义类型——结构体

一.前言

在C语言中,不仅有int、char、short、long等内置类型,C语言还有一种特殊的类型——自定义类型。该类型可以由使用者自己定义,可以解决一些复杂的个体。

二.结构体

2.1结构体的声明

我们在利用结构体的时候一般是用于描述一些有多种因素的对象。比如我们要描述一个学生,那么学生就有他的姓名,性别,年龄,如果我们使用内置类型的话,非常麻烦,我们就可以定义一个结构体类型用来描述一个学生:

//定义了一个学生类型
struct student
{char name[15];//学生姓名char sex[5];//性别int age;//年龄
};

 我们创建了类型之后还得依靠此类型创建变量。

2.2结构体变量的创建和初始化

我们要时刻记住,我们创建的是一个类型,而不是一个变量,我们要根据此类型来创建我们需要的变量。 下来,我们依据上面创建的学生类型来创建变量并进行初始化:

#include <stdio.h>//定义了一个学生类型
struct student
{char name[15];//学生姓名char sex[5];//性别int age;//年龄
};int main()
{//在创建变量的同时进行初始化//该初始化必须得按照结构体内部成员的顺序进行初始化struct student stu1 = { "zhangsan","nan",18 };printf("%s\n", stu1.name);printf("%s\n", stu1.sex);printf("%d\n", stu1.age);//利用访问操作符可实现自定义顺序进行初始化struct student stu2 = { .age = 21,.name = "lisi",.sex = "nv" };printf("%s\n", stu2.name);printf("%s\n", stu2.sex);printf("%d\n", stu2.age);return 0;
}

我们除了在主函数内创建结构体变量外,还可以在定义结构体的同时进行创建变量:

#include <stdio.h>struct student
{char name[15];char sex[5];int age;
}stu1;int main()
{struct student stu2 = { 0 };return 0;
}

stu1和stu2都是结构体类型变量,区别是stu1是全局变量存放在静态区,而stu2是局部变量存放在栈上。

2.3结构体访问操作符

结构体访问操作符有.(句点操作符)和->(箭头操作符)。句点操作符用于结构体变量的访问,而箭头操作符用于结构体指针来访问结构体变量。该操作符我在之前已经介绍过了,大家可以看我之前的博客。结构体中的访问运算符-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/xsc2004zyj/article/details/136722599?spm=1001.2014.3001.5502

 2.4结构体的特殊声明

结构体除了正常的声明外,还有一种特殊的声明方式,叫做匿名结构体。

匿名结构体就是在声明结构体的时候不给结构体标识符,而直接创建一个变量。

//匿名结构体,该结构体没有标识符
struct
{char name[15];char sex[5];int age;
}stu;

而对于匿名结构体类型来说,该结构体基本上只能使用一次,因为该结构体没有标识符,后续没法再对此来创建变量。

下面提出一个问题:

#include <stdio.h>struct
{int a;char b;float c;
}x;struct
{int a;char b;float c;
}*p;int main()
{p = &x;return 0;
}

主函数中的p = &x;是否合法?

答案是:非法,因为以上两个结构体的成员变量完全一样,并且都使用匿名的方式,所以编译器会把两个声明当成两个完全不同的类型,所以是非法的。

2.5结构体的自引用 

如果我们在描述某个对象的时候,需要用到结构体的自引用,我们应该如何写?

struct stu
{int age;struct stu s;
};

上面代码正确么?如果正确,那么sizeof(struct stu)的大小是多少呢?上面的代码其实是错误的,如果这样进行自引用,那么一个结构体变量的空间将会无穷大,因为一个结构体里面永远还有一个结构体。

正确的应该是存放一个该结构体的指针,因为一个指针的大小不是四个字节,就是八个字节。而且该指针也指向了一个该类型的变量,所以也实现了结构体的自引用。

struct stu
{int age;struct stu *s;
};

2.6利用typedef重命名结构体类型

我们在创建结构体变量的时候每次都要写出struct tag x;这样写起来非常麻烦,并且有人可能粗心大意而忘记struct。所以,我们在声明结构体的时候,可以利用typedef关键字给该结构体重新起个名字,用该名字来创建变量。

#include <stdio.h>typedef struct student
{char name[14];int age;
}stu;int main()
{stu s;struct student s2;return 0;
}

此时,在声明时分号前面就不是创建变量了,还是该结构体的一个新名字——stu。在创建变量的时候,stu和struct student是一个意思。

 我们也可以利用typedef来解决匿名结构体的问题,我们只需要给匿名结构体重新起一个名字,利用该名字创建变量就行了。这时,该匿名结构体与正常声明的结构体没有区别。

#include <stdio.h>typedef struct
{char a;int b;
}X;int main()
{X x;X s;return 0;
}

这样就解决了匿名结构体只能使用一次的局限。我们可以利用重命名的结构体进行创建变量,初始化等操作

 三.结构体内存对齐

我们已经基本了解了结构体的内容,下来我们来讨论结构体中的热门话题:结构体的大小。这也是最近热门的考点:结构体内存对齐。

3.1对齐规则

在理解内存对齐之前,我们得先了解结构体内存对齐的规则。

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

 下来我来一一介绍结构体的对齐规则。我们通过一个练习来引出:

struct S1
{char c1;int i;char c2;
};
int main()
{printf("%d\n", sizeof(struct S1));return 0;
}

我们利用该结构体来进行解说,判断该结构体的大小。有的人会想,该结构体的成员两个char,一个int,那就占6个字节,是这样么?

我们看到,结果是12个字节,与我们的猜测不符。这就是因为结构体内部存在内存对齐规则。

在上图,我借助练习题详细介绍了结构体对齐规则如何理解,以及内存是如何进行对齐的。大家先仔细理解上图,后进行下面几个结构体大小的练习。

3.1.1练习1

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

 

3.1.2练习

#include <stdio.h>struct S3
{double d;char c;int i;
};int main()
{printf("%zd\n", sizeof(struct S3));return 0;
}

 

3.1.3练习3

#include <stdio.h>
//结构体嵌套struct S3
{double d;char c;int i;
};struct S4
{char c1;struct S3 s3;double d;
};int main()
{printf("%zd\n", sizeof(struct S4));return 0;
}

3.2为什么存在内存对齐

大部分的参考资料是这样说的:

3.2.1平台原因(移植原因):

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

3.2.2性能原因:

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

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

 3.2.3那么如何尽可能节省空间的浪费呢?

我们来分析一下,这两个结构体:

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

S1和S2的成员类型都一样,只是存储的顺序不同,那这两个结构体的大小是否一样大呢?

我们看到S1和S2的成员虽然相同,但是两者的大小却不同,我们来分析一下。

 那么,我们在设计结构体的时候,既要满足对齐,又要节省空间,如何做到:

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

 3.2.4修改默认对齐数

#pragma这个预处理指令可以修改编译器的默认对齐数

//利用#pragma修改默认对齐数#include <stdio.h>
#pragma pack(1)//设置默认对⻬数为1 
struct S
{char c1;int i;char c2;
};
#pragma pack()//取消设置的对⻬数,还原为默认 int main()
{//输出的结果是什么? printf("%d\n", sizeof(struct S));return 0;
}

我们在上面已经分析该结构体的大小为12个字节(在VS默认8为对齐数的情况下),而我们利用#pragma已经将编译器的默认对齐数修改为1,结果还是12个字节么?

 我们分析后得出,修改默认对齐数后该结构体的大小变成了6字节。

四.结构体传参

我们创建好结构体变量后,可能需要把该结构体变量的某个成员变量传给某个函数。而我们到现在由两种传参的方式:值传递地址传递。那我们应该选择哪一种方式对结构体变量进行传参呢?

//结构体传参#include <stdio.h>struct S
{char c1;char c2;int i;
};//值传递
void test1(struct S s)
{printf("%c\n", s.c1);printf("%c\n", s.c2);printf("%d\n", s.i);
}//地址传递
void test2(struct S* ps)
{printf("%c\n", ps->c1);printf("%c\n", ps->c2);printf("%d\n", ps->i);
}int main()
{struct S s = { 'a','b',10 };test1(s);test2(&s);return 0;
}

我们看到,无论是值传递,还是地址传递,都可以达到我们的目的。那我们到底应该选那种方式呢?

我们首先要知道,值传递中的形参是实参的一份临时拷贝,会在栈上存储其拷贝内容,也就意味着会消耗内存,那如果该结构体的大小非常大的话,我们在栈上就会消耗很多内存。

无论是何种传参方式,都会进行压栈操作,而值传递在压栈过程中机会在时间和空间上有大量的系统开销。而地址传递的话,指针的大小不是8个字节就是4个字节,在压栈过程中不会有太多空间和时间上的开销。 

所以,在结构体传参的时候,要传结构体的地址

 五.结构体实现位段

利用结构体实现位段。位段这个概念大家可能没听过,但段位大家肯定不陌生。位段是一种特殊的结构体类型,位段必须得依靠结构体来实现。

5.1什么是位段?

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

  1. 位段的成员必须是int 、unsigned int、或者signed int,在C99中位段成员的类型也可以选择其他类型。
  2. 位段的成员后面有一个冒号和一个数字。

 比如:

struct S
{int _a : 2;int _b : 5;int _c : 10;int _e : 30;
};

 通常在创建位段式结构体的时候习惯在每个变量前面加上下划线,以此来说明这是位段式结构体。

我们看到结构体和位段式结构体的区别就在于位段的每个成员后面多了一个冒号和一个数字,这是什么意思呢?

这每个成员后面的数字表示该成员占的bit位,_a占2个bit位,_b占5个bit位,_c占10个bit位。为什么要这样设置变量呢?

我们想,如果_a里面只会存1,2,3这三个数,1二进制就是01,2二进制就是10,3二进制就是11,所以存这三个数用两个bit位就够了。以此类推,_b就是存只需5个bit位就能表示的数,_c就是只存10个bit位就能表示的数。这样就可以大大减少空间的浪费。那位段式结构体是怎样储存数据的呢?是如何达到节省空间的?下面我们来了解位段的内存对齐规则。

5.2位段的内存对齐

位段的成员最好都是同一种类型的,位段会根据成员类型来开辟空间,比如成员是int型,就一次开辟4个字节,如果是char型,就一次开辟一个字节。下面我们举一个例子。

 那该位段的大小是不是跟我的结论一样呢?

 我们看到,该位段的大小与我们的推测相同。我们再来看一下该位段是如果写入数据的。

 我们分析完之后,在VS上调试一下,发现s的内存如下,和我们分析的一样,占三个字节,存储的是0a 0c 05。

 5.3位段的跨平台问题

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

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

 5.4位段的应用

下图是网络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要几个bit位就能描述,这里使用位段,能够实现想要的效果,也节省了空间,这样网络传输的数据报大小也会较小⼀些,对网络的畅通是有帮助的。

 5.5位段使用的注意事项

位段的几个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的。

 所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输入值,只能是先输入放在⼀个变量中,然后赋值给位段的成员。

#include <stdio.h>
struct A
{int _a : 2;int _b : 5;int _c : 10;int _d : 30;
};
int main()
{struct A sa = { 0 };scanf("%d", &sa._b);//这是错误的 //正确的⽰范 int b = 0;scanf("%d", &b);sa._b = b;return 0;
}

完! 

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

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

相关文章

javaweb配置JSTL

首先配置好javaweb项目。 在网上下载好jakarta-taglibs-standard并解压。 在web/WEB-INF目录下创建lib目录。 在jakarta-taglibs-standard目录下lib目录内的两个.jar文件复制到javaweb项目lib目录下。 将这两个.jar包导入库。 在idea菜单栏找到“文件”并打开&#xff0c;点…

nvm更新node版本

1、nvm安装和管理多个 Node.js 版本&#xff1a;NVM 允许用户在计算机上同时安装多个不同版本的 Node.js。这使得开发人员可以轻松地在不同的项目中使用不同的 Node.js 版本&#xff0c;而无需手动安装或卸载。 2、nvm切换 Node.js 版本&#xff1a;通过 NVM&#xff0c;用户可…

好菜每回味道不同--建造者模式

1.1 炒菜没放盐 中餐&#xff0c;老板需要每次炒菜&#xff0c;每次炒出来的味道都有可能不同。麦当劳、肯德基这些不过百年的洋快餐却能在有千年饮食文化的中国发展的那么好呢&#xff1f;是因为你不管何时何地在哪里吃味道都一样&#xff0c;而鱼香肉丝在我们中餐却可以吃出上…

Langchain-Chatchat 从入门到精通(基于本地知识库的问答系统)(更新中)

目录 前言一、Langchain-Chatchat介绍1-1、Langchain-Chatchat介绍1-2、LangChainChatGLM 工作流1-3、文档角度的工作流 二、快速上手2-0、硬件要求2-1、环境配置2-2、模型下载2-3、初始化知识库和配置文件2-4、一键启动 三、配置文件详解&#xff08;config目录下&#xff09;…

MybatisPlus实现数据权限隔离

引言 Mybatis Plus对Mybatis做了无侵入的增强&#xff0c;非常的好用&#xff0c;今天就给大家介绍它的其中一个实用功能&#xff1a;数据权限插件。 数据权限插件的应用场景和多租户的动态拦截拼接SQL一样。建议点赞收藏关注&#xff0c;方便以后复习查阅。 依赖 首先导入M…

【Java集合】面试题汇总

Java 集合Java 集合概览1. List, Set, Queue, Map 四者的区别&#xff1f;2. ArrayList 和 Array&#xff08;数组&#xff09;的区别&#xff1f;3. ArrayList 和 Vector 的区别?4. Vector 和 Stack 的区别?&#xff08;了解即可&#xff09;5. ArrayList 可以添加 null 值吗…

【端云一体化开发】云函数本地运行/调试启动失败的两种解决方案

最近本地调试云函数一直出现这个错误&#xff1a;Before launch task execute failed! details:java.lang.lllegalStateException: npm installfailed 这个问题的原因似乎是运行云函数的时候会重新下载 npm 及相关依赖文件&#xff0c;但是 DevEco 的 npm 模块出错导致这个步骤…

智慧园区平台再升级!智慧迭代,服务升级

伴随物联网、人工智能等技术的迅速发展和智能化水平的提高&#xff0c;智慧园区成为了现代区域经济高质量发展的重要组成部分&#xff0c;上承智慧城市的建设&#xff0c;下接智慧运营和管理。智慧园区是一种基于信息技术的智能化管理模式&#xff0c;通过物联网、大数据、人工…

java中常见的几种排序

常见的几种排序整理 冒泡排序选择排序插入排序希尔排序快速排序归并排序堆排序 冒泡排序 思想&#xff1a;对比当前值的下一个值&#xff0c;如果大就交换位置 代码&#xff1a; /*** 冒泡排序*/ public class bubbleSort {public static void main(String[] args) {int[] ar…

【域适应】深度域适应常用的距离度量函数实现

关于 深度域适应中&#xff0c;有一类方法是实现目标域和源域的特征对齐&#xff0c;特征对齐的衡量函数主要包括MMD&#xff0c;MK-MMD&#xff0c;A-distance&#xff0c;CORAL loss&#xff0c; Wasserstein distance等等。本文总结了常用的特征变换对齐的函数定义。 工具 …

初始C++之缺省参数 函数重载 引用

初始C之缺省参数 函数重载 引用& 文章目录 初始C之缺省参数 函数重载 引用&一、缺省参数1.1 缺省参数的定义1.2 缺省参数的分类1.3 注意事项 二、 函数重载2.1 函数重载的定义2.2 参数个数不同2.3 参数类型不同2.4 类型顺序不同2.5 为什么C语言不支持函数重载 三、引用…

OpenHarmony南向开发案例:【智能保险柜】

样例简介 智能保险柜实时监测保险柜中振动传感器&#xff0c;当有振动产生时及时向用户发出警报。在连接网络后&#xff0c;配合数字管家应用&#xff0c;用户可以远程接收智能保险柜的报警信息。后续可扩展摄像头等设备&#xff0c;实现对危险及时报警&#xff0c;及时处理&a…

探究 ChatGPT 的心脏--Transformer(基础知识第一篇)

Transformer 是 ChatGPT 的核心部分&#xff0c;如果将 AI 看做一辆高速运转的汽车&#xff0c;那么 Transformer 就是最重要的引擎。它是谷歌于 2017 年发表的《Attention is All You Need》中提出的 Sequence-to-sequence 的模型&#xff0c;诞生之后便一统江湖&#xff0c;在…

项目存放在git上,在jenkins使用docker打包并推送到Ubuntu上运行

项目添加dockerfile 在需要打包的工程的根目录添加Dockerfile文件&#xff0c;文件内容&#xff1a; # 设置JAVA版本 FROM openjdk:8 # 指定存储卷&#xff0c;任何向/tmp写入的信息都不会记录到容器存储层 VOLUME /tmp# 拷贝运行JAR包 ARG JAR_FILE COPY ${JAR_FILE} app.jar…

蓝桥杯练习系统(算法训练)ALGO-958 P0704回文数和质数

资源限制 内存限制&#xff1a;256.0MB C/C时间限制&#xff1a;1.0s Java时间限制&#xff1a;3.0s Python时间限制&#xff1a;5.0s 一个数如果从左往右读和从右往左读数字是完全相同的&#xff0c;则称这个数为回文数&#xff0c;比如898,1221,15651都是回文数。编写…

内核驱动更新

1.声明我们是开源的 .c 文件末尾加上 2.在Kconfig里面修改设备&#xff0c;bool&#xff08;双态&#xff09;-----》tristate&#xff08;三态&#xff09; 3.进入menuconfig修改为M 4.编译内核 make modules 也许你会看到一个 .ko 文件 5.复制到根目录文件下 在板子…

4.11作业

服务器端 #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include<QTcpServer> //服务器端类 #include<QMessageBox> //消息对话框 #include<QTcpSocket> //客户端类 #include<QList> //链表容器QT_BEGIN_NAMESPACE namespace Ui { cla…

Pycharm远程连接服务器配置详解

背景&#xff1a; 相信很多人都遇到了这种情况&#xff0c;日常的开发和程序的验证都需要在linux环境下验证&#xff0c;而我们都是使用本地windows来进行开发或者脚本的编写&#xff0c;然后再push到远程仓库&#xff0c;再到linux环境下pull下来代码验证&#xff0c;这样每次…

CorelDRAW21.2.4中文最新官方和谐版下载

CorelDRAW是一款由加拿大Corel公司出品的平面设计软件&#xff0c;也被称为CDR。它是一款功能强大的矢量图形制作和排版软件&#xff0c;主要面向绘图设计师和印刷输出人员。该软件提供了矢量插图、页面布局、图片编辑和设计工具&#xff0c;广泛应用于排版印刷、矢量图形编辑及…

HWOD:密码强度等级

一、知识点 回车键的ASCII码是10 如果使用EOF&#xff0c;有些用例不通过 二、题目 1、描述 密码按如下规则进行计分&#xff0c;并根据不同的得分为密码进行安全等级划分。 一、密码长度: 5 分: 小于等于4 个字符 10 分: 5 到7 字符 25 分: 大于等于8 个字符 二、字母: 0…