结构体的相关知识

结构体

结构体类型的声明、定义

结构的概念:
	结构是一些值的集合,这些值称为成员变量,结构的每个成员可以是不同的数据类型。
	(数组也是一些值的集合,这些值称为元素,数组的每个元素是相同的数据类型)
//1.完全声明
struct Stu //关键字  标签
{
	char name[20];//成员变量
	int age;
	char sex[10];
	int id[10];
}stu1;//此处定义的结构体变量是全局变量
//分号不能丢

struct Stu stu2; // 全局变量

//2.匿名声明
//值得注意的是匿名声明如果想创建变量,得在声明结构体类型时一同创建,否则再想创建就没机会了
struct 
{
	int id[10];
	char name[20];
	int price;
}book1;

struct
{
	int id[10];
	char name[20];
	int price;
}*p;

int main()
{
	//创建的局部变量
	struct Stu stu3;
	struct Stu stu4;

	return 0;
}
可以看出,上面两个匿名结构体成员变量相同,那么此处是否可以通过结构体指针*p访问book1呢?
很明显,是不能得,纵使成员变量一样,但实际上编译器会把他们当作不同的类型

结构体的初始化和重命名

重命名
typedef struct Node
{
	int age;
	char name;
}Node;//新的类型名

Node one;//使用新的类型名定义变量
//那么下面这种定义方法可行吗?
typedef struct Book
{
	int price;
	Book* next;
}Book;
//注意,是不行的,我们在重定义类型名之前就使用重定义之后的类型名,
//编译器会报错:未定义的标识符

int main()
{
	Node two;

	return 0;
}

初始化

struct Stu
{
	char name[20];
	int age;
	char sex[10];
	int id[10];
};
struct Book
{
	int price;
	char name[20];
	struct Stu stu3;  //结构体内可以包含不同的结构体类型
};

struct Stu stu2 = { "maal",33,"male",23232 };

int main()
{
	struct Stu stu1 = { "David",22,"male",112233 };
	struct Book book1 = { 88,"old house",{"haha",22,"female",32323} };
	return 0;

结构体的自引用

在结构中包含一个类型为该结构本身的成员是否可以呢?
首先答案是肯定的。
那么怎么包含呢?
struct Meul
{
	int prince;
	struct Meul one;
};

struct Sport
{
	int time;
	struct Sport *next;
};
int main()
{
	sizeof(struct Meul);
	return 0;
}

像上面两种方式的包含,那种是可行的呢?
第一种结构里包含一个自身结构,自身结构又包含一个自己,就这样一直包含下去.......
即一个节点存储下一个节点,下一个节点再存储下下个节点..........
(俄罗斯套娃可还行?)

如果想要求结构体大小,是无法求出来的。	

那么问题来了,第二种不也是自己包含自己吗?
这里我们借助链表(简单的就像Sport结构体,一个存数据,一个存地址)便于理解,但并不作深入讨论。

在这里插入图片描述

第二种包含的是结构体指针,(指针在32位系统下4个字节,64位系统下8个字节)
我们不是存储下一个节点,而是存储下一个节点的地址,当我们某一个指针域不需要再指向下一个节点时
,只需要将最后一个节点的指针域指向空就可以。

结构体内存对齐

求出结构体的大小是一个非常常见的问题。那么怎么求出呢?
首先我们先来看一段代码

struct stu
{
	char name;
	int age;
	char sex;
};

struct book
{
	char name;
	char id;
	int price;
};

int main()
{
	printf("%d\n", sizeof(struct stu));
	printf("%d\n", sizeof(struct book));
	return 0;
}

上面的两个不同的结构体类型的成员变量的数据类型一样,那么他们所占的字节数是否也一样呢?如果一样是否都占6个字节呢?

自认而然的,我们想到,数据类型相同,那么这些变量占的字节数也应该相同,都应该占6个字节
但事实并非如此。

如下图所示:
在这里插入图片描述
那么为什么会这样呢?

首先得掌握结构体的对齐规则:

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

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

对齐数 = 编译器默认的一个值与该成员大小的其中的较小值
例如:VS默认对齐数是8,而gcc没有默认对齐数

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

4.如果嵌套了结构体 , 嵌套的结构体对齐到自己最大对齐数的整数倍
(成员变量中最大的对齐数作为结构体的对齐数),结构体的整体大小就是所有最大对齐数
(含嵌套结构体的对齐数) 的整数倍。 

由此就可以求解了

struct stu
{
	char name;
	int age;
	char sex;
};

如图

假设从箭头处开始存储,箭头处指向的地址偏移量就是0
char变量大小为1,VS默认为8,取较小的,对齐数就是1
同理 int 对齐数为4 
存放数据的步骤:
1.箭头指向偏移地址为0的内存,是char的整数倍,放入1个字节的内容。
2.往下走,到达偏移量为1的内存处,不是4的整数倍,继续往下走,直到偏移量为4,放入4个字节的内容。
3.此时箭头指向的偏移量为8,是char的整数倍,放入1个字节的内容。
4.此时所有数据全部放入,整体大小为9,不是4的倍数(最大对齐数的倍数),补充3个字节,变成12个字节

所以此结构体的大小是12

在内存中存放的方式如图

在这里插入图片描述

同理,

struct book
{
	char name;
	char id;
	int price;
};

内存分布如图

存放数据的步骤:
1.箭头指向偏移地址为0的内存,是char的整数倍,放入1个字节的内容。
2.往下走,到达偏移量为1的内存处,是char的整数倍,放入1个字节的内容。
3.往下走,到达偏移量为2的内存处,不是4的整数倍,继续往下走,直到偏移量为4,放入4个字节的内容。
4.此时所有数据全部放入,整体大小为8,是4的倍数(最大对齐数的倍数),完成存储。

在这里插入图片描述

由此我们便知道了结构体大小的计算。
更进一步,上面我们知道结构体可以嵌套,那么嵌套结构体大小怎么求呢?
struct stu
{
	char name;
	int age;
	char sex;
};

struct book
{
	char name;
	char id;
	int price;
	struct stu s;
};

int main()
{
	printf("%d\n", sizeof(struct stu));
	printf("%d\n", sizeof(struct book));
	return 0;
}

首先我们了解了对齐数的概念,自然就能知道book结构体类型
前面两个char和int在内存的排列方式,最后那个结构体的对齐数怎么确定呢?

根据对齐数规则,这个结构体大小是12,VS默认对齐数是8,选较小的8,结果就是
24,但是事实并非如此。如图

在这里插入图片描述
这时候我们的规则4就起到了至关重要的作用

	4.如果嵌套了结构体 , 嵌套的结构体对齐到自己最大对齐数的整数倍
(成员变量中最大的对齐数作为结构体的对齐数),结构体的整体大小就是所有最大对齐数
(含嵌套结构体的对齐数) 的整数倍。 

根据这个规则,可以得到结构体的对齐数是成员变量中最大的对齐数,所以我们可以确定book类型中的
s结构体变量对齐数是4,步骤如下:

1.箭头指向偏移值为0的内存,是char的倍数,放入char数据。
2.往下走,找到偏移量为4的内存处,放入int数据,占用4个字节。
3.往下走,找到偏移量为5的内存处,放入char数据,占用1个字节。
4.此时偏移量为6,往下走,一直到偏移量为8的内存储,放入12个字节。
5.此时结构体总大小为20个字节,是4的倍数(最大对齐数),完成存储。

为什么会存在内存对齐?

大部分参考资料都是如是说的。

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

如图,假设内存从箭头处开始访问,一次可以访问4个字节的内容,由于没有采用内存对齐,所以需要访问
两次内存才能完整读出a的内容
在这里插入图片描述

总的来说:

结构体的内存对齐是用空间换时间

拓展知识

另外,我们可以通过预编译命令#pragma pack()来改变VS编译器的默认对齐数
#pragma pack(1)//设置默认对齐数为1
struct Stu
{
	char a1;
	int a2;
	char a3;
	int a4;
};
#pragma pack()//取消默认对齐数

int main()
{
	struct Stu s1;
	printf("%d\n", sizeof(s1));
}

如图

在这里插入图片描述

我们也可以通过offsetof宏来得到结构体各个成员变量的偏移量
#include <stddef.h>

#pragma pack(1)//设置默认对齐数为1
struct Stu
{
	char a1;
	int a2;
	char a3;
	int a4;
};
#pragma pack()//取消默认对齐数

int main()
{
	printf("%d\n", offsetof(struct Stu, a1));
	printf("%d\n", offsetof(struct Stu, a2));
	printf("%d\n", offsetof(struct Stu, a3));
	printf("%d\n", offsetof(struct Stu, a4));
}

结果如图(在改变默认对齐数的情况下)
在这里插入图片描述

结构体传参

函数调用时常常要传递一些参数,当我们需要传递结构体时怎么进行传递呢?
struct stu
{
	char name;
	int age;
	char sex;
};


void Init1(struct stu tmp)//值传递
{
	tmp.name = 'a';
	tmp.age = 12;
	tmp.sex = 'm';
}
void Init2(struct stu* p)//地址传递
{
	p->name = 'a';
	p->age = 19;
	p->sex = 'm';
}

void Print1(struct stu tmp)
{
	printf("%c %d %c\n", tmp.name, tmp.age, tmp.sex);
}
void Print2(const struct stu* p)
{
	printf("%c %d %c\n", p->name, p->age, p->sex);
}
int main()
{
	struct stu stu1={'b',22,'w'}, stu2={'q',44,'w'};
	Init1(stu1);
	Init2(&stu2);
	Print1(stu1);
	Print2(&stu2);
	return 0;
}

问题来了,想要修改结构体中的成员变量值,上面两种方式行不行?

首先,第一种值传递方法:

我们知道,传参时会创建临时变量,临时改变的变量值如果不返回并不会影响主函数中的值。

第二种地址传递方式,可以通过指针访问结构体的内存并进行修改。
如图

在这里插入图片描述
这里我们是通过调用Print函数打印结构体成员变量的值的
那么问题就来了,是Print1函数好呢?还是Print2函数好呢?

值传递方式会创建临时变量,占用内存,我们声明的结构体足够小,并不会有多大的影响。
但是当传递的变量足够大时,这样传递就显得尤为尴尬,浪费时间和空间。

而地址传递方式就没有这种顾虑,直接进行访问,当然,如果操作不当会改变原来的值,这并不是我们想要的,
这也就是加const修饰的原因。

所以我们就可以得到结论:结构体进行传参时,传结构体的地址

结构体实现位段

结构体是可以实现位段的
什么是位段?

1.位段的成员必须是整形,例如:int、unsigned int、signed int、short、char等等。
2.位段的成员名后边有一个冒号和数字。
(值得注意的是,冒号后面的数字是不能超过数据类型的大小的,例如:int后面的数字不能超32,char后面不能超8)
struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};

int main()
{
	struct A a;
	printf("%d", sizeof(a));
}

位段的定义如图。如果我们要输出a的大小,该是多少呢?

自然而然的,我们很容易想到,加起来不就行吗?47!!!
但事实并非如此,如图

在这里插入图片描述

顾名思义,位段位段,表示的是二进制位。即_a占2个比特位,_b占6个比特位,_c占10个比特位,
_d占30个比特位,所以后面的数字表示变量所占的比特位,而不是字节数。

表示比特位的话,不应该是47个比特位,就算用字节,也应该是6个字节,怎么是8个呢?

这就需要我们先来了解位段的内存分配

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

如图
内存分配步骤:

1.一次性先开辟一块整形空间,4个字节,32个比特位。
2.分给 a 两个bit,再分给 b 5个bit,再分给 c 10个bit
3.此时还剩下15个bit,不够 d 的空间了怎么办,那就再来一块空间吧,系统会再开辟一块4字节空间
4.把d放在新开辟的空间中,存储完成,占8个字节

在这里插入图片描述
但是再使用过程中可能会出现以下问题:

struct S
{
	char _a : 3;
	char _b : 4;
	char _c : 5;
	char _d : 4;
};

int main()
{
	struct S s;
	s._a = 10;
	s._b = 20;
	s._c = 3;
	s._d = 4;
	printf("%d %d %d %d\n", s._a, s._b, s._c, s._d);
}

首先,我们可以得出,总共占3个字节,那么输出的值应该为多少呢?

莫非是10、20、3、4?

然而并没有这么简单,如图
在这里插入图片描述

我们知道 a 占3bit,b 占 4 bit, c 占 5 bit,d 占 4 bit,
10、20、3、4转化为二进制分别是1010、10100、0011、0100
所以 a 就截取了后3位010,b 就截取了后4位 0100、c最高位补了个0变成00011、d正好,转化为
十进制就和图中一样了

如图
在这里插入图片描述

这里就会有疑问,为什么不是从低地址向高地址写入数据呢?这就牵扯到位段的跨平台问题了,如下

位段的跨平台问题

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

总结:

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

这里举一个位段的应用例子:

比如我们平常经常上网,给别人聊天发信息什么的,都要进行数据的传输,假如我要给女朋友发一个呵呵(慎用),
当我发这条消息的时候,系统怎么会知道发给我的女朋友的呢?是谁发的呢?因此我们在发消息的时候,并不只是
发消息这么简单,系统会追加许多相关信息,比如谁发的,发给谁等等信息。这里有一张图简单了解下

在这里插入图片描述

可以看出,发的消息绑定了许多信息,而且绑定的信息并不是char、int等数据类型的整数倍,如果不使用位段的话,
占的	空间就太多了,而位段却将内存空间用到了极致。
所以位段并不是一无是处
  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 11
    评论
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值