首页 > 其他分享 >结构体总结

结构体总结

时间:2023-03-19 20:32:56浏览次数:37  
标签:总结 char struct int 位段 对齐 结构

在之前我们已经学过了,结构体类型的创建,初始化,和结构体的传参。那么下面我们就来学习一下

1.结构体的特殊声明

2.结构体的自引用

3.结构体变量的定义和初始化

4.结构体的内存对齐(极其重要)

5.修改默认对齐数

6.结构体的传参

在学习完后我们还会讲解利用结构体完成位段,包括以下知识点。

1.什么是位段

2.位段的内存分配

3.位段的跨平台问题

4.位段的应用

下面我们就来学习

1.结构体的特殊声明

下面我们我们来看一下一个普通的结构体的声明和初始化。

#include<stdio.h>
struct stu
{
int num;
char name[20];
}s1;////声明类型的同时定义变量s1
int main()
{
struct stu s1 = { 10,"asddf" };//完成对s1的初始化
return 0;
}

除了这种声明方式外,结构体还有一个特殊的声明方式,即不写类型名(例如不写上面代码的stu)这种特殊的声明方式(也是结构体的不完全声明)也就是匿名结构体类型。

下面我们就来看代码

#include<stdio.h>
#include<string.h>
struct
{
int num;
char name[20];
}s1;
int main()
{
s1.num = 100;
strncpy(s1.name, "wangwu",6);
printf("%d %s", s1.num, s1.name);
return 0;
}

运行结果:

我饿们

结构体总结_#include

这种匿名结构体就只能使用一次,当我们用完s1后若还想要再次创建一个s2是不可能的,因为没有结构体的类型名,若想再次创建只能在匿名结构体的最后在创建结构体变量。而有类型名的结构体就可以在你需要的地方随时创建结构体变量。

还有一种错误用法如下图代码

#include<stdio.h>
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
int main()
{

p = &x;//这样也是不行的,因为计算机任务这两个结构体是不同的,不能因为这两个结构体都是匿名的,
//并且成员也相同就认为p作为结构体指针就可以接收x的地址。
return 0;
}

2.结构体的自引用

在写代码的时候我们可能会遇到在访问这个结构体的时候能顺便访问另外一个结构体,而这也就是结构体的自引用。

我们举一个简单的例子,假设这里有1 2 3 4 5 这一组数据,我们要把它们储存起来有几种方法呢?

首先就是我们向内存申请一块连续的空间用以储存这些数据(和数组相似),在数据结构中这种储存方式也叫做顺序表。

除了顺序以外有没有可能计算机在储存这些数据的时候,在内存里不是连续而是乱序的呢?

对应顺序表我们要找到这些数据只用从头到尾遍历就可以了,但是这种乱序的数据我们要怎么找呢?

方法就是我们在找到1的时候1能够让我们找到2,2能让我们找到3,3能让我们找到4,4能让我们找到5。而到5的时候最好就不要让我们继续往下找了。这个时候我们就只用找到1就可以找到所有的数字。

图像表示

结构体总结_位段_02

这种数据的储存方式就叫做链表。

上面的这两种结构都是如同被一根线串起来一样,这两种数据结构也就被称为线性数据结构。除此之外还有树形的数据结构(例如二叉树)但我还未学习到这些就不解释了。

我们回到链表的那种方式,在链表储存里,储存数据的位置被叫做节点,我们这里就是要通过节点来找到下一个节点,直到找到最后一个节点停止寻找。而且这些数据都还是同一类型的。那么就意味着一个节点里不仅要存储自己的数据还要储存下一个节点的地址。

我们用结构体来表示节点

#include<stdio.h>
struct node//假设这就是一个节点
{
int num;//用以自身储存数据
struct node* p;//用以储存下一个节点的地址。因为节点都是struct node型的
//所以就用这种类型的指针接收下一个节点的地址
};//这种写法就不会因为地址大小只会是4或者8个循环
下面我写的就是一种错误写法
struct node//假设这就是一个节点
{
int num;//用以自身储存数据
struct node p;//这里不是用指针而是直接储存下一个节点
//所以就用这种类型的指针接收下一个节点的地址
};//对于这种写法我们想一下,这个结构的大小是多少呢?
//在这个node里面有一个数据还有一个结构体,而这个结构体里面还有一个数据和结构体,
//这样就会导致无限循环
int main()
{
struct node n1;
struct node n2;
n1.p = &n2;//这样就能通过n1找到n2
return 0;
}

而为了方便也有可能会有人写出以下代码

#include<stdio.h>
typedef struct node
{
int num;
i* nect;
}i;//这里的i不是创建了一个变量,而是将struct node这个结构的名字改成了i这也是typedef的功能(这个功能也能运用于匿名结构体)
//这种写法肯定是不可取的,因为代码执行是有顺序的当代码运行至结构体程序内的时候
//计算机并不知道这个i*是个什么东西
//只有在运行完这个代码之后,计算机才会知道i是什么。
//所以这种写法不可取
int main()
{
return 0;
}

总结:在结构体需要调用另一个结构体(这个结构体和本身是同一类型的)的时候要使用指针来调用。

结构体变量的定义和初始化。

变量的定义就是两种1.在创建这个结构体的时候就在末尾创建了变量,2.运用类型来创建变量.

变量的初始化:

1.未嵌套结构体的变量的初始化:

#include<stdio.h>
struct S
{
int num;
char c;
};
int main()
{
struct S a = { 100,'c' };//这里就是创建了一个结构体变量
//并且初始化了(初始化的顺序是按照默认顺序来的)
return 0;
}
//当然如果我们想要初始化c,再初始化c也是可以的
int main()
{
struct s a={.c ='a',.num=100};//(初始化顺序是按照我们想要的顺序来的)
return 0;
}

2.嵌套结构体的变量的初始化:

#include<stdio.h>
struct S
{
int num;
char c;
};
struct B
{
int num;
struct S c;
};//这个和自调用不同,不是调用的本身而是调用了另外一个结构体。
int main()
{
struct S a = { 100,'c' };//这里就是创建了一个结构体变量
//并且初始化了.
struct B c = { 100,{100,'d'} };//因为这个c变量里面还有一个结构体所以还需要一个大括号
//初始化调用的那个结构体。
return 0;
}

通过调式就可以看到:

结构体总结_位段_03

而对于如何访问结构体成员运用的操作符就是.和->了(两者也是有区别的)

点操作符左边的操作数是一个“结果为结构”的表达式;

箭头操作符左边的操作数是一个指向结构的指针。

​结构体的内存对齐(极其重要)

我们现在学习了结构体的一些基本知识,现在让我们想一下计算机是如何为一个结构体分配空间的呢?(也就是一个结构体的大小是多少呢?)

我们看下面的代码

#include<stdio.h>
struct s1
{
int a;
char c;
};
struct s2
{
char b;
int a;
char c;
};
struct s3
{
char b;
int a;
char c;
char d;
};
int main()
{
printf("%d\n", sizeof(struct s1));
printf("%d\n", sizeof(struct s2));
printf("%d\n", sizeof(struct s3));
return 0;
}

运行结果是:

结构体总结_位段_04

那么为什么呢?下面我们就要说明结构体内存对齐的4条规则:

1.结构体的第一个成员永远放在0偏移处

2.第二个成员开始,以后的每个成员都要对齐到某个对齐数的整数倍处

这个对齐数是:成员自身大小和默认对齐数比较后的较小值

备注:vs环境之下的默认对齐数是8,gcc环境下没有默认对齐数,对齐数就是成员自身的大小

3.当成员全部放进去之后,结构体的大小是全部结构体成员的对齐数比较后,最大对齐数的整数倍

4.当遇到嵌套结构体的时候,嵌套的结构体对齐到自己元素里的对齐数的最大值的整数倍处(也就是看被嵌套的结构体的成员,里的最大对齐数,这个结构体被存放的空间的首位置的偏移量也就是这个最大对齐数的整数倍)而这个包含嵌套结构体的大小也就是所有对齐数(包含被嵌套结构体成员的对齐数)的最大值的整数倍。

首先我们来讲解什么是0偏移处:

假设有一个结构体变量,当它的第一个元素的首字节所放的那个空间,就是0偏移处然后下一个空间也就是1偏移处。用图像表示:

结构体总结_#include_05

然后下面我们就来逐个分析上面的三个代码:

同时一个结构体成员的对齐数求法就是:将这个成员的大小和默认对齐数比较取较小者

#include<stdio.h>
struct s1
{
int a;//首先这个a的大小是4 默认对齐数是8,所以这个成员的对齐数就是4
char c;//同上这个的对齐数就是1
};
int main()
{
struct s1 s;
printf("%d ", sizeof(struct s1));
return 0;
}

然后我们按照规则1为s变量里的a分配空间如图(红色就是)

然后按照规则2:为第二个成员分配的空间的偏移量应该为1的整数倍(灰色的就是为char分配的空间)

然后依据规则三:结构体的大小应该是所有对齐数里最大对齐数的整数倍。(这里最大对齐数为4,而8就是4的整数倍所以满足。

在这里我们也可以运用代码将偏移量求出来验证

运用的宏就是offsetof

看代码

#include<stdio.h>
#include<stddef.h>//offsetof的头文件
struct S
{
char c;
int a;
};
int main()
{

printf("%d\n", offsetof(struct S, c));
printf("%d\n", offsetof(struct S, a));//这样就能得到c和a的偏移量
return 0;
}


结构体总结_#include_06

和我们下面画的图也是一样的头字节的偏移量就是0和4

结构体总结_数据_07

然后我们分析下一个代码:

#include<stdio.h>
struct s2
{
char b;//对齐数为1
int a;//对齐数为4
char c;//对齐数为1
};
int main()
{
printf("%d ", sizeof(struct s2));
return 0;
}

图像分析:

结构体总结_#include_08

最后的那个代码就是将浪费的偏移量为9的空间重新利用起来了所以大小还是12,就不再画图细说了。

下面我们来看下面这个结构体的大小计算

#include<stdio.h>
struct S3
{
double d;//对齐数为8,
char c;//对齐数为1
int i;//对齐数为4
};
int main()
{
printf("%d ", sizeof(struct S3));
return 0;
}

结构体总结_#include_09

在上面那个代码的基础下我们来看下面的这个代码:

#include<stdio.h>
struct S3
{
double d;//对齐数为8,
char c;//对齐数为1
int i;//对齐数为4
};
struct S4
{
char c1;//对齐数为1
struct S3 s3;//要向8对齐
double d;//对齐数为8
};
int main()
{
printf("%d ", sizeof(struct S4));
return 0;
}//同样都是求结构体的大小,但是这个结构体里面还包含了一个结构体。

对于这种结构体要计算大小我们就要运用到规则四了。

4.当遇到嵌套结构体的时候,嵌套的结构体对齐到自己元素里的对齐数的最大值的整数倍处(也就是看被嵌套的结构体的成员,里的最大对齐数,这个结构体被存放的空间的首位置的偏移量也就是这个最大对齐数的整数倍)而这个包含嵌套结构体的大小也就是所有对齐数(包含被嵌套结构体成员的对齐数)的最大值的整数倍。

图像表示:

结构体总结_#include_10

所以这里的大小就是32。

这样我们就学习完了一个结构体在内存里面是如何分配空间的。

将上面的规则缩减一下:

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

那么我们想一下为什么会有内存对齐这种东西存在呢?明明只需要按照结构体成员的字节大小分配空间就可以储存,为什么要有内存对齐呢?

  1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特 定类型的数据,否则抛出硬件异常。
  2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问

用图像来解释性能原因

对于32位的机器来说:一次读取的内容也就是32个比特位也就是4个字节。

而未对齐的情况下读取a机器就要读取两次才能读取到a

结构体总结_位段_11

若是已对其的情况下要读取a就只需要先跳到a的最左端读取一次

结构体总结_#include_12

。我们之前说过在vs环境之下默认对齐数是8,而gcc的环境之下不存在默认对齐数。

那么在vs环境下我们是否能够修改默认对齐数呢?

当然是可以的。

使用#pragma pack(4)来修改默认对齐数。这里我就是将默认对齐数修改为了4

当我们不需要4来作为默认对齐数的时候,#pragma pack()就可以让默认对齐数重新修改为8.

我们来看下面的代码。

#include <stdio.h>
#pragma pack(8)//设置默认对齐数为8
struct S1
{
char c1;//对齐数为1
int i;//对齐数为4
char c2;//对齐数为1
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数为1
struct S2
{
char c1;//对齐数为1
int i;//对齐数为1,因为自身大小为4但默认对齐数为1所以对齐数为1
char c2;//对齐数为1
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
//输出的结果是什么?
printf("%d\n", sizeof(struct S1));//按照规则这里的大小就是12
printf("%d\n", sizeof(struct S2));//按照规则这里就是6
return 0;
}

运行结果:

结构体总结_数据_13

结构体传参:

下面我们来看一个代码:

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;
}

那么这两种传参方式哪一种更好呢?明显是第二种传地址更好,如果是第一种那么在传递参数时就会将1000个整型拷贝一份后传递,很浪费空间,而第二种传递地址就只需要拷贝一个地址也就是4或者8个字节就可以,极大的节省空间。

在学习完结构体后我们就要学习使用结构体来实现位段了。

首先什么是位段

首先位段的成员只能

是整型家族的类型才能有位段,浮点型家族是不能的。但一般我们只会对int ,unsigned int,signed int类型的变量进行位段。

#include<stdio.h>
//要搞清楚下面结构体的意思就要知道位段里的位指的就是二进制位
struct A
{
int _a : 2;//想一下如果我们创建了一个整型变量a但是只用来储存0,1,2,3而这几个数
//只需要两个bite位就能完成了,而一个整型有32个比特位就浪费了30个比特位
//这里就是创建了一个_a但是分配给它的空间只有2个比特位便足够_a使用了。
//而这也是位段出现的原因,当我们的结构体成员某些时候能够少一些空间就能够储存到所有可能的值,
//那么我们就可以让这个成员所占的空间少一点。
int _b : 5;//同上
int _c : 10;
int _d : 30;
};//这就是一个位段
int main()
{
//那么这个结构体的大小是多少呢?
printf("%d \n", sizeof(struct A));//大小为8
//但是为什么呢?
//经过理解我们知道A里面的元素占47个比特位那为什么是8个字节64个比特位呢?
//我们通过画图解决这个问题
return 0;
}

下面就是位段的内存分配

也能够解释为什么位段不适用于跨平台,和上面的那个结构体为啥是8个字节64个比特位。

首先位段内存分配的规则:

  1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
  2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
  3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

那么通过规则我们就来分析一下上面的代码首先计算机会分配四个字节的空间当作备用那么_a要了2个比特位,备用空间就还剩30个比特位,_b又要了5个,备用空间就还剩25个,_c要了10个,备用空间就还剩15个,但是_d要30个,而备用空间只有15个了所以计算机就又拿了32个比特位当作备用空间。那么这时候就出现了分歧,之前剩下的15个bite位的空间是要使用还是丢弃呢?而c语言的标准也没有明确规定。到底使用还是不适用完全取决于编译器,所以位段不具备跨平台性。

但是还有很多细节我们没有搞清楚:

1.计算机拿32个bite位作为备用空间,在使用这个空间的时候是从低地址到高地址使用呢?还是从高地址到低地址使用呢?

2.在重新拿了一片空间之后原空间里剩余的bite位是被丢弃了还是继续使用呢?

那么我们下面就来看一下在vs的环境之下,计算机是怎么做的。

试验代码

struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
s.a = 10;//10的二进制表示就是1010
s.b = 12;//1100
s.c = 3;//0011
s.d = 4;//0100
return 0;
}

我们通过图像来了解计算机怎么为这些位段分配空间

我们先假设计算机使用是从低位到高位使用备用空间的,然后对于一个重新拿了一片空间后原空间剩余的bite位我们全部选择丢弃。

那么最后通过图像表示abcd就是这么储存的。

结构体总结_#include_14

那么我们如何验证我们的假设呢?我们便继续分析将数据存入到这些空间内部。

需要注意的是在计算机内部是以16进制表示的的而两个16进制数也就是1个字节。

结构体总结_数据_15

通过调式验证我们的假设就是对的

结构体总结_#include_16

那么位段为什么会出现跨平台问题呢?

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

位段最重要的应用就是能优化数据储存的空间。

标签:总结,char,struct,int,位段,对齐,结构
From: https://blog.51cto.com/u_15838996/6131329

相关文章

  • 3.19每日总结
     今天学习了1h。数据库操作类新建一个类"UserDBHelper",这个类extendsSQLiteOpenHelperpublicclassUserDBHelperextendsSQLiteOpenHelper{}定义类内的成员变量p......
  • c++11新特性总结
    C++11新增加特性1.=default,delete=default如果我们没有定义构造函数,C++编译器会自动为我们创建一个默认构造函数。但是如果我们定义了一个构造函数,那么编译器就不会为......
  • ChatGPT背后的算法——RLHF总结
    ChatGPT背后的算法——RLHF总结参考链接:抱抱脸:ChatGPT背后的算法——RLHF|附12篇RLHF必刷论文(qq.com)背景 (文本生成的语言模型评价不在训练中)chatGPT训练4步骤......
  • 控制结构
    控制结构:顺序结构、选择结构、循环结构顺序结构:按照代码的顺序由上往下执行代码选择结构:单分支结构if(条件语句)双分支结构if(条件语句1)......
  • [LeetCode] 数据结构入门
    数据结构入门217存在重复元素给你一个整数数组nums。如果任一值在数组中出现至少两次,返回true;如果数组中每个元素互不相同,返回false。解法1:两层循环第一层循......
  • SpringBoot集成Swagger错误总结
    错误展示rorstartingApplicationContext.Todisplaytheconditionsreportre-runyourapplicationwith'debug'enabled.2023-03-1915:37:55.307ERROR12980---......
  • 分布式事务解决方案总结 - 本地消息表
    1,什么是分布式事务?在传统架构中往往是一个单体架构,一个系统就对应一个war包,然后这个系统也只有一个数据库。即一个应用对应一个数据库,此时能满足传统的数据库事务,满足ACID......
  • 模拟赛总结
    模拟赛总结2023-3-19赛时T1花了半个小时看没有推出来式子直接跳T2。一开T2先的部分分,看到一个没有环的情况。想了一下发现直接拓扑排序每次炸相同深度的点就可以了。后......
  • Stream 总结
    1前言Stream是Java8中为方便操作集合及其元素而定制的接口,它将要处理的元素集合看作一种流,对流中的元素进行过滤、排序、映射、聚合等操作。使用StreamAPI,就好像使......
  • 十大排序总结
    Introduction​ 本篇是对十大排序的总结,会涉及每个排序的重要步骤、时间复杂度、空间复杂度、稳定性、代码实现Summary排序算法最差时间复杂度空间复杂度平均时......