结构体介绍

我们都知道,数据有很多种类型:整型,字符型和浮点型等等,但有时候,这些类型并不够用,所以这个时候,就需要用到自定义类型。而今天我们要讲的就是其中之一的结构体

首先我们先给大家写一段代码,举一个例子:

struct Stu//(结构体类型)
{
	char names[20];//名字
	int age;//年龄
	char gender;//性别
};
  • 上面的Stu就是结构体的类型,而大括号里变量的称作结构体成员,是结构体的重要组成部分
  • 再给大家看一段代码
struct Stu
{
	int arr[20];
	char str;
}Un;
  • 大家应该可以看到分号前面跟着的Un,其实就是结构体Stu的变量,我们可以在结构体的最后创建变量,不一定需要在main函数中再创建
  • 而如果我们去掉Stu的话,又会发生什么呢?这样子做的话,这就是一个匿名结构体,其在定义时没有指定名称,因此只能创建单个结构体变量,比较适用于只需要使用1次的结构体
  • 给大家看一个关于匿名结构体的问题:
//匿名结构体类型
struct
{
  int a;
  char b;
  float c;
}x;
struct
{
  int a;
  char b;
  float c;
}a[20], *p;
  • 如果我们在上面代码的基础上,加上一个p=&x,会怎么样呢?
    答案是发生错误,编译器会把上面两个结构体当成两个完全不同的类型,所以是非法的
  • 尽管这两个匿名结构体的成员名称和类型相同(都有 int、char 和 float 类型的成员),但它们是在两个不同的结构体定义中声明的。在 C 语言中,每个结构体定义都会创建一个独立的类型。
    若要使 p 指向 x,需要使用相同的结构体类型定义,而不是两个匿名结构体定义

而结构体不光可以用其他类型当自己的成员,还可以自引用,但自引用也需要注意形式,先写一种错误的自引用形式:

struct Stu
{
	int a;
	struct Stu next;
};
  • 这是一种自引用(self-reference)的情况,其中结构体包含一个指向相同类型结构体的成员。通过在结构体内部定义成员 next,可以创建一个链表数据结构,其中每个节点都包含一个指向下一个节点的指针。
  • 命名成员为 next 的选择是任意的,实际上你可以使用任何合法的标识符名称作为自引用的成员名。在链表结构中,next 通常被用来表示下一个节点,方便遍历整个链表。
  • 在结构体内部,自引用成员必须是指针类型,而不是实际的结构体类型。否则,结构体的大小将无限增长,导致无法分配合适的内存空间。

而正确的形式是这样写的:

struct Stu
{
	int a;
	struct Stu* next;
};

结构体的自引用在数据结构中经常被使用,并且在许多情况下非常有用。以下是一些使用自引用的常见数据结构和它们的用途:

1. 链表(Linked List):链表是一种动态数据结构,其中每个节点包含数据和指向下一个节点的指针。通过结构体的自引用成员,可以链接多个节点,形成一个链表。链表可以高效地进行插入和删除操作,并且不需要预先分配连续的内存空间。

2. 树(Tree):树是一种分层的数据结构,其中每个节点可以有多个子节点。在树的表示中,结构体的自引用成员可以用来指向同类型的子节点,并形成树状结构。树在搜索、排序、组织和表示层次关系等问题中非常有用。

3. 图(Graph):图是由节点和节点之间的连接关系组成的数据结构。结构体的自引用成员可以用于在图的表示中指向其他相关节点,形成不同类型的图,例如有向图或无向图。图是解决网络、路径查找和相互关系等问题的重要工具。

4. 堆(Heap):堆是一种特殊的树形数据结构,用于动态地管理内存。在堆的实现中,结构体的自引用成员可以指向堆结构中的父节点、子节点或兄弟节点,以实现堆的性质和操作。

这些仅是结构体自引用的一些常见用途示例。结构体的自引用允许在数据结构中有效地组织和操作复杂的关系,提供更灵活和动态的数据表示和操作方式。

另外,如果想在自引用中给结构体重命名,一定不要给匿名结构体typedef,因为重命名是在自引用之后才重命名的,但是在匿名结构体中已经提前运用到了,这样是不行的

结构体变量的创建与初始化

struct Str1
{
	int a;
	char b[20];
}p1;//声明类型同时定义变量p1

struct Str1 str1;//定义结构体变量str1

struct Str1 str2 = { 4,"happy everyday" };//初始化结构体变量str2

struct Str2
{
	int i;
	struct Str1 p;//成员p的类型是struct Str1
}p2 = { 3,{4,"hello"} };//结构体嵌套初始化

以上都是结构体变量创建与初始化的例子

还有一种不按照顺序初始化的情况,在C99标准中允许这种方式:

struct Str1
{
	int a;
	char b[20];
}p1;

struct Str1 str2 = { .b="happy everyday",.a = 4 };

需要注意的是如果不这么写,那么一定要按照顺序初始化,否则运行结果会出错

结构成员访问操作符

结构成员访问操作符有两个,⼀个是‘ .’ ,⼀个是‘ ->’ 。

  • 这样使用:
    结构体变量.成员变量名
    结构体指针—>成员变量名
#include<stdio.h>

struct Stu//创建结构体类型Stu
{
	int a;
	char b[10];
};

int main()
{
	struct Stu u[4] = { {1,"long"},{2,"time"}, {3,"no"},{4,"see"} };//初始化u
	printf("%s ", u->b);//u是首元素地址,打印long,用到->
	for (int i = 0; i < 4; i++)
	{
		printf("%d ", u[i].a);//u[i]=*(u+i),解引用变为结构体变量,所以用.
	}
	return 0;
}

结构体内存对齐(重点)

首先得掌握结构体的对⻬规则:
1. 结构体的第⼀个成员对⻬到相对结构体变量起始位置偏移量为0的地址处
2. 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。
对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值。

  • VS中默认的值为8
  • Linux中没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩
    3. 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的整数倍。
    4. 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。
//练习1
	struct S1
	{
		char c1;//1
		int i;//4
		char c2;//1
	};
	printf("%d\n", sizeof(struct S1));//12
	//练习2
	struct S2
	{
		char c1;//1
		char c2;//1
		int i;//4
	};
	printf("%d\n", sizeof(struct S2));//8
	//练习3
	struct S3
	{
		double d;//8
		char c;//1
		int i;//4
	};
	printf("%d\n", sizeof(struct S3));//16
	//练习4-结构体嵌套问题
	struct S4
	{
		char c1;//1
		struct S3 s3;//16
		double d;//8
	};
	printf("%d\n", sizeof(struct S4));//32

在这里插入图片描述

在这里插入图片描述
上图是结构体内存对齐的图解
拿第一个举例:
(1)结构体的第⼀个成员对⻬到相对结构体变量起始位置偏移量为0的地址处,char类型字节为1,所以c1放在0处
(2) 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。
对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值。int的字节数为4,4与8相比,4小,所以对齐到4,往下存储4个
(3)c2与c1相同,故放1个
(4)结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的整数倍。此时在8的位子上,也就是9个字节,那么与最大对齐数4并不对,所以再往后面空3个字节到11,刚好12个字节

而大家更疑惑的应该是第四个,按照规律来说,S3应该放在16的位子上,而不是8,而这就涉及到了(4)如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。
所以就是S3中的double=8,但大小16个字节还是不会缩水,最后刚好放到31,刚好32个字节,是16的倍数

上面如果是空白的空间,就是被浪费的空间

结构体为什么需要内存对齐

了解了内存对齐的方式,我们也需要知道结构体为什么需要内存对齐
1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两个8字节内存块中。

这个可以举个例子:
在这里插入图片描述

如果一个结构体里有一个char类型和double类型,那么不分配的话,就会占领9个字节,而此时如果读取8个字节,那么读取的只是一部分double的数据,还要再读取后面的8个字节才可以把double 的数据读取完整。而如果对其了,可以每8个字节读取,而第一个8个字节存储的就是char,而第二个则是存储的double,让内存访问变得高效

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

如果要使结构体既效率高,占用空间也少,那么最好的方法就是把占用内存小的变量放在前面,而占用大的放在后面
同时,如果在使用时认为访问不够便利,也可以改变编译器的对齐数,使用#pragma back(num),num=几对齐数就被修改为几,如果里面是空的,就是说修改回默认对齐数

结构体传参

#include<stdio.h>

struct Stu
{
	int num;
	char arr[10];
}un;

//方式1:传值调用
void print_num(struct Stu un)
{
	printf("%d ", un.num);
}

//方式2:传址调用
void print_arr(struct Stu* Un)
{
	printf("%s\n", Un->arr);
}

int main()
{
	struct Stu un = { 25,"happiness" };
	print_num(un);
	print_arr(&un);
	return 0;
}

上面的两种方法分别是传值调用和传址调用,我们一般推荐使用第二种,因为函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,所以会导致性能的下降。

结构体实现位段

位段的声明与结构体是类似的,有两个不同:
1. 位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以
选择其他类型。

2. 位段的成员名后边有⼀个冒号和⼀个数字。

#include<stdio.h>

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

int main()
{
	printf("%d\n", sizeof(struct Stu));//输出3,表示占3个字节
	return 0;
}

Stu就是一个位段类型,而在用sizeof测试位段所占内存大小时,程序输出的是3
需要注意的是:
1. 位段的成员可以是 int、 unsigned、 int 、signed int 或者是 char 等类型
2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的⽅式来开辟的。
3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使⽤位段。

分析:
1.当开辟了内存后,每个bit从右向左使用还是从左向右不确定
2.当前面的空间使用了之后,剩余的空间不足下一个成员使用时,剩余的空间是否使用,这不确定

在这里插入图片描述
这就是对于上面代码的分析,我们假设是从右使用,a用3个比特位,b用4个比特位,第一个字节还剩一个比特位;而第二个用5个比特位,剩下的3个不足以放下d,则开辟第三个字节,放下4个比特位的d

而当大家弄懂这个之后,接下来还有一道题请大家做做:

//了解空间是如何开辟的
struct S
{
 char a:3;//分配3个比特位
 char b:4;//分配4个比特位
 char c:5;//分配5个比特位
 char d:4;//分配4个比特位
};
struct S s = {0};//所有元素先初始化为0
s.a = 10;//1010,取三位010
//0000 0000变为0000 0010
s.b = 12;//1100,取4位1100
//变为0110 0010
s.c = 3;//011,取5位00011
//再开辟一个字节0000 0000,变为0000 0011
s.d = 4;//0100,取四位0100
//再开辟一个字节0000 0000,变为0000 0100
//最后合在一起就是 0110 0010 0000 0011 0000 0100
//  用十六进制表示  6    2    0     3    0    4

不知道经过分析之后,大家是否懂了在内存中是如何开辟空间的,如果大家感兴趣,也可以自己试一试
如果想把这些以16进制的形式打印出来,可以试试这段代码:

unsigned char* ptr = (unsigned char*)&un;

	for (int i = 0; i < sizeof(struct Stu); i++) {
		printf("%02X ", ptr[i]);
	}

当你对结构体进行内存打印时,需要以字节为单位访问结构体的内存表示。结构体 Stu 中的每个位字段(abcd)被编译器按照一定的顺序布局在内存中。

通过将结构体的地址强制转换为 unsigned char* 类型的指针,我们可以将结构体内存视为一系列连续的字节。然后,通过遍历这些字节并使用 %02X 格式转换说明符,我们可以以十六进制形式打印每个字节的值。

%02X 中的 %X 是用来将值格式化为十六进制的大写字母表示形式,并始终使用两位字符。02 表示至少使用两位字符来表示值,并在前面补上零,以保持一致宽度。

通过以十六进制格式打印结构体的内存表示,你可以观察结构体中每个位字段所占的字节,并了解它们的具体存储方式。这对于调试和理解位字段的布局和存储非常有帮助。

位段的跨平台问题

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

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

位段使用的注意事项

位段的⼏个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的。所以不能对位段的成员使⽤&操作符,这样就不能使⽤scanf直接给位段的成员输⼊值,只能是先输⼊放在⼀个变量中,然后赋值给位段的成员。

以上就是我对于结构体的一些介绍与运用,如果大家有更好的见解,欢迎来与我探讨!

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值