⛺️生活的理想,就是为了理想的生活!
文章目录
- 📋 前言
- 1 结构体的声明
- 1.1 结构的基础知识
- 1.2 结构的声明
- 1.2.1 . 匿名结构体类型声明
- 1.2.2 匿名结构体类型的的缺陷
- 2. 结构的自引用
- 2.1 结构体自引用的作用
- 2.2 结构体自引用的注意事项
- 3.结构体变量的定义和初始化
- 3.1 用标签名定义和初始化
- 3.1.1 如何不按顺序初始化
- 3.2 typedef 的定义和初始化
- 3.结构体嵌套的定义和初始化
- 3.1 自引用的定义和初始化
- 4. 结构体该如何传参
- 4.1 错误的结构体传参
- 4.2 正确的结构体传参
- 📝全篇总结
📋 前言
🌈hello! 各位宝子们大家好啊,结构体的基本使用和常见错误在上一篇详细讲解过了,不知道大家都学会了没有。 ⛳️今天给大家来个硬菜,教点高级点的结构体结构,给我们的数据结构开个好头!废话不多说直接进入正题
1 结构体的声明
1.1 结构的基础知识
这个部分博主在前一篇博客已经详细讲解过了,一俩句话也解释不清楚,结构体这方面基础不是很好的铁铁们可以去阅读一下这篇文章哦!万字详解包看包会!
1.2 结构的声明
1.2.1 . 匿名结构体类型声明
在声明结构的时候,也可以不完全的声明。面的两个结构在声明的时候省略掉了结构体标签(tag),看下有什么后果。
- 而这就叫匿名结构体类型
📚 代码演示:
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], * p;q
1.2.2 匿名结构体类型的的缺陷
那么匿名结构体有什么缺陷呢?其实有俩个缺陷匿名结构体第一没有 标签名,连名字都没有所以只能在创建结构体时定义。
- 只能在创建结构体时定义结构体变量
- 相同类型的结构体,我们编译器认为是不一样的
假如我们有俩个相同类型的 匿名结构体
,一个用来创建。一个创建 匿名结构体指针
用来存放相同类型的结构体变量地址!
📚 代码演示:
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}* p;
int main()
{
p = &x;
return 0;
}
📑 代码结果:
这里我们就可以看出虽然都是相同类型的匿名结构体,但是在编译期间我们的编译器认为他们俩类型是不一样的。
- 相同类型的
匿名
结构体指针,接收不了相同类型匿名结构体的地址
总结:
- 匿名结构体只能在创建是定义
- 相同类型的匿名结构体,编译器会这两个声明当成完全不同的两个类型。
- 所以匿名结构体只适合,那种只用一次的结构体上用法很少。
2. 结构的自引用
那么结构体如果想包含一个该结构本身的成员是否可以呢?答案是可以的,这种用法我们在数据结构这门课 链表时会经常使用!
- 那么我们看下面这段代码,自引用结构体是否可以这样定义呢?
📚 代码演示:
//代码1
struct Node
{
int data;
struct Node next;
};
//可行否?
如果可以,那 sizeof(struct Node)是多少?诶这里我们就会发现我们根本计算不了这个结构体的大小是多少!
- 这里就和套娃一样,
int data
我们知道是四个字节 - 而
struct Node
里面又包含了struct Node
这个根本就算不了嘛!逻辑上就错误了!
2.1 结构体自引用的作用
在我们数据结构体中有一个叫做链表的数据结构,这里就不给大家详细解释了,只给大家见见猪跑!免得搞混了。链表是我们数据结构中用来指向相同类型的元素但是在不同空间里的连续存储方式。
- 使他们向像一个链子一样可以相互链接访问
大家看着张图是不是就明白很多呢? 我们想在节点一找到相同类型的下一个节点:
- 诶!那这样我们只需要把相同类型节点的地址存放到上一个节点处是不是就可以了?
- 而不是去存放下一个节点的内容。那么我们就定义一个结构体
- 他的成员一个负责存放数组,一个负责存放下一个地点的地址
📚 代码演示:
//代码2
struct Node
{
int data;
struct Node* next;
};
这样我们就可以访问一块不连续空间但是,是相同类型的结构体变量了。也可以对比数组
- 数组是一块连续的空间里存放相同类型的数据
- 链表是一块不连续的空间里存放不相同类型的数据
- 而这就是结构体自引用的妙用了
2.2 结构体自引用的注意事项
但是在使用的时候,有些人会犯这样的错误一定要注意。
- 我们知道结构体可以重命名而很多人就会把重名的结构体当成结构体成员。
- 但这是非常错误的
//代码3
typedef struct
{
int data;
Node* next;
}Node;
//这样写代码,可行否?
📑 代码结果:
这时在编译期间就会发生错误,我们typedef 重定义还没生效呢!你就开始使用重定义之后的类型名了。
✅ 原因:结构体重定义在结构体结束时最后一行才生效,但是我们在重定义生效之前就想使用这肯定回发生错误呢!
- 正确的做法是在结构体里面我们还是使用未重命名之前的标签名。
//解决方案:
typedef struct Node
{
int data;//数据域
struct Node* next;//指针域
}Node;
3.结构体变量的定义和初始化
3.1 用标签名定义和初始化
这样的话我们可以直接在结构体后面直接定义变量,或者在需要定义的地方使用标签名定义结构体变量。
- 在声明结构体是创建的结构体变量是 全局变量
- 在大括号里面创建的结构体是 局部变量
📚 代码演示:
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
//全局变量
int main()
{
struct Point p2;//直接使用标签名定义
//局部变量
return 0;
}
而初始化的话也非常简单和数组是差不多的每个成员赋值用大括号扩起来,单引号隔开就好了!
- 大括号括起来,后面加引号
- 每个成员逗号隔开
📚 代码演示:
struct Point
{
int x;
int y;
}p1 = { 2, 1 };//创建时直接赋值
int main()
{
struct Point p2 = {12,13};//直接使用标签名定义
//然后进行赋值
return 0;
}
3.1.1 如何不按顺序初始化
有人说,那么我不想按结构体成员顺序赋值怎么办呢?
- 其实只需要用 点操作符
(.)
找到成员然后再赋值就可以了
#include <stdio.h>
struct Stu
{
char name[20];//姓名
int age;//年龄
char sex[5];//性别
char id[20];//学号
}p1 = { .id="20202356",.age = 18, .name = "lisi",.sex = "nan"};
int main()
{
printf("姓名\t年龄\t性别\t学号\n");
printf("%s\t %d\t%s\t%s\t",
p1.name,
p1.age,
p1.sex,
p1.id);
return 0;
}
📑 代码结果:
3.2 typedef 的定义和初始化
typedef重命名的结构体类型,只是定义与前面不一样,其他部分都是一样的!
- 但是重命名了,在声明后面就不能再创建我们的结构体变量了。
- 因为typedef 的定义结构体后面默认跟的是重命名的类型名。
📚 代码演示:
#include <stdio.h>
typedef struct Stu
{
char name[20];//姓名
int age;//年龄
char sex[5];//性别
char id[20];//学号
}Stu;
//全局变量
Stu p1 = { .id = "20202356",.age = 18, .name = "lisi",.sex = "nan" };
int main()
{
printf("姓名\t年龄\t性别\t学号\n");
//局部变量
Stu p2 = { .id = "202329",.age = 20, .name = "zhangsan",.sex = "nan" };
printf("%-10s\t %d\t %s\t %s\n", p1.name, p1.age, p1.sex, p1.id);
printf("%-10s\t %d\t %s\t %s\t", p2.name, p2.age, p2.sex, p2.id);
return 0;
}
📑 代码结果:
3.结构体嵌套的定义和初始化
结构体包含结构体的初始化,既然我们知道结构体是如何初始化的,那么结构体包含也就很明确了。
- 既然你也是结构体那么我,用大括号在包含一下
- 给你赋值不就完了,大括号套大括号
#include <stdio.h>
struct Stu
{
char a;
int num;
};
struct S
{
int arr[10];
struct Stu sn;
double d;
};
int main()
{
struct S s = {{1,2,3,4,5,6,7,8,9,10},
{"l",99},
3.14};
return 0;
}
3.1 自引用的定义和初始化
前面结构体包含结构体的的初始化我们都知道了,那么自引用和它基本一样,自引用引用的是相同类型的地址所以我们可以先赋值为 NULL 空指针就好!
📚 代码演示:
#include <stdio.h>
struct Point
{
int x;
int y;
};
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = { 10, {4,5}, NULL }; //结构体嵌套初始化
int main()
{
struct Node n2 = { 20, {5, 6}, NULL };//结构体嵌套初始化
printf("%d\t%d\t%d\t0x%p",
n2.data,
n2.p.x,
n2.p.y,
n2.next);
return 0;
}
📑 代码结果:
4. 结构体该如何传参
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;
}
上面的 print1 和 print2 函数哪个好些?答案是:首选print2函数。因为:
- 函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
- 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
- 所以结构体传参的时候一定要传地址!
🔥 由于函数在传递参数时,如果我们传的是实参,那么形参将是实参的一份临时拷贝。如果我们传过去的结构体很大,那么形参也要开辟相同大小的空间就会照成空间的浪费!
4.1 错误的结构体传参
我们来看一下下面这个例子大家就明白了,这里我们在main()
主函数里面创建了结构体变量想通过test()
函数进行赋值。
- 但是我们是
传值调用
,所以改变形参并不会改变实参。 - 形参只是实参的一份临时拷贝
#include <stdio.h>
typedef struct Point
{
int x;
int y;
}Pt;
void test(Pt pf)
{
pf.x = 2;
pf.y = 3;
}
int main()
{
Pt p2 = {0};
test(p2);
printf("%d %d", p2.x, p2.y);
return 0;
}
📑 代码结果:
4.2 正确的结构体传参
所以我们在结构体传参的时候一定要使用 传址调用 ,这样才能改变我们的结构体!
- 如果只想使用里面的值,而不想改变结构体变量
- 只许需要加上const修饰一下指针,让指针所指向的值不能发生改变这样就可以了!
📚 代码演示:
#include <stdio.h>
typedef struct Point
{
int x;
int y;
}Pt;
void test(Pt* x)
{
x->x = 10;
x->y = 5;
}
void test1(const Pt* x)
{
printf("%d %d", ps.x, ps.y);
}
int main()
{
Pt ps = { 0 };
test(&ps);
test1(&ps);
printf("%d %d", ps.x, ps.y);
return 0;
}
📑 代码结果:
📝全篇总结
✅ 归纳: 好了以上就是关于结构体进阶篇的全部内容了,希望各位铁铁们看完也都会了呢! 结构体的自引用自引用的例子链表结构体嵌套如何定义结构体传参的注意事项
☁️ 把本章的内容全部掌握,铁汁们就又向下一部分知识数据结构前进了一大步呢!