编程之路,从0开始:结构体详解

news2024/11/19 15:53:39

目录

前言

正文

1、结构体引入

2、结构体的声明

3、typedef

4、结构体的匿名声明

5、结构的自引用

(1)链表

(2)自引用

6、结构体内存对齐

(1)对齐规则

(2)题目

(3)为什么存在内存对齐?

(4)默认对齐数

7、结构体实现位段

(1)什么是位段

(2)位段的跨平台问题

(3)位段的应用

总结


 

前言

        Hello大家好!很高兴我们又见面啦!给生活添点passion,开始今天的编程之路!

        今天我们来学习C语言中非常重要的一部分:结构体。


正文

1、结构体引入

        首先,什么是结构体?结构体是我们的自定义类型之一。我们在之前简单提到过结构体,这里我们再复习一下。我们在描述一个物品时,可能需要从多个角度进行描述,比方说描述一个人,可能由年龄,名字,性别,身高等等多个方面来描述。那么这时我们就可以声明一个结构体,用来存放描述这个学生的各种信息,然后再把值赋予这个结构体里的各种类型信息。

2、结构体的声明

        现在我们声明一个结构体:

struct stu 
{
    char name[20];
    char sex[10];
    int age;
};

        其中,stu是我们的标签名。

        现在我们给结构体中放上东西:

#include "stdio.h"
struct stu 
{
    char name[20];
    char sex[10];
    int age;
}s3;//全局变量
struct stu s4;//全局变量
int main()
{
    struct stu s1 = { "张三","男",20 };//创建局部变量
    struct stu s2 = { .name = "李四",.sex = "男",.age = 19 };
    printf("%s\n", s1.name);
    printf("%s\n", s1.sex);
    printf("%d\n", s1.age);
    printf("%s\n", s2.name);
    printf("%s\n", s2.sex);
    printf("%d\n", s2.age);
    return 0;
}

        结构体声明就相当于规定这个自定义类型里面包含什么数据,就比方说我们的int,他里面放的就是整型,这是C语言规定的。结构体也是一样,只不过是我们得通过声明自己规定里面有什么。

        这里我们怎么理解呢?我们可以把声明想象成一个学生的个人档案,里面有很多很多表格。这个表格填什么呢?这就需要声明来确定填入哪些数据。然后我们把这个档案起个名字,比方说是张三的档案,然后我们把档案交给张三,让他去填他的个人信息。那这个给档案起名的过程放到结构体中就是给结构体起个名字。

        现在我们在上面的代码介绍了三种给档案起名字的方法:其中有两个是定义全局变量,有一个是局部变量。

        填入数据的方式我们也介绍了两种,如以上代码所示。

        打印方法如以上代码所示,输出结果:

b72fb1b939c54cc09dd2fe90c6592a1f.png

3、typedef

        那现在我说你这太麻烦了,放东西的时候得写struct,再写个标签名,再写个名字,可读性太差是不是?

        这时候就用到我们的关键字typedef了:typedef可以用于为基本数据类型、结构体、联合体、枚举等复杂数据类型创建新的名称。

        当然了,联合体和枚举我们以后会讲,这里我们先用他给结构体重命名:

typedef struct stu
{
    char name[20];
    char sex[10];
    int age;
}student;//全局变量
struct stu s4;//全局变量
int main()
{
    student s1 = { "张三","男",20 };//创建局部变量
    student s2 = { .name = "李四",.sex = "男",.age = 19 };
    printf("%s\n", s1.name);
    printf("%s\n", s1.sex);
    printf("%d\n", s1.age);
    printf("%s\n", s2.name);
    printf("%s\n", s2.sex);
    printf("%d\n", s2.age);
    return 0;
}

 

        现在原代码所有的struct stu都可以用student来代替。当然了typedef还有可以以其他形式给别的数据类型起别名,这里由于我们主要是来讲结构体我们只介绍怎么给结构体起别名,这也是最主要的用法。 

4、结构体的匿名声明

        什么是匿名声明呢?就是我们在声明的时候不给他标签名。

#include "stdio.h"
#include<stdlib.h>
 struct 
{
    char name[20];
    char sex[10];
    int age;
}s1;//全局变量
int main()
{
    strcpy(s1.name,"张三");
    strcpy(s1.sex, "男");
    s1.age = 20;
    printf("%s\n", s1.name);
    printf("%s\n", s1.sex);
    printf("%d\n", s1.age);
    return 0;
}

        我们发现尽管不给他标签名,也可以正常使用。只是全局变量在没标签名在赋值有些不同寻常。

        但是这样有一个非常严重的问题,就是在我们的结构体不重命名的情况下只能使用一次,意思就是说我们只能定义s1这一个结构体变量!

比如以下代码是无法实现的:

#include "stdio.h"
#include<stdlib.h>
 struct 
{
    char name[20];
    char sex[10];
    int age;
}s1;//全局变量
int main()
{
    struct  s2 = { "sss","ddd",10 }; //报错,无法实现
   
    return 0;
}

        其次,这种匿名结构体不能实现如下代码:

#include "stdio.h"
#include<stdlib.h>
 struct 
{
    char name[20];
    char sex[10];
    int age;
} * s1;//全局变量
 struct 
 {
     char name[20];
     char sex[10];
     int age;
 } p;

int main()
{  
    s1 = &p;//报错  
    return 0;
}

        如果你这么想了,首先我得夸你聪明。我们把结构体声明为一个指针对象,在放入另一个结构体的地址不就好了吗(两个结构体对应的数据类型必须相同),但是如上代码无法打印,为什么?

我们看一看他打印的的错误信息是:

710e9f41e7e049f3ac774400231a5c28.png

        也就是说,我们系统仍然认为这两个结构体的类型是不同的!

        这里其实我们可以想一下,是不是struct这个东西根本就不能作为指针呢?那么我们是不是可以给这个结构体里的每一个元素,都对应的指向另一个结构体中的每一个元素,然后再打印呢?

        我把这个问题留给各位思考。

        实际上,匿名指针有什么优势呢?什么优势也没有!反而会带来很多的问题!所以我们尽量不要使用匿名指针。其实一些细节问题,也完全没必要扣的很细。

5、结构的自引用

        在结构中包含一个类型为该结构本身的成员是否可以呢?

(1)链表

        首先我们来介绍一下链表。

3f1230b5d6434403b478619d2be4a72e.png

        我们把一个字符串放到一个char类型数组里,我们打印的时候只需要把这个数组的首地址放到printf里,他就能顺着打印出来。这是因为我们的每个字母所占的内存块都是连着的!

        那么如果内存块没连着,该怎么办呢?这也很容易想到,我们只需要把每块内存手动连接起来不就好了吗?(如图下面的那种情况)。

        这时,每个块块我们叫他一个节点,那么我们怎么通过节点与节点之间进行连接呢?

        我在这里也不卖关子了,直接给大家写出来:

struct node
{
    char a;
    struct node* next;
};

        我们在填入数据的时候,把下一个节点的地址填入,那么我们访问的时候是不是就可以通过访问第一个节点来访问第二个节点了呢?

注:链表这部分在以后我们还会详细讲解。

(2)自引用

        像这种在结构中包含一个类型为该结构本身的成员就叫自引用。

        但需要注意的是,自引用无法通过匿名结构体实现!

6、结构体内存对齐

        这一部分可以说是很有意思哈~

        我们先想想,int 型占4个字节,char占一个字节,double占八个字节,那结构体占几个字节呢?我们声明的时候也没说它占几个字节啊?实际上,系统会自己计算它占几个字节,系统计算的规则就是我们的内存对齐规则。

(1)对齐规则

        内存对齐规则

  1. 结构体内的第一个变量的地址偏移量为0:结构体的第一个成员总是从偏移量为0的地址开始存储。

  2. 结构体内的其他变量的起始地址:其他变量的起始地址应为对齐数整数倍的地址处。对齐数等于编辑器默认对齐数(VS默认为8)与该变量大小的较小值。

  3. 结构体整体对齐:结构体的总大小应为结构体中最大对齐数的整数倍。

  4. 嵌套结构体的对齐:如果结构体中包含嵌套的结构体,则子结构体的最大数据类型作为子结构体的内存对齐标准。

        我们通过小题来理解一下

(2)题目

#include "stdio.h"
#include<stdlib.h>
struct s1
{
    char a;
    int i;
    char c;
};

int main()
{
    printf("%d", sizeof(struct s1));
    return 0;
}

        我们在不知道内存对齐之前可能以为他是占1+2+1=5个字节,但实际上,它占12个字节。为啥呢?首先根据对齐规则,我们把第一个char类型放在最开始。

        然后就是int型,但是int型根据对齐规则应该在对齐数的整数倍处开始,也就是第4个字节的位置开始存放。

        接着存放char型,占一个字节,理论来说9个字节就够了啊?但是根据对齐规则来说,结构体总大小是结构体中最大对齐数的整数倍,当然了,这个数应该比9大,在这里应该是12,他是最大对齐数4的倍数。

0d89fa63a2de4327a3800136f7633fb1.png

图片理解:

e2ff35ee353e44f0a4f663de81786d0c.png

        那么我们空白的地方就浪费掉了!我们想想,如果把两个char类型的数据放在前,int类型数据放在后面,是不是能少浪费一些内存块呢?

代码:

#include "stdio.h"
#include<stdlib.h>
struct s1
{
    char a;
    char c;
    int i;
};

int main()
{
    printf("%d", sizeof(struct s1));
    return 0;
}

输出:

dfe65d9f017546cc80ab877328da27df.png

图片理解:

810b2baa7ef04e37951baf102d2989e9.png

下面我们来看这道题:

#include "stdio.h"
#include<stdlib.h>
struct s2
{
    double i;
    char z;
    int w;
};
struct s1
{
    char a;//1
    struct s2 s;//16
    double i;//8
};

int main()
{
    printf("%d", sizeof(struct s1));
    return 0;
}

        先分析一下,s2占用16个字节,然后再看s1,a占一个字节,由于struct s2 s的字节数为16大于默认对齐数8,所以在这struct s2 s的对齐数为8,他从第八个字节开始放入。接着再放8个字节的double类型。输出结果为32:

a7d4444b54f04d16a55eaaa941372e99.png

图片理解:

e9785a5eb929439c99b9f5952cec68b5.png

(3)为什么存在内存对齐?

        比方说现在一个处理器每次读取四个字节,倘若没有内存对齐,那下图这种情况读取的时候就要复杂一些

a7ba6dc9c68841f5bedccbcf0341838d.png

        我们第一次读取四个字节,只能读char和四分之三个int类型,第二次读取才能读完int类型。也就是说当我们只读int时需要读取两次!这就无疑给系统增加了计算时间和计算复杂度。这就是内存对齐存在的原因。

(4)默认对齐数

        我们可以通过#pragma修改默认对齐数:

#include "stdio.h"
#pragma pack(1)
struct s2
{
    double i;
    char z;
    int w;
};
struct s1
{
    char a;//1
    struct s2 s;//16
    double i;//8
};

int main()
{
    printf("%d", sizeof(struct s1));
    return 0;
}

7、结构体实现位段

(1)什么是位段

       其实就是修改结构体成员所占内存大小。

       但需要注意的是,位段的成员必须是int, unsigned int ,signed int。

       位段成员名后边有一个冒号和数字。

#include "stdio.h"
struct A
{
    int a : 2;
    int b : 5;
    int c : 10;
    int d : 30;
};
int main()
{
    struct A a = { 0 };
    a.a = 10;
    a.b = 20;
    a.c = 5;
    a.d = 6;
    printf("%d", sizeof(struct A));
    return 0;
}

        我们乍一看,2+5+10+30=47,那不应该是六个字节吗?这是怎么回事?

        但实际上,位段的空间上是按照需要以4个字节( int )或者1个字节( char )的⽅式来开辟的,也就是说,4个字节32bit,存不下47个,那么就需要64个bit,也就是8字节才能存下。换句话说,字节数一定是4的整数倍,因为我们这里a b c d都是int类型的。

        那我们具体到底是怎么开辟的呢?到底是怎么放入数据的呢?别急,我们一点点来看

69643bf867ae442999fc24abc8e26f1f.png

 

       现在我把上面代码开辟的内存拿出来了(我把后面四个字节省略了),首先开辟的原则是从右往左开辟,我们依次开辟了2 5 10个比特。剩下的30比特由于装不下了,所以放在了后面的四个字节里。

       现在我们存入数据,第一个位置存入10,10的二进制补码是1010,现在问题来了,你第一个位置就两位啊,这怎么存?就两位那就只能存两位了,我们存入的是后边的10。同理,剩下的数据也是如此存入。

       也就是说,如果我们开辟空间不当的话,数据有可能会出现错误!所以说位段使用的时候还得多加小心啊!

(2)位段的跨平台问题

       位段固然可以节省一些空间,比如在结构中,由于对齐规则,我们不可能让a,b,c这三个空间放到一个int中挨着存放数据。

       但是位段其实存在一些问题:

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

2、位段中最大位的数目不能确定。

3、位段中的成员在内存中从左向右分配还是从右向左分配,标准尚未定义。当然在vs2022中是从右向左分配。

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

也就是说,位段的使用可以节省空间,但是有跨平台的问题出现。有可能我们换个编译器就达不到我们想要的结果了。

(3)位段的应用

        1、处理位操作‌:位段使得对位的操作更加方便和高效。例如,在操作系统和TCP/IP协议中,位段可以用来处理各种标志位和状态位,使得代码更加简洁和高效‌。

        2、定义复杂的数据结构‌:位段可以用来定义复杂的数据结构,特别是那些需要精确控制存储空间的数据结构。例如,在嵌入式系统中,常常需要精确控制硬件寄存器的每一位,位段提供了这种能力‌。

        3、优化存储‌:在需要存储大量标志或状态信息时,使用位段可以大大减少所需的存储空间。例如,在处理大量开关状态时,每个开关只需要一位,使用位段可以极大地节省空间‌。


总结

       大家也发现了这一篇很长,这是因为结构体这一部分很重要。他和指针一样,对于今后的学习有很大的影响,所以希望大家能够掌握。

       好了,今天的内容就分享到这。制作不易,希望各位老铁三连一波支持一下,我会持续更新c/c++/算法相关知识!期待我们的下次见面。

 

 

 

 

 

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

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

相关文章

01_MinIO部署(Windows单节点部署/Docker化部署)

单节点-Windows环境安装部署 在Windows环境安装MinIO&#xff0c;主要包含两个东西&#xff1a; MinIO Server&#xff08;minio.exe&#xff09;&#xff1a;应用服务本身MinIO Client&#xff08;mc.exe&#xff09;&#xff1a;MinIO客户端工具&#xff08;mc&#xff09;…

qt5半成品飞机大战小游戏

最近在学Qt&#xff0c;心血来潮做了个飞机大战小游戏&#xff0c;由于一些资源比较难找&#xff0c;就做了个半成品。效果图如下&#xff1a; 目前已做功能&#xff1a;人物飞机的自由移动&#xff0c;子弹的发射&#xff0c;子弹与敌机的物体碰撞,碰撞特效。 缺少功能&#x…

html 图片转svg 并使用svg路径来裁剪html元素

1.png转svg 工具地址: Vectorizer – 免费图像矢量化 打开svg图片,复制其中的path中的d标签的路径 查看生成的svg路径是否正确 在线SVG路径预览工具 - UU在线工具 2.在html中使用svg路径 <svg xmlns"http://www.w3.org/2000/svg" width"318px" height…

Android OpenGL ES详解——几何着色器

目录 一、概念 1、图元 2、几何着色器 1、输入类型 2、输出类型 3、输出顶点数量最大值限制 二、使用几何着色器 三、应用举例——造几个房子 四、应用举例——爆破物体 1、获取法向量 2、显示法线 五、应用举例——细分三角形 六、应用举例——广告牌技术 一、概…

基因组之全局互作热图可视化

引言 PlotHiC 是一个专为 Hi-C 数据可视化分析而设计的 Python 包。Hi-C 技术是一种能够检测染色体三维结构的实验方法&#xff0c;它能揭示 DNA 在细胞核内的三维组织结构。为了更好地展示和解释这些复杂的数据&#xff0c;PlotHiC[1] 可以帮助用户方便地绘制Hi-C 数据的热图。…

JVM详解:类的加载过程

JVM中类的加载主要分为三个部分&#xff0c;分别为加载&#xff08;loading&#xff09;&#xff0c;链接&#xff08;linking&#xff09;&#xff0c;初始化&#xff08;initing&#xff09;。其中加载负责的主要是讲类文件加载到内存中变为类对象&#xff0c;不过此时只有基…

FPGA开发流程

注&#xff1a;开发板&#xff1a;小梅哥的ACX720。本实验可直接运行在小梅哥的ACX720开发板上&#xff0c;后续的实验都可直接运行在小梅哥的ACX720上。 一、打开VIVADO并创建工程 1、双击VIVADO图标&#xff0c;打开vivado。 2、打开vivado界面打&#xff0c;点击有 Create …

免费开源!DBdoctor推出开源版系统诊断工具systool

​前言 在开发和运维过程中&#xff0c;经常会遇到难以定位的应用问题&#xff0c;我们通常需要借助Linux系统资源监控工具来辅助诊断。然而&#xff0c;系统的IO、网络、CPU使用率以及文件句柄等信息通常需要通过多个独立的命令工具来获取。在没有部署如Prometheus这样的综合…

Restful API接⼝简介及为什么要进⾏接⼝压测

一、RESTful API简介 在现代Web开发中&#xff0c;RESTful API已经成为一种标准的设计模式&#xff0c;用于构建和交互网络应用程序。本文将详细介绍RESTful API的基本概念、特点以及如何使用它来设计高效的API接口。 1. 基于协议 HTTP 或 HTTPS RESTful API通常使用HTTP&am…

R语言统计分析与MATLAB数学建模书籍推荐

文章目录 一、《R语言统计分析与可视化》1.1 内容核心1.2 内容简介 二、《MATLAB数学建模从入门到精通》2.1 关键点2.2 内容简介2.3 作者简介 一、《R语言统计分析与可视化》 R语言统计分析与可视化从入门到精通。学R语言、练语法、取数据、预处理、可视化、回归分析、方差分析…

智慧社区平台系统提升物业管理效率与居民生活质量

内容概要 智慧社区平台系统是为应对现代城市管理挑战而诞生的重要工具。随着城市化进程的加快&#xff0c;传统的物业管理方式已经难以满足日益增长的居民需求和管理复杂性。因此&#xff0c;引入智能化管理手段显得尤为重要。这个系统不仅仅是一个简单的软件&#xff0c;它是…

【ASR技术】WhisperX安装使用

介绍 WhisperX 是一个开源的自动语音识别&#xff08;ASR&#xff09;项目&#xff0c;由 m-bain 开发。该项目基于 OpenAI 的 Whisper 模型&#xff0c;通过引入批量推理、强制音素对齐和语音活动检测等技术。提供快速自动语音识别&#xff08;large-v2 为 70 倍实时&#xf…

STM32CUBEIDE FreeRTOS操作教程(九):eventgroup事件标志组

STM32CUBEIDE FreeRTOS操作教程&#xff08;九&#xff09;&#xff1a;eventgroup事件标志组 STM32CUBE开发环境集成了STM32 HAL库进行FreeRTOS配置和开发的组件&#xff0c;不需要用户自己进行FreeRTOS的移植。这里介绍最简化的用户操作类应用教程。以STM32F401RCT6开发板为…

力扣(leetcode)题目总结——动态规划篇

leetcode 经典题分类 链表数组字符串哈希表二分法双指针滑动窗口递归/回溯动态规划二叉树辅助栈 本系列专栏&#xff1a;点击进入 leetcode题目分类 关注走一波 前言&#xff1a;本系列文章初衷是为了按类别整理出力扣&#xff08;leetcode&#xff09;最经典题目&#xff0c…

计算器的实现

计算器的实现 计算器实现思路 我们⽇常写的计算表达式都是中缀表达式&#xff0c;也就是运算符在中间&#xff0c;运算数在两边&#xff0c;但是直接读取⽆ 法⻢上进⾏运算因为⼀个计算表达式还涉及运算符优先级问题。如&#xff1a; 都⽆法运算&#xff0c;因为后⾯还有括号优…

Python蓝桥杯刷题1

1.确定字符串是否包含唯一字符 题解&#xff1a;调用count函数计算每一个字符出现的次数&#xff0c;如果不等于1就输出no&#xff0c;并且结束循环&#xff0c;如果等于1就一直循环直到计算到最后一个字符&#xff0c;若最后一个字符也满足条件&#xff0c;则输出yes import…

《基于 PySpark 的电影推荐系统分析及问题解决》

以下是一篇关于上述代码的博客文章&#xff1a; 基于PySpark的电影推荐系统实现与分析 在当今数字化时代&#xff0c;个性化推荐系统在各个领域中都发挥着至关重要的作用&#xff0c;尤其是在娱乐行业&#xff0c;如电影推荐。本文将详细介绍如何使用PySpark构建一个简单的电…

每天五分钟深度学习pytorch:批归一化全连接网络完成手写字体识别

本文重点 前面我们学习了普通的全连接神经网络,后面我们学习了带有激活层的全连接神经网络,本文我们继续进一步升级,我们学习带有批归一化的全连接神经网络,批归一化可以加快神经网络的训练速度,减少过拟合,具体它的原理,大家可以看我们的《每天五分钟深度学习》专栏,…

JavaWeb后端开发知识储备1

目录 1.DTO/VO/PO 2.MVC架构/微服务架构 3.JWT令牌流程 4.ThreadLocal 5.接口路径/路径参数 6.自定义注解 1.DTO/VO/PO 1.1 DTO DTO 即 Data Transfer Object—— 数据传输对象&#xff0c;是用于传输数据的对象&#xff0c;通常在服务层与表现层之间传递数据&#xff…

什么是SMARC?模块电脑(核心板)规范标准简介三

1. 概念 SMARC&#xff08;Smart Mobility ARChitecture&#xff0c;智能移动架构&#xff09;是一种通用的小型计算机模块定义&#xff0c;基于ARM和X86技术的模块化计算机低功耗嵌入式架构平台&#xff0c;旨在满足低功耗、低成本和高性能的应用需求。这些模块通常使用与平板…