结构体详解 - 数据结构

目录

一.结构体类型的声明

1.结构体回顾

(1)结构的声明

​编辑(2)结构体变量的创建和初始化

[2.结构的特殊声明 - 用的不多](#2.结构的特殊声明 - 用的不多)

(1)代码举例:正常结构体类型写法

(2)代码举例:匿名结构体类型写法

(3)特殊情况:

[(4)匿名结构体重命名 - 关键字 typedef](#(4)匿名结构体重命名 - 关键字 typedef)

[2.结构的自引用 - 一般用于定义链表](#2.结构的自引用 - 一般用于定义链表)

(1)数据结构:知识点补充

[(2)代码举例:描述链表 - 错误写法](#(2)代码举例:描述链表 - 错误写法)

[(3)代码举例:描述链表 - 正确写法](#(3)代码举例:描述链表 - 正确写法)

(4)结构体自引用匿名写法行不行?

二.结构体内存对齐

1.对齐规则

(1)知识点补充

(2)练习:计算结构体的大小

2.为什么存在内存对齐?

三.结构体设计写法:

1.修改默认对⻬数

2.结构体传参

[四. 结构体实现位段](#四. 结构体实现位段)

1.什么是位段

2.位段的内存分配

3.位段的跨平台问题

4.位段的应用

(1)知识点补充:IP数据报

5.位段使用的注意事项


一.结构体类型的声明

2种访问方式:

1.结构体变量.成员名

2.结构体指针->成员

3.如果指针想点访问,必须自己先解引用.成员 ,因为.的优先级高。(*pa).成员名

1.结构体回顾

结构是⼀些值的集合,这些值称为成员变量。/*结构的每个成员可以是不同类型的变量*/。
数组是一组相同类型元素的集合。

(1)结构的声明

(2)结构体变量的创建和初始化

代码举例:

cpp 复制代码
struct Stu
{
	char name[20];
	int age;
	char sex[5];
	char id[20];
};
int mian()
{
	struct Stu s1 = { "zhangsan",20,"男","20222211058" };//按照默认顺序初始化
	struct Stu s2 = {.age = 30,.name = "lisi",.set = "nv",.id = "2022211059"};//指定顺序初始化

	printf("%s %d %s %s\n", s1.name, s1.age, s1.sex, s1.id);//结构体访问

	return 0;
}

2.结构的特殊声明 - 用的不多

在声明结构的时候,可以不完全的声明。- 就是匿名结构体 - 省略结构体名字

(1)代码举例:正常结构体类型写法

cpp 复制代码
struct s //这个结构体叫s
{
	int a;
	float b;
	char c;
}x;//x是这个结构体类型变量
int main()
{

	return 0;
}

(2)代码举例:匿名结构体类型写法

cpp 复制代码
struct  //去掉结构体类型名字
{
	int a;
	float b;
	char c;
}x;
int main()
{
	struct //struct在这里不能使用了,因为没有名字。
	return 0;
}

特点:匿名结构体只能使用一次。

(3)特殊情况:

匿名结构体遇见2个拥有相同成员类型,但结构体变量不一样

代码举例:

cpp 复制代码
struct  
{
	int a;
	float b;
	char c;
}x;
struct  
{
	int a;
	float b;
	char c;
}* p; //匿名结构体指针
int main()
{
	p = &x; //这样的代码行不行?
	//警告:
	//等号两边类型不兼容,因为是匿名的编译器不认识,所以编译器会认为是2种不同的类型。
	//匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使⽤⼀次。

	return 0;
}

这样的写法非常危险不建议使用。

什么时候用匿名结构体?
这个类型只是用一次以后再也不用了,但大部分情况不会用到匿名结构体类型,不要写出这种代码。

(4)匿名结构体重命名 - 关键字 typedef

作用:想多次使用匿名结构体

代码举例:

cpp 复制代码
typedef struct // struct S - 原结构体名字
{
	int a;
	char b;
	float c;
} S;//重命名为S
int main()
{
	S s1 = { 0 }, s2 = { 0 };

	return 0;
}

2.结构的自引用 - 一般用于定义链表

(1)数据结构:知识点补充

(2)代码举例:描述链表 - 错误写法

cpp 复制代码
struct Node
{
	int data;//存放数据
	struct Node next;//包含同类型结构体变量 - 错误写法
};
int main()
{
	return 0;
}

如果用sizeof计算结构体大小是多少?
sizeof(struct Node)是多少?

自己引用自己每次都包含自己同类型结构体变量 stryct Node next,这个类型大小会无穷无尽。所以这种写法肯定不行。

总结:结构体自引用绝对不能是结构体里边包含同类型结构体变量。

(3)代码举例:描述链表 - 正确写法

方法:存地址 - 存下个节点的地址,不往后面找了传NULL。

cpp 复制代码
struct Node
{
	int data;//存放数据 - 数据域 存数据
	struct Node* next;//存放下一个节点的结构体的地址 - 指针域 存地址 - 正确写法

};
int main()
{
	return 0;
}

总结:
1.结构体自己包含同类型节点结构体指针地址,这就叫结构体自己引用自己。

2.结构体自引用绝对不能是结构体里边包含同类型结构体变量,而是结构体里边可以有同类型结构体指针。
3.链表描述:上面是数据域存放数据 ,下面是指针域存放下个节点结构体的地址。

4.链表特点:只要知道头就能知道尾。

(4)结构体自引用匿名写法行不行?

总结:
1.结构体自引用不能写成匿名的。

2.结构体自引用类型必须是现成的才使用,使用完再重命名是可以的。

二.结构体内存对齐

讨论的问题就是:计算结构体的大小。
考点: 结构体内存对⻬。

1.对齐规则

掌握结构体的对⻬规则:

1./*结构体的第⼀个成员*/对⻬到和结构体变量起始位置/*偏移量*/为0的地址处。第一个成员总在偏移量0地址处。

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

对⻬数:是编译器默认的⼀个对⻬数 与 该成员变量大小的较⼩值。

  • VS 中默认的值为 8。

  • Linux中 gcc 没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩。

成员 和 默认对齐数8 选最小值就是自身对齐数,再对齐到地址自身的倍数就行。
补充:任何地址是对齐数1的倍数 - 用于char。

3.结构体总⼤⼩:/*为最⼤对⻬数*/(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)/*的整数倍*/。
不满足继续浪费空间,直到结构体总大小为对齐数的倍数。

4.如果/*嵌套了结构体的情况*/,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,

结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。

偏移量:是成员对齐的起始位置。

(1)知识点补充

宏 offsetof:功能计算结构体成员相较于起始位置的偏移量。

offsetof(type, member) - <stddef.h>

参数:

1.type:传结构体类型

2.member:传成员名

(2)练习:计算结构体的大小

代码举例:

cpp 复制代码
#include <stddef.h>
struct S1//结构体创建
{
	char c1;//对齐数:1 8 1 偏移量:0
	char c2;//对齐数:1 8 1 偏移量:1
	int i;  //对齐数:4 8 4 偏移量:4
};
struct S2//结构体创建
{
	char c1;//对齐数:1 8 1 偏移量:0
	int i;  //对齐数:4 8 4 偏移量:4
	char c2;//对齐数:1 8 1 偏移量:8
};
struct S3
{
	double d;//对齐数:8 8 8
	char c;//对齐数:1 8 1
	int i;//对齐数:4 8 4
};
struct S4
{
	char c1;//对齐数:1 8 1 偏移量:0
	struct S3 s3;//对齐数:16 8 8 偏移量:8
	double d;//8 8 8 偏移量:24
	分析:
	1.c1放在0偏移处,占1个字节//1.
	2.s3只需要对齐自己嵌套里面成员最大对齐数的倍数,再放S3所占结构体大小。//浪费空间+所占空间=7+16
	3.找到d最大对齐数的倍数,再放d自身大小。//8
	4.总共占23+1+8 = 32
	5.结构体总大小:因为满足嵌套结构体,所有对齐数包括嵌套结构体的最大对齐数的倍数。
};
int main()
{
	//初始化 - 结构体初始化用大括号
	struct S1 s1 = { 0 };
	struct S1 s2 = { 0 };

	//计算结构体的大小
	printf("%zd\n",sizeof(struct S1));//8 - 计算考虑对其原则1.2
	printf("%zd\n",sizeof(struct S2));//12 - 计算考虑对其原则1.2.3
	printf("%zd\n",sizeof(struct S3));//16 - 计算考虑对其原则1.2.3
	printf("%zd\n",sizeof(struct S3));//32 - 计算考虑对其原则1.2.3.4

	//计算结构体成员的偏移量
	printf("%zd\n", offsetof(struct S2, c1));
	printf("%zd\n", offsetof(struct S2, i));
	printf("%zd\n", offsetof(struct S2, c2));

	return 0;
}

结构体里面成员都是一样的,为啥顺序发生变化类型大小会不一样?只需要6个字节为啥一个是8一个是12?

因为:因为结构体这些成员有一个对齐的规则,放在一些对齐的边界上,如果要对齐就会浪费一些空间,使得实际开辟的空间会大于需要的空间。

总结:

1.每个成员都该对齐每个位置上去,不是随便哪都放,对齐过程可能造成空间浪费,结构体总大小不满足规则的时候还要继续浪费空间,让大小整体对齐。浪费的空间别人也用不上。

2.计算结构体的时候要想到这4种规则。

2.为什么存在内存对齐?

1.平台原因(移植原因):

不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
比如:整形变量4个字节有可能规定说像这种整形这种对象,存的时候存在地址为4的倍数中,取的时候只能在地址4的倍数中取。

有些平台就这样规定的,它总是规定那些数据在什么样地址取,不是任意地址都能访问的 - 平台原因。

2.2.性能原因:
/*数据结构(尤其是栈)*/应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要作两次内存访问。

假设:⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。

如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以⽤⼀个内存操作来读或者写值了。
否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两个8字节内存块中。

总结:

1.放在对齐的边界上更加方便只需要1次,访问效率更高,没有放在对齐边界要2次读取,效率比较低 - 性能角度。

2.总体来说:结构体的内存对⻬是拿空间来换取时间的做法。牺牲一些空间放在对齐边界上,拿取数据的效率高。

3.浪费空间会效率高一些,但是并不是无节制的浪费空间。

三.结构体设计写法:

那在设计结构体的时候,我们既要满⾜对⻬,⼜要节省空间,如何做到?
让占⽤空间⼩的成员尽量集中在⼀起。

1.修改默认对⻬数

默认对齐数是可以修改的,你认为这个对齐数不合理可以修改。

一般设置对齐数,是按类型大小设置的,不是设置3,5这种奇数。

#pragma 这个预处理指令,可以改变编译器的默认对⻬数。

代码举例:

cpp 复制代码
#pragma pack(1)//设置默认对⻬数为1
struct S
{
	char c1;
	int i;
	char c2;
};
#pragma pack()//取消设置的对⻬数,还原为默认
int main()
{
	//输出的结果是什么?
	printf("%d\n", sizeof(struct S));//6
	return 0;
}

总结:当你不想按对齐规则的时候,设置默认对齐数为1就没有对齐的概念了。

2.结构体传参

代码举例:

cpp 复制代码
struct S
{
	int data[1000];
	int num;
};
//传值调用
void print1(struct S t)
{
	//结构体.成员访问
	printf("%d %d\n", t.data[0], t.num);//1 100
}
//传址调用
void print2(const struct S* ps)
{
	//指针->成员访问
	printf("%d %d\n", ps->data[0], pa->num);
}
int main()
{
	struct S s = { {1,2,3,4,5},100 };
	print1(s);
	print2(&s);

	return 0;
}

那种设计方式更好?print1好,还是print2好?,这2个函数哪个好?

1.传址调用更好!因为形参是实参的临时拷贝,结构体要是太大,形参也要开辟很大空间。

2.传址发方式只需要传递一个地址空间4/8个字节的大小,通过地址找到这块空间。不管空间上还是时间上都更高效。

3.传值调用更安全,修改不了结构体成员的值,但是const修饰指针也可以更安全,也没法改变s的值。

原因:

函数传参的时候,参数是需要/*压栈*/,会有时间和空间上的系统开销。pus - 压

如果传递⼀个结构体对象的时候,结构体过大,参数压栈的的系统开销⽐较⼤,所以会导致性能的下降。

压栈:函数在传参的时候会把参数pus压栈的,把参数放在栈里面去。
结论:结构体传参的时候,要传结构体的地址。

四. 结构体实现位段

用的比较少,主要用于计算机底层实现 和 网络
位段是基于结构体的。

1.什么是位段

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

1.位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以选择其他类型,尤其char类型。
2.位段的成员名后边有⼀个冒号和⼀个数字。

代码举例:1

cpp 复制代码
代码举例:结构体写法
struct B
{
	int _a;
	int _b;
	int _c;
	int _d;
};
int main()
{
	//4+4+4+4 = 16个字节
	printf("%zd", sizeof(struct B));//16

	return 0;
}

代码举例:2

cpp 复制代码
struct A
{
	int _a : 2;//指所占的bit位,占2个bit位
	int _b : 5;//             占5个bit位...
	int _c : 10;
	int _d : 30;
};
int main()
{
	printf("%zd\n", sizeof(struct A));//8
	//2+5++10+30=47bit,要开辟2个整形的大小才能放的下,所以是8个字节

	return 0;
}

A就是⼀个位段类型。

那位段A所占内存的⼤⼩是多少?8

分析:位段只能在特殊场景使用

比如:一个整型变量a, 里面只存1,2,3,0,这4个数字其中1个,只占2个bit位,要是给1个整形的空间就浪费了。

所以说,能满足当前表达需求就可以给对应的空间。但不是节省的一点都不浪费!是按照4个字节或1个字节开辟的。

举例:47bit开辟相应的空间是8个字节不是6个字节因为是按照4个字节分配的,当4个字节不够用或用完再分配了4个字节空间给你使用。

知识点补充:

变量名:

1.字母,数字,下划线。

2.不能是数字开通。

2.位段的内存分配

1.位段的成员可以是 int unsigned int signed int 或者是 char 等类型
2.位段的空间上是按照需要以4个字节( int )或者1个字节( char )的⽅式来开辟的。

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

代码举例:研究位段在vs开辟的方式

cpp 复制代码
struct S
{
	char a : 3;//a放入1010,存不下4位只能存低位3位 010
	char b : 4;//b放入1100,刚刚好存下
	char c : 5;//c放入11,只有2位,高位补0补齐5位 00011
	char d : 4;//d放入100,只有3位,高位补0补齐4位 0100
	按一个字节开辟:开辟了3个字节
	//00000000 00000000 00000000  一个对象内部地址也有:高低之分
	//xb   a   xxxxc    xxxxd      x - 是浪费的空间,
	// 6   2   0   3   0   4
	//内存存的值:62 03 04
	//所以vs符号假设。
};
int main()
{
	struct S s = { 0 };//结构体所有成员初始化为0
	s.a = 10;
	s.b = 12;
	s.c = 3;
	s.d = 4;
	printf("%zd\n", sizeof(s));//3 

	return 0;
}

假设:规则 - //vs假设

1.从右向左使用。

2.如果剩余的空间不够下一个成员使用,就浪费。

3.仅仅符合vs,在其它平台不一定。

总结:位段不确定因素

1.当分配4个字节空间32bit位使用,不确定从左开始使用,还是从右开始使用。c语言没有规定,取决于编译器,跟大小端没关系。

2.当分配4个字节空间32bit不够使用,剩余的空间会不会接着使用,还是浪费剩余空间在下一块开辟的空间使用。c语言没有规定,取决于编译器。

3.位段的跨平台问题

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

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

总结:
跟结构对齐相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
跨平台或可移植:就是同一段代码可以在不同编译器照样能够识别编译。

4.位段的应用

/*根据网络协议中,IP数据报格式应用*/。很多的属性只需要⼏个bit位就能描述,这里使⽤位段,能够实现想要的效果,也节省了空间,这样⽹络传输的数据报⼤⼩也会较⼩⼀些,对⽹络的畅通是有帮助的。

(1)知识点补充:IP数据报

A 发送信息呵呵给 B ,不是随便扔在网络上就发送给B,那怎么没发送给C或者B?发送信息不仅仅发送呵呵,我们要对数据进行各种分装 - 根据IP数据报格式。

最重要2个分装:32位源IP地址和32位目的IP地址。IP地址决定发送给谁

源IP地址:决定从哪里来。

目的IP地址:决定从那里去。

总结:数据在网络上传输肯定是有协议的,约定好数据怎么发送,对方才能解析清楚你发送的什么,怎么样解析

5.位段使用的注意事项

位段的⼏个成员共有同⼀个字节,有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。
内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的。

所以不能对位段的成员使⽤ & 操作符,这样就不能使⽤scanf直接给位段的成员输⼊值,只能是先输⼊放在⼀个变量中,然后赋值给位段的成员。

代码举例 - 位段使用&操作符

cpp 复制代码
struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};
int main()
{
	struct A sa = { 0 };
	scanf("%d", &sa._b);//错误写法

	//正确写法
	int b = 0;
	scanf("%d", &b);
	sa._b = b;

	return 0;
}

总结:

1.对比结构体内存对齐节省了一半的空间。

2.对于/*内存对齐*/来说我可以适当牺牲一些空间,换来效率的提升。
3.对于位段来说就是为了节省空间。

4.使用位段一定是特殊场景。

5.位段是按照int或者char类型大小开辟空间的,当一块空间不够用或使用完才开辟下一块空间给你4个字节。
6.位段开辟空间是严格依赖于编译器。

7.注重可移植程序避免使用位段,或在不同编译环境位段开辟方式研究透再使用。
8.位段的大小不能超过成员的大小,否则会出错,位段有跨平台问题存在。

9.跨平台问题并不是不可解决的,得把不同平台情况研究透,针对不同平台写出不一样的代码!

10.结构体位段成员不能直接使用&操作符,因为里面的数据起始位置,并不是字节的起始位置。
11.数据结构的实现是离不开结构体的。会把各种数据结构节点,整体都会定义成数据结构!

相关推荐
chao_7891 小时前
二分查找篇——搜索旋转排序数组【LeetCode】两次二分查找
开发语言·数据结构·python·算法·leetcode
秋说3 小时前
【PTA数据结构 | C语言版】一元多项式求导
c语言·数据结构·算法
谭林杰4 小时前
B树和B+树
数据结构·b树
卡卡卡卡罗特5 小时前
每日mysql
数据结构·算法
chao_7895 小时前
二分查找篇——搜索旋转排序数组【LeetCode】一次二分查找
数据结构·python·算法·leetcode·二分查找
lifallen6 小时前
Paimon 原子提交实现
java·大数据·数据结构·数据库·后端·算法
不吃洋葱.7 小时前
前缀和|差分
数据结构·算法
哦吼!9 小时前
数据结构—二叉树(二)
数据结构
码农Cloudy.11 小时前
C语言<数据结构-链表>
c语言·数据结构·链表
lightqjx11 小时前
【数据结构】顺序表(sequential list)
c语言·开发语言·数据结构·算法