关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解

写在前面

大家好呀,我是不一样的烟火a,一个专注于C/C++和算法领域的博主,以后还会持续更新C/C++和算法相关的博客,欢迎大家关注,让我们一起努力进步吧!

目录

写在前面

前言

⚔️结构体的声明

结构的基础知识

结构应该怎么声明

‍♂️结构体变量的定义和初始化

特殊的声明

结构的自引用

结构体内存对齐

为什么存在内存对齐?

修改默认对齐数

结构体传参

位段

什么是位段

位段的内存分配

位段的跨平台问题

总结


前言

今天我要给大家讲的是C语言结构体,由于这篇博客将会非常详细的讲述结构体内容,附带的图解也十分的多,所以建议大家先收藏再观看,让我带大家一起拿捏C语言结构体,要是大家觉得有用的话记得别忘了三连加关注哟!


⚔️结构体的声明

结构的基础知识

结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。

结构应该怎么声明

我们就是按照下面这样的形式对结构体进行声明的:

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第1张图片

 例如描述一个学生:

struct Stu
{
 char name[20];//名字
 int age;//年龄
 char sex[5];//性别
 char id[20];//学号
} stu1, stu2, stu3; //注意这里的分号不能丢

这里的Stu就是一个标签(表示这个结构体是在描述一个学生),这个标签是一个有意义的名字(当然这里的Stu也就是Student的简写,表示学生),这个标签就标示着这个结构体在干什么(上面这个结构体就是在描述一个学生)。而{ }里面的类容就是结构体的成员变量,成员变量就是这个正在描述的事物所含的内容(就像上面描述的学生,他的成员变量就是学生的名字、年龄、性别、学号),而这些成员变量都是属于成员列表里面的内容。而为了描述不同学生的信息,我们可以声明多个结构体变量来存储不同学生的信息,上面的stu1、stu2、stu3就是三个结构体变量,可以用来存储不同学生的信息,它们就属于变量列表里面的类容。(注意:结构体声明的时候是不用将结构体成员变量初始化的,例如学生结构体的声明就只用定义学生名字需要什么类型多大的数组来存储,用什么类型来存储年龄,和需要什么类型多大的数组来存储学号等。)

当然上面的结构体变量(stu1、sut2、stu3)也可以不在结构体声明的时候创建,你什么时候需要一个这样的结构体变量来存储数据,你就什么时候创建。例如:

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第2张图片

注意:这里创建这个学生的结构体变量,是要将struct Stu全写出来,不能只写一个Stu来表示这个结构体,struct Stu才是结构体类型(也就是学生的结构体类型)。当但要是觉得这样创建一个结构体变量太麻烦了也可以用下面方法简化一下,更方便使用。(也就是用typedef将该结构体类型重定义/重命名一下)

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第3张图片

这里我们就用typedef将struct Stu重命名成了Stu,当然这里用了typedef重命名了后,在{}后面就不能直接创建结构体变量了,因为这后面是类型的重命名,创建结构体变量应该像上面这样创建。

‍♂️结构体变量的定义和初始化

下面是结构体的定义和初始化的多种方法:

struct Point
{
    int x;
    int y;
}p1; //声明类型的同时定义变量p1

struct Point p2; //定义结构体变量p2

//初始化:定义变量的同时赋初值。
struct Point p3 = { 3, 6 };

struct Stu        //类型声明
{
    char name[15];//名字
    int age;      //年龄
};

struct Stu s = { "zhangsan", 20 };//初始化

//后面会重点讲下面这种自身嵌套的结构体
struct Node
{
    int data;
    struct Point p;
    struct Node* next;
}n1 = { 10, {4,5}, NULL }; //结构体嵌套初始化

struct Node n2 = { 20, {5, 6}, NULL };//结构体嵌套初始化

特殊的声明

在声明结构的时候,可以不完全的声明。

例如:匿名结构体(省略了结构体的名字/标签),我们来看看下面这两个结构体,下面这两个类型的结构体就是匿名结构体。

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第4张图片

 相信细心的同学一定会发现,这两个结构体的成员列表是一样的,而且都是匿名结构体,但是为什么我上面要说这是两个不同类型的结构体呢?

接下来我们来看看编译器运行起来是怎么说的。

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第5张图片

 编译器运行后出现警告,说类型不兼容。这是为什么呢?这两个结构体的成员列表明明是一样的,都是匿名结构体,但是为什么编译器却说它们是不相同的类型呢?原来这是C语言规定的,C语言规定可以存在这样的匿名结构体,但是每个匿名结构体的类型是不同的,虽然你表面上看它们好像是两个一样的类型,但是编译器会把它们当成两个不同类型的结构体(你可以认为每个匿名结构体内部都隐藏着一个自己专属的代号来代替自己的名字,如果有两个匿名结构体的成员列表是一样的,这时你就会意识到这两个匿名结构体的代号其实是不同的,所以它们是两个不同类型的结构体)。所以不能像上面那样用p来存储x的地址。

除此之外用匿名结构体还有一个易错点,虽然有名结构体也可能犯这个错误,但是相对匿名结构体较少一点

例如:

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第6张图片

 关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第7张图片

这里编译器报错,相信厉害的小伙伴已经知道为什么错误了,因为在声明匿名结构体的时候一定要在结尾加一个结构体变量,这样以后才能用这个匿名结构体,而在结尾加了结构体变量后也就定义了这个匿名结构体变量,如果在定义一个结构体变量的时候没有用 = 号将它所有的成员进行赋值的话,后面就不能整体一起赋值了,只能一个成员一个成员的赋值,就像数组那样。而有名结构体则可以在后面想用这个结构体的时候再定义一个这种类型的结构体变量,然后将结构体成员整体初始化。如果你坚持要将匿名结构体的成员一起初始化的话就只能像下面这样初始化:

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第8张图片

️运行结果:

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第9张图片

像这样不就行了吗,只不过看起来有点不美观。 

那么既然匿名结构体这么拉跨,要它有何用?下面我们来讲讲它的优点。

优点:有名结构体因为有名称,所以每次创建有名结构体时名称都不能相同 ,也就是不能重名。而匿名结构体随便创建多少个、管它成员列表相不相同,它都不会出现重合、重命名的情况(但是在声明匿名结构体的时候要记得在结构体末尾填上结构体变量,因为要是这次没有填上结构体变量的话,后面就找不到它了。),因为它压根就没有名字(匿名结构体:我就是这么强)。

 例如下面这个代码:

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第10张图片关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第11张图片

 相信大家也看出来了,这上面两个结构体类型是一样的,它们重命名了,所以编译器说它们类型重定义了,这种情况在头脑发热的时候还是有可能出现的,所以这时候匿名结构体就出来了,大家看下面这个代码:

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第12张图片

 我们用匿名结构体来写刚刚这个代码,编译器就不会报错了,就问你匿名结构体强不强。

如果你认为匿名结构体就这么一点优点就大错特错了,刚刚只是个开胃菜,接下来匿名结构体要放大招了

C语言规定,可以在结构体中声明某个结构体而不用指出它的名字,如此之后就可以像使用结构体成员一样直接使用其中结构体的成员,我们这里说的不指出名字的结构体就是匿名结构体。看完下面两段代码的对比你就知道匿名结构体多强了:

我们分别用匿名结构体有名结构体来实现嵌套结构体

用匿名结构体

 关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第13张图片

️运行结果:

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第14张图片

 不用匿名结构体:

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第15张图片

 ️运行结果:

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第16张图片

 我们发现上面两种方法写的结果都是一样的,但是使用匿名结构体,结构体对象 jim 可以通过 jim.area_code 直接访问匿名结构体成员变量 area_code,代码相对比较简洁,不使用匿名结构体就必须通过 jim.office.area_code 来访问结构体成员变量 。

看完上面的代码是不是觉得匿名结构体真香,嘿嘿,但是在使用匿名结构体的时候也要看情况的,怎么写代码更简洁更巧妙就怎么来,有时候用匿名结构体是真的能简化代码的,就像上面这种情况。

结构的自引用

我们知道数据结构里面有顺序表和链表

顺序表就是把数据按顺序一个接着一个连续的存储在内存一个位置,就像数组一样。

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第17张图片

链表就是把每个数据任意存储在内存的不同位置,然后用一根根链条将它们有序的连接起来。

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第18张图片

 而我们的结构体自引用就是像链表一样把数据存储起来。

下面就是结构体的自引用(或者说是结构体自身嵌套)

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第19张图片

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第20张图片

上面这个就是结构体的自引用,虽然思路是对的,但是这种声明是错误的,因为这种声明实际上是一个无限循环,成员Next是一个结构体,Next的内部还会有成员是结构体,依次下去,无线循环。在分配内存的时候,由于无限嵌套,也无法确定这个结构体的长度,所以这种方式是非法的。

所以正确的结构体自引用的方法应该是用结构体指针,我们将结构体里面放一个相同类型的结构体指针,这样我们就可以实现结构体的自引用了。当我们想要结束结构体自引用的时候,我们只用将这个指针设为空指就可以了,

下面是代码和图解:

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第21张图片

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第22张图片

我们像上面这样在每个位置处都存储一个需要存储的数据和一个地址,我们根据这个地址就能找到下一个数据存储的位置,直到我们找到NULL我们就结束了。 

上面我们就是使用结构体指针来实现的结构体自引用,为什么这样写编译器就不会报错了呢?因为指针的长度是确定的(在32位机器上指针长度为4),所以编译器能够确定该结构体的长度。

具体的结构体自引用的使用我们可以像下面这样:

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第23张图片

 ️运行结果:

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第24张图片

这里只是举个例子,实际使用时要根据情况而定。

结构体内存对齐

大家是不是在刚学C语言的时候发现每个类型都有大小,例如整形大小是4字节,字符型是1字节,指针大小是4字节等(在32位平台下)。而一个结构体的体内有很多不同类型的成员,那么这个结构体的大小是多少呢?下面我们就来研究研究。

我们来看看下面这个结构体,大家可以试图猜一猜它的大小是多少。

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第25张图片

️运行结果:

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第26张图片

我们看见代码运行结果是8,也就是说这个结构体的大小是8,而且刚好这个结构体的成员是由两个整形组成的,而且一个整形的大小是4,两个整形大小就是8了,刚好大小就是8。大家是不是就会认为一个结构体的大小就是所有成员变量的大小加起来总共的大小呀。

如果你是这么认为的,那我们就再来看看下面这个结构体。

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第27张图片

 ️运行结果:

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第28张图片

我们将刚刚的结构体体内的一个整形的成员换成了一个字符型的成员,我们发现这个结构体的大小还是8字节,如果是像你刚刚猜想那样,这里应该是5字节呀,为什么还是8字节呢?这就是因为结构体的内存存在对齐,这就使得结构体的大小和你猜想的并不一样。所以我们下面就来看看结构体内存的对齐。

结构体的对齐规则:

(1)第一个成员在与结构体变量偏移量为0的地址处。

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

       注:对齐数 = 编译器默认的一个对齐数 与 该成员的大小  它们二者中的较小值。

   例如:如果该成员大小 < 编译器默认的一个对齐数,则 对齐数 = 该成员大小

              如果编译器默认的一个对齐数 < 该成员大小,则 对齐数 = 编译器默认的一个对齐数

                VS中默认对齐数的值为8

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

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

说完结构体对齐的规则,那接下来我们就来具体看一下结构体是怎么对齐的。

我们就拿下面这个结构体来分析:

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第29张图片

️运行结果:

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第30张图片

我们看见这个结构体的大小是24,这个结构体的大小是怎么来的呢,我们看看下面这个图解就懂了。

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第31张图片

 我们根据上面的规定来看:

(1)第一个成员在与结构体变量偏移量为0的地址处。

也就是图中的第一个成员a,每个结构体中第一个成员都是从偏移量为0的位置开始分配空间,不管它的偏移量是多少,它都是从a是char类型占一个字节,所以分配一个空间。

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

图中的第二个成员是b,b是int类型大小为4字节,根据上面的公式:(对齐数 = 编译器默认的一个对齐数该成员的大小  它们二者中的较小值)。因为VS中默认对齐数的值为8,而b这个成员的大小是4,它小于编译器默认的一个对齐数 所以b的对齐数就是4,所以b就要从相对于起始位置的偏移量为4的倍数(也就是在相对于起始位置偏移量为4、8、16、20等这些是4的倍数的位置开始给a分配空间)的地方开始给b分配空间。

第三个成员是c,c是short类型大小为2字节,我们第二个成员结束分配空间的位置是相对于起始位置偏移量为7处,下一个位置就偏移量为8的位置,8刚好是2的倍数,所以c可以从这里开始分配空间。

第四个成员是d,d是double类型大小为8字节,我们第三个成员结束分配空间的位置是相对于起始位置偏移量为9处,下一个位置就偏移量为10的位置,10不是8的倍数,所以这里不能给d分配空间,我们必须找到偏移量为8的倍数的位置才能给d分配空间。所以我们一直向后找,一直走到偏移量为16的位置才能给d开始分配空间,因为16是8的倍数。

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

我们上面这个结构体所有成员中d的对齐数是最大的,d是double类型的大小为8字节,所以结构体的总大小应该是8的整数倍,我们根据上面的图可以看到,我们最后分配空间的位置是偏移量为23的位置,从偏移量为0到偏移量为23的位置,结构体大小总共占了24个字节,24为8的倍数,所以可以用24作为结构体最终的大小。如果说我们在给刚刚这个结构体的所有成员对齐分配空间后,结构体的大小占25个字节,因为25不是8的倍数,所有这个时候我们就需要给这个结构体的末尾再分配7个字节的空间,让这个结构体的总大小为32,以32作为这个结构体的最终大小,因为32才是8的倍数。这里说的是假设情况哈,上面这个结构体最终大小还是24,不要看错了哦

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

下面是嵌套结构体的情况:

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第32张图片

 ️运行结果:

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第33张图片

我们在上面已经详细讲解了一个结构体的大小怎么求了,这里就不说第一个结构体大小怎么求了,我们直接开始将嵌套了一个结构体的结构体的大小怎么求。

下面为图解:

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第34张图片

 上面说嵌套的结构体对齐到自己的最大对齐数的整数倍处(上面这个嵌套的结构体就是Example1,它的最大对齐数是自己成员d的对齐数,因为d是int类型,是这个结构体中大小最大的,所有它的对齐数是4)

结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。这个结构体(Example2)的成员中最大的对齐数是4,因为这个结构体(Example2)中成员类型大小最大的类型是int类型),由于我们将这个结构体对齐分配空间后这个结构体占14个字节,14不是4的倍数,所以结构体末尾要多添加2个字节的空间,让结构体总共的大小为16,因为16才是4的倍数。

为什么存在内存对齐?

️存在内存对齐的原因:

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

2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。(由于我们32位机器读取内存里面放的数据是一次读取4字节的,所以编译器每次读取数据的位置要么是0偏移量的位置,要么就是偏移量为4的倍数的位置,注意:编译器读取一个数据时是不能从任意位置读取的,只能是从固定的位置开始读取数据,也就是刚刚说的0偏移量的位置和偏移量为4的倍数的位置开始读取你需要的那个数据,这就使得内存对齐很重要了,下面就是对齐和不对齐存放数据的区别)

我们就拿这个结构体举例子:

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第35张图片

不考虑对齐的情况:

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第36张图片

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第37张图片

 考虑对齐的情况:关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第38张图片

 关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第39张图片

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

注意:在vs编译器中默认对齐数是8。在Linux下没有默认对齐数,它的对齐数就是每个成员变量的类型的大小。在不同的编译器下默认的对齐数可能是不同的。

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

我们来看看下面这两个结构体:

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第40张图片

 运行结果:

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第41张图片

在这里我们发现,上面的两个结构体体内的成员变量的类型是一样的,我们仅仅只是交换了一下它们的顺序,使得第二个结构体的大小比第一个结构体小了4字节,从而节省了4字节的空间。

下面是图解:

s1:关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第42张图片

由于s1这个结构体将所有成员对齐分配空间后,这个结构体目前的大小为9,9不是最大对齐数4的倍数,不能作为结构体最终的大小,所以只能在结构体末尾再多添加3字节的空间,使得结构体的大小为12,12才是4的倍数,所以结构体最终的大小为12。

s2:关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第43张图片当s2这个结构体所有成员都对齐分配完空间后,结构体目前的大小为8,8是最大对齐数4的倍数,所以8可以作为结构体的最终大小。

经过调换结构体成员的顺序,我们发现可以使结构体的大小发生变化,所以在声明一个结构体的时候,让占用空间小的成员尽量集中在一起,这样就能够有效的节约空间。

修改默认对齐数

我们用#pragma就可以修改我们的默认对齐数,下面就是修改默认对齐数的方法:

#pragma pack(n)//设置默认对齐数为n

#pragma pack()//取消设置的默认对齐数,还原为默认

我们下面用具体例子来介绍对齐数的修改:

修改前:

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第44张图片

 ️运行结果:

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第45张图片

 修改后:

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第46张图片

️运行结果:

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第47张图片

 上面我们看到,我们将默认对齐数修改后,结构体的大小就变成了12,下面为图解:

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第48张图片

由于我们把这个结构体的默认对齐数改成了4,所以成员变量d的对齐数就变成了4,因为d的类型是double类型大小为8,对齐数 = 编译器默认的一个对齐数 与 该成员的大小  它们二者中的较小值,默认对齐数4比成员d的大小8要小,所以d的对齐数是4。这个结构体最大的对齐数也就是4,所以最后结构体的大小应该是4的倍数。

 注意:在修改结构体的对齐数后需要将对齐数恢复到默认,要不然在这个结构体之后的所有结构体的对齐数都将是你现在修改的这个对齐数。

下面是一些例子,大家下去可以自己尝试算一算大小是多少。

#include 

#pragma pack(8)//设置默认对齐数为8
struct S1
{
    char c1;
    int i;
    char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认

#pragma pack(1)//设置默认对齐数为1
struct S2
{
    char c1;
    int i;
    char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认

int main()
{
    //输出的结果是什么?
    printf("%d\n", sizeof(struct S1));
    printf("%d\n", sizeof(struct S2));

    return 0;
}

结论:结构在对齐方式不合适的时候,我么可以自己更改默认对齐数。

结构体传参

话不多说,我们这里直接上代码:

struct S
{
 int data[1000];
 int num;
};

struct S s = {{1,2,3,4}, 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;
}

上面的 print1 和 print2 函数哪个好些?

答案是:首选print2函数

原因:函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的 下降。

位段

我们结构体讲完就要讲讲结构体实现位段的能力了。

什么是位段

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

1.位段的成员必须是整形家族( int、unsigned int 或signed int 或char(char也属于整形家族,具体原因可以看我另外一篇博客——C语言数据的存储))。(double、float等这些类型就不能作为位段成员)

2.位段的成员名后边有一个冒号和一个数字(冒号后面这个数就代表这个成员变量占多少个比特位,记住这是比特位不是字节)。

3.注意:冒号后面这个数代表这个位段成员变量占多少个比特位,我们在给这个位段成员赋值的时候就只能赋这个大小的值,超过位段成员大小的部分将会截断,然后再将这个截断后的值赋给这个位段成员变量。

例如:

#include

struct A
{
    int _a : 2;
    int _b : 5;
    int _c : 10;
    int _d : 30;
};

int main()
{
    printf("%d\n", sizeof(struct A));

    return 0;
}

A就是一个位段类型。 那位段A的大小是多少呢?

我们看看运行结果:

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第49张图片

运行后我们发现这个位段的大小是8字节,我们看完下面的位段内存分配后就知道这个位段的大小是多少了,这里先不急。

位段的内存分配

1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型

2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。

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

4.如果位段成员既有int类型又有char类型,那么开辟空间就按大小最大的类型来开辟空间,也就是说一次开辟一个int类型的大小,也就是4个字节。

5.每次开辟空间都只开辟4个字节或1个字节的空间,等将第一个开辟的4字节空间或1字节空间用完后再开辟下一个4字节空间或1字节空间。所以位段的大小永远是4的倍数(位段成员大小最大的是int类型)或1的倍数(位段成员大小最大的是char类型)。

我们在来看看下面这个位段:

#include

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

int main()
{
    struct S s = { 0 };
    s.a = 10;//二进制为00001010
    s.b = 12;//二进制为00001100
    s.c = 3; //二进制为00000011
    s.d = 4; //二进制为00000100
    printf("%d\n", sizeof(struct S));
    printf("%d  %d  %d  %d\n", s.a, s.b, s.c, s.d);

    return 0;
}

️ 运行结果:

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第50张图片

 为什么运行结果和我们赋值的类容不一样呢?,而且这个位段的大小为什么是3呢?,下面我们来看看图解。

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第51张图片

 下面是调试结果:

关于我终于拿捏了C语言结构体那点事 ——超详细的C语言结构体讲解_第52张图片

 我们调试后发现,这个位段里面存储的数据就跟我们上面图解判断的一样。

但是我们当我们将存储在这些位段成员里面的数据拿出来的时候,有些却不是我们存储进去的那个数字,这是为什么呢?

我们就来分析一下s.a和s.b,因为存在这两个位段成员变量里面的数据和拿出来的不一样。

s.a:存进去的是10,二进制为00001010,因为s.a这个位段成员只能存储3比特位的数据,所以只能存进去010,前面所有内容都要被截断。当我们要把010拿处来的时候,因为s.a这是个字符型的变量(大小为8字节),所以010前面要补上5个0,为什么要补5个0呢,因为编译器会把010当成一个正的数字,所以前面所有位都要补符号位0。所以我们拿出来的数据就是00000010,将这个数用整形的方式打印出来就是2。

s.b:存进去的是12,二进制为00001100,因为s.b这个位段成员只能存储4比特位的数据,所以只能存进去1100,前面所有内容都要被截断。当我们要把1100拿处来的时候,因为s.b这是个字符型的变量(大小为8字节),所以1100前面要补上5个1,为什么要补5个1呢,因为编译器会把1100当成一个负的数字,因为1100中第一个数字是1,第一个数字代表符号,所以前面所有位都要补符号位1。所以我们拿出来的数据是11111100,这是补码,我们将11111100转换成原码为10000100,10000100这个数转化为十进制就是-4,所以我们用整形的方式将s.b打印出来就是-4。

我们了看完了位段的内存分布介绍后,我们就知道上面的第一个位段的大小为什么是8字节了,我们通过计算,得到所有位段成员加起来的总共大小为47个比特位,一个字节是32个比特位,不够存储47个比特位,因此我们还要再给这个位段分配一个字节的空间,所以上面那个位段的大小为8字节。

位段的跨平台问题

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

2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。

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

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

我们刚刚上面介绍的位段都是在vs编译器下实现的,其他编译器对位段的定义还不知道,所以最好是不要跨平台用位段。

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


总结

这就是C语言结构体的全部内容了,感谢各位老铁能看到最后,最后给大家送上我最真挚的祝福,愿考研的小伙伴都能上岸,找工作的老铁都能进大厂。希望能得到各位老铁的三连支持,我们一起加油吧,奥里给!

你可能感兴趣的:(详解C语言,c语言,后端)