自学内容网 自学内容网

结构体详解 - 数据结构

目录

一.结构体类型的声明

1.结构体回顾

(1)结构的声明

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

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

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

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

(3)特殊情况:

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

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

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

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

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

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

二.结构体内存对齐

1.对齐规则

(1)知识点补充

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

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

三.结构体设计写法:

1.修改默认对⻬数

2.结构体传参

四. 结构体实现位段 

1.什么是位段

2.位段的内存分配

3.位段的跨平台问题

4.位段的应用

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

5.位段使用的注意事项


一.结构体类型的声明

2种访问方式:
1.结构体变量.成员名
2.结构体指针->成员
3.如果指针想点访问,必须自己先解引用.成员 ,因为.的优先级高。(*pa).成员名

1.结构体回顾

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

(1)结构的声明


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

代码举例:

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)代码举例:正常结构体类型写法

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

return 0;
}

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

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

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

(3)特殊情况:

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

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

作用:想多次使用匿名结构体
代码举例:

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

return 0;
}

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

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

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

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

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


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

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

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

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

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)练习:计算结构体的大小

代码举例:

#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 这个预处理指令,可以改变编译器的默认对⻬数。

代码举例:

#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.结构体传参

代码举例:

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

代码举例:结构体写法
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

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所占内存的⼤⼩是多少?

分析:位段只能在特殊场景使用
比如:一个整型变量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开辟的方式

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直接给位段的成员输⼊值,只能是先输⼊放在⼀个变量中,然后赋值给位段的成员。

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

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.数据结构的实现是离不开结构体的。会把各种数据结构节点,整体都会定义成数据结构!
 


原文地址:https://blog.csdn.net/2302_80515097/article/details/137813781

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!