当前位置: 首页 > article >正文

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

目录

前言

正文

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.kler.cn/a/399763.html

相关文章:

  • 大数据技术Kafka详解 ① | 消息队列(Messages Queue)
  • 微信小程序底部button,小米手机偶现布局错误的bug
  • etcd部署(基于v3.5.15)
  • C++标准模板库 -- map和set
  • 怎么看待Ai发展前景?
  • 用python中的tkinter包实现进度条
  • 【C++】C++11 新特性揭秘:序章
  • window的Anaconda Powershell Prompt 里使用linux 命令
  • AIGC----生成对抗网络(GAN)如何推动AIGC的发展
  • 11.12.2024刷华为OD-集合的使用,递归回溯的使用
  • 【青牛科技】D2030 14W 高保真音频放大电路介绍和应用
  • 使用Mac下载MySQL修改密码
  • 【C++】了解map和set及平衡二叉树和红黑树的原理
  • 使用docker快速部署Nginx、Redis、MySQL、Tomcat以及制作镜像
  • ArcGIS Pro的arpx项目在ArcGIS Server中发布要素服务(FeatureServer)
  • ue中使用webui有效果白色拖动条 有白边
  • 【C++】构造与析构函数
  • 网络安全之国际主流网络安全架构模型
  • 已有docker增加端口号,不用重新创建Docker
  • 删除k8s 或者docker运行失败的脚本