【数据结构】双向链表 超详细 (含:何时用一级指针或二级指针;指针域的指针是否要释放)

目录

一、简介

二. 双链表的实现

1.准备工作及其注意事项

1.1 先创建三个文件

1.2 注意事项:帮助高效记忆

1.3   关于什么时候 用 一级指针接收,什么时候用 二级指针接收?

1.4 释放节点时,要将节点地址 置为NULL,难道 节点内部的 指针域的指针 就不用置为 NULL吗? 

2.双链表的基本功能接口

2.1 初始化哨兵位

 2.2 链表的创建新节点接口

2.3 打印

3. 插入 接口

3.1 尾插法

3.2 头插法

3.3 在 pos 位置之后插入数据

4. 查找

5.删除  接口

5.1 尾删法

5.2 头删法

5.3  删除 pos 位置的数据

6. 销毁链表 接口

6.1 二级指针版 

6.2 一级指针版

7. 总代码概览

List.h

List.c

test.c

三. 顺序表和双向链表的优缺点分析



一、简介

        单向链表在解决那些需要大量查找前趋节点的问题时,效率较为低下,因为单向链表适合“从前往后”查找,并不适合“从后往前”查找。

(若想要看 单链表,可以点击跳转:【数据结构】单向链表实现 超详细)

如果要提高链表的查找效率,那  双向链表(双链表)无疑是首选。

双向链表:即为 字面上的意思是“双向”的链表,如下图所示。


       

  • 双向 指各个节点之间的逻辑关系是 双向的,该链表通常 有一个头节点 -- 称为 哨兵位
  • 概念: 当链表中只有哨兵位节点的时候,我们称该链表为空链表,即哨兵位是不能删除的
  • 从上图还可以看出,双向链表中每个节点包括一下3个部分,分别是:
  • 指针域(前驱节点 prev )、
  • 数据域(用于存储数据元素 data)
  • 指针域(后继节点 next)。

二. 双链表的实现

1.准备工作及其注意事项

1.1 先创建三个文件

 解释这三个文件的作用
 1、头文件List.h  是来声明接口函数,定义链表,将几个公共用到的库函数集合起来
 2、源文件List.c  是用来具体实现接口
 3、源文件test.c  用于接口的测试工作 ,即具体的使用场景

1.2 注意事项:帮助高效记忆

1. 传递指针,都要断言 不能为 NULL ,指针不能为空:assert(phead);
2. 存在 关系到  前趋节点prev 或 后继节点next  的 情况,

    可以 直接定义变量 Prev = .... Next = ....   便于 理清思路,不易乱
3. 所有的 删除接口 都需要 断言链表不能只有 哨兵位: assert(phead->next != phead); 
4. 尾删法: 记得双链表的找尾 不像 单链表需要循环去找尾!!ptail = phead->prev;

5. 初始化创建头节点:推荐使用 调用 创建节点接口  的 方法
5. 销毁接口:推荐使用 传一级指针的方法

1.3   关于什么时候 用 一级指针接收,什么时候用 二级指针接收?(不看水话,可以直接 看下面 总结 部分

(有点水话,实在不明白思路的可以看一下详细解说的 ”水话“)

         在 单向链表 中,有一个指向 第一个节点的指针 plist,由于 头插法等操作,可能会改变 第一个节点,则 plist 要对应进行 更新,而 要想直接改变一个变量(plist是指针变量)的值,需要传地址,plist 的 &plist 是 一级指针的地址,就要用 二级指针 来接收

         在 双向链表 中,存在 头节点 head ,即 哨兵位,哨兵是 不用进行 改变本身这个节点的 地址的!!

那就有铁铁要问了,不是还要改变 头节点 head( 哨兵位 ) 的指向,要指向 第一个 节点 或 尾节点 吗? 

回答:因为 我们 要改变的 是 双链表节点 结构 中 的 结构体成员变量prev 和 next ,改变 结构体的成员变量 只需要利用 结构体指针 p->prev = .... 或 p->next   = .... 就达到 修改 双链表节点指向 问题了,而你本身并不需要改变 一个节点的地址 p

总结 

总结:修改 双链表节点 的指向,是通过  修改节点结构体内部的 两个成员变量 来实现的只需要用到 结构体指针(即该节点的地址 p),找到  两个成员变量,即可完成修改,因而传递 一级指针就好,不用像 单链表那样还要 传递 一级指针的 地址

1.4 释放节点时,要将节点地址 置为NULL,难道 节点内部的 指针域的指针 就不用置为 NULL吗? 

回答:不用 因为节点的空间已经还给操作系统了 那个指针域的指针所占据的空间也还回去了 操作系统后续分配内存就会把那块空间覆盖掉 就不会又啥影响

2.双链表的基本功能接口

2.1 初始化哨兵位

初始化哨兵位 第一种方法:传入 指针,进行"加工" 成 哨兵位

// 初始化一个哨兵位:
void LTInit(LTNode** pphead)
{*pphead = (LTNode*)malloc(sizeof(LTNode));// 开辟空间是否成功的判断:一般malloc不会失败,失败证明内存不够了,写下面的证明算是好习惯,不写一般没问题if (*pphead == NULL) {perror("malloc fail!");exit(1);}(*pphead)->data = -1;(*pphead)->next = (*pphead)->prev = *pphead; // 哨兵位初识化,两个指针都指向自己
}

初始化哨兵位第二种方法:直接一个函数生成一个哨兵位,返回哨兵位就好,不用传指针

LTNode* LTInit()
{LTNode* phead = (LTNode*)malloc(sizeof(LTNode));// 开辟空间是否成功的判断if (phead == NULL) {perror("malloc fail!");exit(1);}phead->data = -1; // data 随便定,反正哨兵位data无效phead->next = phead->prev = phead;return phead;
}

初始化哨兵位 第二种方法的2.0 版本:因为哨兵位的初始化 和 2.2 创建新新节点的方法一样,可以合并调用


LTNode* LTInit_2()
{LTNode* phead = LTCreatNode(-1);return phead;
}

 2.2 链表的创建新节点接口

// 创建新节点
LTNode* LTCreatNode(LTDataType x)
{LTNode* newNode = (LTNode*)malloc(sizeof (LTNode));if (newNode == NULL){perror("malloc fail!");exit(1);}newNode->data = x;newNode->prev = newNode->next = newNode; // 都是 指向自己return newNode;
}

2.3 打印

双链表的打印
和 单链表打印一样,都需要循环,但是结束条件不一样
单链表以 pcur = NULL 为结束条件,双链表是 一种循环链表,头尾相连,不会有  pcur = NULL 的情况
正解:既然 哨兵位无有效数据,同时 循环一轮 还是 回到头(哨兵位),干脆:while (pcur != phead)


void LTPrint(LTNode* phead)
{assert(phead);// 哨兵位不能为空LTNode* pcur = phead->next;while (pcur != phead){printf("%d -> ", pcur->data);pcur = pcur->next;}printf("\n");
}

3. 插入 接口

前言:

// 不需要改变哨兵位,则不需要传二级指针
// 如果需要修改哨兵位的话,则传二级指针

3.1 尾插法

  

双链表的 尾插法
双向链表尾插需不需要找尾的操作 ?

不需要 :ptail = head->prev;  注意这个 等价关系,便于理解下面的代码
尾插法 也称作 在哨兵位之前插入节点/最后一个有效节点之后插入数据

void LTPushBack(LTNode* phead, LTDataType x)
{assert(phead);// 哨兵位不能为空LTNode* newNode = LTCreatNode(x); LTNode* ptail = phead->prev;// 三者关系:phead->prev(即ptail)    newNode      phead// 处理顺序:先 newNode,  再 phead->prev(即ptail) ,最后  phead:否则会乱套// 先 newNodenewNode->next = phead;newNode->prev = ptail; // 就是  phead->prev// 再尾节点 ptail = head->prev  ;   ptail  -> next = head-> prev -> next;ptail->next = newNode;// 最后头节点 phead->prev = newNode; 
}

3.2 头插法

双链表的 头插法
注意:头插 是在第一个有效位置进行插入,不是在哨兵位 前面,由于 双链表的循环成环状的特性,若在哨兵位前面,就是尾插了

 

void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newNode = LTCreatNode(x);// 三者关系:phead   newNode   phead->next (即 第一个 有效节点,命名为Next) ;// 处理顺序:先 newNode,  再 phead->next(即 第一个 有效节点,命名为Next) ,最后  phead:否则会乱套LTNode* Next = phead->next; // 这里就是定义了个变量,便于梳理// 先 newNodenewNode->next = Next;newNode->prev = phead;// 再 phead->next(即 第一个 有效节点) Next->prev = newNode;// 最后头节点 phead->next = newNode;
}

3.3 在 pos 位置之后插入数据

在 pos 位置之后插入数据:要配合   4.查找   接口实现

和 头插法思路一样:只是将 哨兵的带头作用 换成了 pos

pos 是通过  4.查找  的接口找的void LTInsert(LTNode* pos, LTDataType x)
{assert(pos);// 先创建新节点LTNode* newNode = LTCreatNode(x);// 关系三个节点:pos     newNode    pos->next// 先处理newNode,后 pos->next , 最后 pos // 注意三者的执行顺序 不能换!! LTNode* Next = pos->next; // 这里将 pos 的下一个节点(pos->next) 命名成 Next (避免和 next 混淆)newNode->next = Next;newNode->prev = pos;Next->prev = newNode;pos->next = newNode;
}

4. 查找

LTNode* LTFind(LTNode* phead, LTDataType x)
{assert(phead);// 下面是遍历 双链表的模板操作LTNode* pcur = phead->next;while (pcur != phead){if (pcur->data == x){return pcur;}pcur = pcur->next;}return NULL;
}

5.删除  接口

5.1 尾删法

// 思路:让 倒数第二个 节点 next 指向 head ,head 指向 倒数第二个节点,最后 free 掉 ptail
// 尾节点前一个节点:ptail->prev = phead->prev->prev
// 尾节点:ptail = phead->prev
// "尾节点前一个节点" 指向 head哨兵位:ptail->prev->next = phead;
// head哨兵位 指向 "尾节点前一个节点" :phead->prev = ptail->prev;
// 最后 free 掉 ptail

 

// 尾删
void LTPopBack(LTNode* phead)
{assert(phead);//链表为空:只有一个哨兵位节点assert(phead->next != phead);LTNode* ptail = phead->prev;ptail->prev->next = phead;phead->prev = ptail->prev; free(ptail);ptail = NULL;
}

5.2 头删法

    //  被删除的 第一个有效节点:pFirst = phead->next; 

    //  下一个节点 pSecond = pFirst->next;
    //  先 下一个节点 指向 head : pSecond->prev = phead;
    //  后 head 指向 下一个节点 :phead->next = pSecond; 
    //  注意:因为对于循环链表来说, 第一个节点的下个节点 也可以是 哨兵位,所以 只存在一个有效节点,也是可以直接删除第一个节点的、

// 头删
// 删除的是 第一个有效节点
void LTPopFront(LTNode* phead)
{assert(phead); // 地址不能为NULLassert(phead->next != phead); // 链表不能只有 哨兵位LTNode* pFirst = phead->next;  // 要被删除的 第一个节点LTNode* pSecond = pFirst->next;pSecond->prev = phead;phead->next = pSecond;free(pFirst);pFirst = NULL;
}

5.3  删除 pos 位置的数据

删除 pos 位置的数据:要配合   4.查找   接口实现

void LTErase(LTNode* pos)
{assert(pos);// 关系三个节点: pos->prev      pos     pos->next     LTNode* Prev = pos->prev; //  pos 的前一个 LTNode* Next = pos->next; //  pos 的下一个Next->prev = Prev;Prev->next = Next;free(pos);pos = NULL;
}

6. 销毁链表 接口

更推荐 一级指针版:手动置为NULL

为了保持 接口的一致性:不然前面接口都是 一级指针,到这里突然 二级指针,当程序交给用户时,会增加记忆的成本

6.1 二级指针版 

// 思路:先将 有效节点删除,后删除哨兵位
// 删除有效节点, 要将下个节点保存起来,不然找不到
// 注意:这里 最后需要   改变  哨兵位 为NULL ,因而要传递地址,用二级指针接收
// 否则,传值 只会影响 形参,哨兵位 需要手动 置为NULL


void LTDestory(LTNode** pphead) 
{assert(pphead);// 指针本身不能为 空assert(*pphead); // 哨兵位 不能为 空LTNode* pcur = (*pphead)->next; // *pphead = phead 即哨兵位 ;还有记得 加括号while (pcur != *pphead){LTNode* next = pcur->next;free(pcur);pcur = next;}// 最后删除 哨兵位free(*pphead);*pphead = NULL;
}

6.2 一级指针版

// 双链表的 销毁:一级指针版:哨兵位 需要 手动额外 置为空

void LTDestory(LTNode* phead)
{assert(phead);// 哨兵位 不能为 空LTNode* pcur = (phead)->next; // *pphead = phead 即哨兵位 ;还有记得 加括号while (pcur != phead){LTNode* next = pcur->next;free(pcur);pcur = next;}// 最后释放掉 形参的指针:这里不是释放 哨兵位free(phead);phead = NULL;
}

7. 总代码概览

List.h

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>// 创建新节点
typedef int  LTDataType;
typedef struct LTistNode
{LTDataType data;struct LTistNode* prev;struct LTistNode* next;
}LTNode;// 创建新节点
LTNode* LTCreatNode(LTDataType x);// 初始化:生成哨兵位
LTNode* LTInit();// 打印函数
void LTPrint(LTNode* phead);// 尾插法
void LTPushBack(LTNode* phead, LTDataType x);// 头插法
void LTPushFront(LTNode* phead, LTDataType x);// 在 pos 之后插入数据
void LTInsert(LTNode* pos, LTDataType x);// 尾删法
void LTPopBack(LTNode* phead);// 头删法
void LTPopFront(LTNode* phead);// 删除 pos 位置节点
void LTErase(LTNode* pos);// 查找
LTNode* LTFind(LTNode* phead, LTDataType x);// 销毁
void LTDestory(LTNode* phead);

List.c

#include"List.h"// 创建新节点
LTNode* LTCreatNode(LTDataType x)
{LTNode* newNode = (LTNode*)malloc(sizeof(LTNode));if (newNode == NULL){perror("malloc fail!");exit(1);}newNode->data = x;newNode->prev = newNode->next = newNode;return newNode;
}// 初始化:生成哨兵位
LTNode* LTInit()
{LTNode* head = LTCreatNode(-1);return head;
}// 打印函数
void LTPrint(LTNode* phead)
{assert(phead);LTNode* pcur = phead->next;while (pcur != phead){printf("%d -> ", pcur->data);pcur = pcur->next;}printf("\n");
}// 尾插法
void LTPushBack(LTNode* phead, LTDataType x)
{assert(phead);// 创建新节点LTNode* newNode = LTCreatNode(x);LTNode* ptail = phead->prev;// 三者关系:ptail   newNode   pheadnewNode->next = phead;newNode->prev = ptail;ptail->next = newNode;phead->prev = newNode;
}// 头插法
void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);// 创建新节点LTNode* newNode = LTCreatNode(x);// 三者关系:phead   newNode   phead->next (定为Next);LTNode* Next = phead->next; // 这里就是定义了个变量,便于梳理newNode->next = Next;newNode->prev = phead;Next->prev = newNode;phead->next = newNode;
}// 查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{assert(phead);LTNode* pcur = phead->next;while (pcur != phead){if (pcur->data == x) return pcur;pcur = pcur->next;}return NULL;
}// 在 pos 之后插入数据:头插法实现逻辑一样
void LTInsert(LTNode* pos, LTDataType x)
{assert(pos);// 创建新节点LTNode* newNode = LTCreatNode(x);// 三者关系:pos   newNode   pso->next (定为Next);LTNode* Next = pos->next;newNode->next = Next;newNode->prev = pos;Next->prev = newNode;pos->next = newNode;
}// 尾删法:记得双链表的找尾 不像 单链表需要循环去找尾
void LTPopBack(LTNode* phead)
{assert(phead);assert(phead->next != phead);// 链表不能只有 哨兵位 (删除的接口 都要断言这条)// 三者关系:ptail->prev   ptail   headLTNode* ptail = phead->prev;ptail->prev->next = phead;phead->prev = ptail->prev;free(ptail);ptail = NULL;}// 头删法
void LTPopFront(LTNode* phead)
{assert(phead);assert(phead->next != phead); // 链表不能只有 哨兵位 (删除的接口 都要断言这条)// 三者关系:phead    phead->next    phead->next->next(命名为pSecond)LTNode* pFirst = phead->next;  // 要被删除的 第一个节点  一定要先保存下来!!LTNode* pSecond = pFirst->next;pSecond->prev = phead;phead->next = pSecond;free(pFirst);pFirst = NULL;
}// 删除 pos 位置节点
void LTErase(LTNode* pos)
{assert(pos);// 关系三个节点:pos->prev    pos     pos->nextLTNode* Prev = pos->prev; //  pos 的前一个 LTNode* Next = pos->next; //  pos 的下一个Next->prev = Prev;Prev->next = Next;free(pos);pos = NULL;
}// 销毁
void LTDestory(LTNode* phead)
{assert(phead);// 先全部删除有效节点,后删除头节点LTNode* pcur = phead->next;while (pcur != phead){LTNode* Next = pcur->next;free(pcur);pcur = Next;}// 最后释放掉 形参的指针:这里不是释放 哨兵位free(phead);phead = NULL;
}

test.c

#include"List.h"void LTTest()
{// 创建哨兵位LTNode* head = LTInit();  // 这里直接用 head 代表哨兵位:更直观一点,和 前面讲解用的 plist 是一样的// 尾插法LTPushBack(head, 1);LTPushBack(head, 2);LTPushBack(head, 3);LTPushBack(head, 4); // 1 -> 2 -> 3 -> 4 ->printf("测试尾插:");LTPrint(head);// 头插法LTPushFront(head, 5);LTPushFront(head, 6);// 6 -> 5 -> 1 -> 2 -> 3 -> 4 ->printf("测试头插:");LTPrint(head);// 查找//LTFind(head, 1);// 在 pos 之后插入数据:头插法实现逻辑一样LTNode* FindRet1 = LTFind(head, 1);LTInsert(FindRet1, 100);LTNode* FindRet2 = LTFind(head, 2);LTInsert(FindRet2, 200); // 6 -> 5 -> 1 -> 100 -> 2 -> 200 -> 3 -> 4 ->printf("测试pos 之后插入:");LTPrint(head);// 头删法LTPopFront(head);LTPopFront(head); // 1 -> 100 -> 2 -> 200 -> 3 -> 4 ->printf("测试头删:");LTPrint(head);// 尾删法:记得双链表的找尾 不像 单链表需要循环去找尾LTPopBack(head);LTPopBack(head); // 1 -> 100 -> 2 -> 200 ->printf("测试尾删:");LTPrint(head);// 删除 pos 位置节点LTNode* FindRet3 = LTFind(head, 1);LTErase(FindRet3); // 100 -> 2 -> 200 ->printf("测试删除 pos 位置节点:");LTPrint(head);// 双链表的 销毁:这里就不演示销毁了//LTDesTroy(head);//head= NULL;  //哨兵位 需要手动 置为NULL
}
int main()
{LTTest();return 0;
}

三. 顺序表和双向链表的优缺点分析

不同点

顺序表

链表(单链表)

存储空间

物理上⼀定连续

逻辑连续,但物理上不⼀定连续

机访问

支持O(1)

不支持:O(N)

任意位置插或者删除

可能要搬移素,率低O(N)

指针指向

插入

动态顺序表,空间够时要扩容

没有容的概念

应用场景

素高存储+频繁访问

任意位置插和删除频繁

完。

若上述文章有什么错误,欢迎各位大佬及时指出,我们共同进步!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/449796.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

WPS Office18.7软件日常更新

【应用名称】&#xff1a;WPS Office 【适用平台】&#xff1a;#Android 【软件标签】&#xff1a;#WPS 【应用版本】&#xff1a;18.6.1➡18.7 【应用大小】&#xff1a;160MB 【软件说明】&#xff1a;软件日常更新。WPS Office是使用人数最多的移动办公软件。独有手机阅读模…

Blender教程(基础)-面的法向-12

一、准备 新建如下图所示立方体演示面的法向 默认法向方向 二、显示法向 再菜单栏右上角、找到网络编辑模式&#xff0c;最下面的显示发法线打勾&#xff0c;如下图所示&#xff0c;出现的浅蓝色线条就是代表法向方向。 调整大小显示 三、正面 再显示叠加层菜单下找到面…

day38 斐波那契数 爬楼梯 使用最小花费爬楼梯

题目1&#xff1a;509 斐波那契数 题目链接&#xff1a;509 斐波那契数 题意 斐波那契数列由0和1开始 后面的每一项数字都是前面两项数字之和 计算F(n) 动态规划 动规五部曲&#xff1a; 1&#xff09;dp数组及下标i的含义 dp[i] : 第i个斐波那契数值 i: 第i个斐…

和鲸科技与智谱AI达成合作,共建大模型生态基座

近日&#xff0c;上海和今信息科技有限公司&#xff08;简称“和鲸科技”&#xff09;与北京智谱华章科技有限公司&#xff08;简称“智谱AI”&#xff09;签订合作协议&#xff0c;双方将携手推动国产通用大模型的广泛应用与行业渗透&#xff0c;并积极赋能行业伙伴探索领域大…

K8s 集群可观测性-数据分流最佳实践

简介 在微服务架构下&#xff0c;一个 k8s 集群中经常会部署多套业务&#xff0c;同时也意味着不同团队、不同角色、不同的业务会在同一集群中&#xff0c;需要将不同业务的数据在不同的空间进行管理和查看。 在传统的主机环境下&#xff0c;这个是可以通过不同的主机部署 Da…

springboot集成rocketmq-spring-boot-starter的坑(避坑指南)

1.说明版本&#xff08;解决方法&#xff09; springboot版本&#xff1a;2.2.2.RELEASE RocketMQ版本&#xff1a;rocketmq-spring-boot-starter 2.2.2 2.坑 rocketmq-spring-boot-starter的版本一开始&#xff0c;使用的是2.2.0版本&#xff0c;一直出现一个问题&#x…

【MybatisPlus篇】查询条件设置(范围匹配 | 模糊匹配 | 空判定 | 包含性判定 | 分组 | 排序)

文章目录 &#x1f384;环境准备⭐导入依赖⭐写入User类⭐配置启动类⭐创建UserDao 的 MyBatis Mapper 接口&#xff0c;用于定义数据库访问操作⭐创建配置文件&#x1f6f8;创建测试类MpATest.java &#x1f354;范围查询⭐eq⭐between⭐gt &#x1f354;模糊匹配⭐like &…

11 插入排序和希尔排序

1. 插入排序 基本思想 直接插入排序是一种简单的插入排序法&#xff0c;基本思想&#xff1a; 把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中&#xff0c;直到所有的记录插入完为止&#xff0c;得到一个新的有序序列 在玩扑克牌时&#xff0c;就用…

【开源】JAVA+Vue+SpringBoot实现学生综合素质评价系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 学生功能2.2 教师功能2.3 教务处功能 三、系统展示四、核心代码4.1 查询我的学科竞赛4.2 保存单个问卷4.3 根据类型查询学生问卷4.4 填写语数外评价4.5 填写品德自评问卷分 五、免责说明 一、摘要 1.1 项目介绍 基于J…

vite, vue3, vue-router, vuex, ES6学习日记

学习使用vitevue3的所遇问题总结&#xff08;2024年2月1日&#xff09; 组件中使用<script>标签忘记加 setup 这会导致Navbar 没有暴露出来&#xff0c;导致使用不了&#xff0c;出现以下报错 这是因为&#xff0c;如果不用setup&#xff0c;就得使用 export default…

Flutter开发2:安装Flutter

在本篇博客中&#xff0c;我们将详细介绍如何安装Flutter开发环境。安装Flutter是开始使用Flutter进行跨平台移动应用开发的第一步。让我们开始吧&#xff01; 官方安装文档 步骤1&#xff1a;下载Flutter SDK 打开浏览器&#xff0c;访问Flutter官方网站&#xff1a;https://…

Java面向对象的三大特性

目录 封装 封装的好处&#xff1a; 权限修饰符 this 关键字 继承 使用继承的好处 什么时候用继承 ​编辑 继承的特点 注意点 super关键字 多态 多态的好处? 多态调用成员的特点 多态的弊端 封装 封装告诉我们&#xff0c;如何正确设计对象的属性和方法 简单来说…