网站尾部一般怎么做,网站正在建设中色综合,资源网站建设活动感受,今天开始做魔王免费观看网站文章目录 从零实现 list 容器#xff1a;细粒度剖析与代码实现前言1. list 的核心数据结构1.1节点结构分析#xff1a; 2. 迭代器设计与实现2.1 为什么 list 需要迭代器#xff1f;2.2 实现一个简单的迭代器2.2.1 迭代器代码实现#xff1a;2.2.2 解释#xff1a; 2.3 测试… 文章目录 从零实现 list 容器细粒度剖析与代码实现前言1. list 的核心数据结构1.1节点结构分析 2. 迭代器设计与实现2.1 为什么 list 需要迭代器2.2 实现一个简单的迭代器2.2.1 迭代器代码实现2.2.2 解释 2.3 测试简单迭代器2.3.1 测试代码2.3.2 输出2.3.3 解释 2.4 增加后向移动和 - 运算符2.4.1关键点2.4.2 增加后向移动和 - 运算符的实现代码 2.5 测试前后移动和 - 运算符2.5.1 目的2.5.2 测试代码2.5.3 输出2.5.4 解释 2.6 为什么不能简单使用 const 修饰2.6.1 问题解释2.6.2 为什么不能简单使用 const 修饰2.6.3 错误示例直接使用 const 修饰2.6.4 错误代码2.6.5 错误分析 2.7 正确的解决方案使用模板参数区分 const 和 non-const2.7.1 使用模板参数的好处2.7.2 实现代码 2.8 测试模板泛化后的迭代器2.8.1 测试代码2.8.2 输出结果2.8.3 解释 3. list 容器的基本操作3.1 构造函数3.2 构造函数分析 4. 插入与删除操作4.1 插入操作4.1.1 插入操作分析 4.2 删除操作4.2.1 删除操作分析 5. 反向迭代器的设计5.1 反向迭代器分析 6. 迭代器失效问题6.1 删除操作中的迭代器失效6.2 错误使用示例6.3 解决方案 7. 总结与展望 完整的 list 容器实现代码 从零实现 list 容器细粒度剖析与代码实现
接上篇【C篇】深度剖析C STL玩转 list 容器解锁高效编程的秘密武器 欢迎讨论学习过程中有问题吗随时在评论区与我交流。你们的互动是我创作的动力 支持我如果你觉得这篇文章对你有帮助请点赞、收藏并分享给更多朋友吧 一起成长欢迎分享给更多对计算机视觉和图像处理感兴趣的小伙伴让我们共同进步 本文详细介绍如何从零开始实现一个 C list 容器帮助读者深入理解 list 的底层实现包括核心数据结构、迭代器的设计、以及常见的插入、删除等操作。从初学者到进阶开发者都能从中受益。 前言
在 C 标准模板库 (STL) 中list 是一种双向链表容器适合频繁的插入和删除操作。它与 vector 的主要区别在于 list 不支持随机访问并且在进行插入、删除操作时无需移动其他元素。这使得 list 在某些需要大量动态修改元素的场景下具有独特优势例如链表的插入删除操作具有 O(1) 的时间复杂度。
为了更好地理解 list 的工作原理本文将从零开始实现一个简化版的 list并详细分析每个步骤背后的实现原理及其易错点。 1. list 的核心数据结构 在 list 的实现中底层是通过双向链表结构来存储数据。双向链表中的每个节点不仅包含数据还包含指向前一个节点和后一个节点的两个指针。以下是节点结构的定义 namespace W {// 定义链表节点templateclass Tstruct ListNode {T _val; // 节点存储的值ListNode* _prev; // 指向前一个节点ListNode* _next; // 指向后一个节点ListNode(const T val T()) : _val(val), _prev(nullptr), _next(nullptr) {}};
}1.1节点结构分析
_val存储节点的数据。_prev 和 _next分别指向前一个节点和后一个节点便于实现双向链表的遍历、插入和删除操作。
该结构简单明了双向链表的节点可以方便地进行前向和后向操作。接下来我们将实现如何使用该结构构建一个完整的 list 容器。 2. 迭代器设计与实现
2.1 为什么 list 需要迭代器 在 C 中vector 是一种动态数组元素在内存中是连续存储的因此我们可以使用下标快速访问元素例如 vec[0] 可以直接访问 vector 的第一个元素。而 list 底层是通过链表结构实现的每个节点在内存中的位置并不连续。因此链表无法像数组一样通过下标随机访问元素。每个节点都通过指针链接到前一个节点_prev和后一个节点_next。为了遍历链表我们需要使用迭代器。 迭代器的作用类似于一个指针它指向链表中的某个节点允许我们通过类似指针的方式来访问和操作链表节点。与普通指针不同迭代器提供了更高级的功能并且能够保持接口的一致性因此它成为了 STL 容器中访问元素的核心工具。 2.2 实现一个简单的迭代器
为了实现最基本的链表迭代器首先我们需要定义链表节点的结构。该结构已经在上文定义了。
接下来我们将实现 ListIterator它内部保存一个指向 ListNode 的指针 _node并支持以下基本操作
解引用操作通过 *it 访问链表节点中的值。前向移动操作通过 it 访问链表中的下一个节点。比较操作通过 it ! end() 判断两个迭代器是否相等。
2.2.1 迭代器代码实现
namespace W {templateclass Tclass ListIterator {typedef ListNodeT Node; // 使用 Node 表示链表节点类型public:// 构造函数接受一个指向链表节点的指针ListIterator(Node* node nullptr) : _node(node) {}// 解引用操作返回节点的值T operator*() { return _node-_val; }// 前向移动操作指向下一个节点ListIterator operator() {_node _node-_next; // 将当前节点移动到下一个节点return *this; // 返回自身以支持链式调用}// 比较操作判断两个迭代器是否相等bool operator!(const ListIterator other) const { return _node ! other._node; }private:Node* _node; // 迭代器指向的链表节点};
}2.2.2 解释
构造函数初始化一个指向链表节点的指针 _node用于遍历链表。operator*返回节点存储的值 _val。operator将迭代器移动到链表中的下一个节点。operator!用于判断两个迭代器是否相等。 2.3 测试简单迭代器
为了验证我们刚刚实现的迭代器功能先创建一些链表节点并将它们链接成一个链表。然后我们使用迭代器遍历链表并输出每个节点的值。
2.3.1 测试代码
#include iostreamint main() {// 创建三个节点分别存储值 1、2、3W::ListNodeint node1(1); W::ListNodeint node2(2); W::ListNodeint node3(3); // 链接节点形成链表node1._next node2; // node1 的下一个节点是 node2node2._prev node1; // node2 的前一个节点是 node1node2._next node3; // node2 的下一个节点是 node3node3._prev node2; // node3 的前一个节点是 node2// 创建迭代器指向第一个节点W::ListIteratorint it(node1);// 使用迭代器遍历链表并输出每个节点的值while (it ! nullptr) {std::cout *it std::endl; // 输出当前节点的值it; // 前向移动到下一个节点}return 0;
}2.3.2 输出
1
2
32.3.3 解释
迭代器 it 初始指向第一个节点 node1。通过 *it 获取节点的值并通过 it 将迭代器移动到下一个节点直到链表末尾。 2.4 增加后向移动和 - 运算符 目前的迭代器只能进行前向移动而 list 是双向链表因此我们还需要增加后向移动 (--) 的功能使迭代器可以从链表末尾向前遍历。同时为了让迭代器像指针一样操作我们还需要重载 - 运算符以便可以通过 - 访问链表节点的成员。 2.4.1关键点 当 _val 是基本数据类型如 int时可以直接通过 *it 来获取节点的值而不需要使用 *(it-)。虽然 *(it-) 语法上是正确的但显得繁琐且不必要。 为什么 *(it-) 是正确的 因为 it- 是在调用 operator-()返回 _val 的指针然后 *(it-) 解引用该指针。语法上是没有问题的但通常我们直接使用 *it 更简洁。 当 _val 是自定义类型时可以使用 it-x 直接访问自定义类型的成员变量 x。编译器会将 it-x 优化为 it.operator-()-x让访问更加方便。
2.4.2 增加后向移动和 - 运算符的实现代码
namespace W {templateclass Tclass ListIterator {typedef ListNodeT Node;public:ListIterator(Node* node nullptr) : _node(node) {}// 解引用操作返回节点的值T operator*() { return _node-_val; }// 指针操作返回节点的指针T* operator-() { return (_node-_val); }// 前向移动ListIterator operator() {_node _node-_next;return *this;}// 后向移动ListIterator operator--() {_node _node-_prev;return *this;}// 比较操作bool operator!(const ListIterator other) const { return _node ! other._node; }private:Node* _node;};
}2.5 测试前后移动和 - 运算符
2.5.1 目的 我们通过一个测试程序验证迭代器的前向和后向移动功能同时通过 - 运算符访问链表节点的值。我们会分别测试基本数据类型 int 和自定义类型 CustomType 的场景展示迭代器在不同数据类型下的使用方式。 2.5.2 测试代码 对于 int 类型我们可以通过 *it 来访问节点的值而不需要使用 *(it-)虽然 *(it-) 也是合法的但没有必要。 对于自定义类型 CustomType可以通过 it-x 来访问自定义类型 CustomType 中的成员变量 x。
#include iostreamstruct CustomType {int x;
};int main() {// 创建三个 int 类型的节点分别存储值 1、2、3W::ListNodeint node1(1); W::ListNodeint node2(2); W::ListNodeint node3(3); // 链接节点形成链表node1._next node2;node2._prev node1;node2._next node3;node3._prev node2;// 创建迭代器初始指向第二个节点W::ListIteratorint it(node2);// 对于 int 类型使用 *it 访问节点的值std::cout *it std::endl; // 输出 2// 后向移动指向第一个节点--it;std::cout *it std::endl; // 输出 1// 前向移动指向第三个节点it;it;std::cout *it std::endl; // 输出 3// 创建自定义类型 CustomType 的节点W::ListNodeCustomType customNode1({1});W::ListNodeCustomType customNode2({2});customNode1._next customNode2;customNode2._prev customNode1;// 创建自定义类型 CustomType 的迭代器W::ListIteratorCustomType customIt(customNode1);// 使用 it- 访问 CustomType 的成员变量 xstd::cout customIt-x std::endl; // 输出 1return 0;
}2.5.3 输出
2
1
3
12.5.4 解释
对于 int 类型的节点我们通过 *it 访问节点的值it 和 --it 分别实现了前向和后向移动。对于自定义类型 CustomType 的节点通过 it-x 可以访问自定义类型成员变量 x。编译器会将 it-x 优化为 it.operator-()-x使得操作简化。 2.6 为什么不能简单使用 const 修饰
2.6.1 问题解释
在 vector 中const_iterator 通过 const 修饰符即可实现不可修改的迭代器这是因为 vector 的底层存储是连续的内存块通过 const 限制访问的值即可。而 list 的底层是双向链表迭代器不仅需要访问链表节点的值还需要操作链表的前驱和后继节点即 _prev 和 _next 指针。直接使用 const 修饰迭代器无法满足这些需求因为 const 限制了对链表结构的必要修改。
2.6.2 为什么不能简单使用 const 修饰
const 修饰的迭代器会限制所有成员的修改包括迭代器内部的 _node 指针。如果我们对 const 迭代器执行 或 -- 操作这些操作会修改 _node而 const 禁止这种修改。
2.6.3 错误示例直接使用 const 修饰
下面是一个简单的错误示例展示了为什么简单地使用 const 修饰符会导致问题
2.6.4 错误代码
#include iostreamtemplateclass T
struct ListNode {T _val;ListNode* _prev;ListNode* _next;ListNode(T val) : _val(val), _prev(nullptr), _next(nullptr) {}
};templateclass T
class ListIterator {typedef ListNodeT Node;public:ListIterator(Node* node nullptr) : _node(node) {}// 解引用操作返回节点的值T operator*() { return _node-_val; }// 前向移动ListIterator operator() {_node _node-_next;return *this;}// 后向移动ListIterator operator--() {_node _node-_prev;return *this;}private:Node* _node;
};int main() {// 创建三个节点分别存储值 1、2、3ListNodeint node1(1), node2(2), node3(3);// 链接节点形成链表node1._next node2;node2._prev node1;node2._next node3;node3._prev node2;// 尝试创建一个 const 迭代器const ListIteratorint constIt(node1);// 错误1前向移动时编译器报错因为 操作符不能对 const 迭代器操作constIt; // 编译错误// 错误2解引用操作也无法进行修改*constIt 5; // 编译错误
}2.6.5 错误分析 无法执行前向移动 (constIt)由于 const 修饰符限制了修改成员变量 _node因此 操作无法执行因为该操作会修改迭代器的内部指针。 无法修改节点的值 (*constIt 5)由于迭代器是 const 的解引用操作也不能用于修改节点的值因此编译器会报错。 2.7 正确的解决方案使用模板参数区分 const 和 non-const
为了处理上述问题我们可以使用模板参数来区分 const 和 non-const 的情况。通过模板参数 Ref 和 Ptr我们可以控制迭代器的行为使得它在常量链表和非常量链表中都能正常工作。
2.7.1 使用模板参数的好处
灵活性可以根据需要处理 const 和 non-const 的迭代器场景。安全性对于常量链表保证不能修改节点的值对于非常量链表允许修改。代码复用通过模板参数既可以编写一套代码处理 const 和 non-const 两种情况。
2.7.2 实现代码
namespace W {templateclass T, class Ref, class Ptrclass ListIterator {typedef ListNodeT Node; // 使用 Node 表示链表节点类型public:ListIterator(Node* node nullptr) : _node(node) {}// 解引用操作返回节点的值Ref operator*() const { return _node-_val; }// 指针操作返回节点的值的指针Ptr operator-() const { return _node-_val; }// 前向移动ListIterator operator() {_node _node-_next;return *this;}// 后向移动ListIterator operator--() {_node _node-_prev;return *this;}// 比较操作判断两个迭代器是否相等bool operator!(const ListIterator other) const { return _node ! other._node; }private:Node* _node;};
}2.8 测试模板泛化后的迭代器
现在我们通过测试来验证模板参数 Ref 和 Ptr 的设计是否能够正确处理常量链表和非常量链表的迭代器情况。以下场景将会被测试
非常量链表迭代器允许修改节点的值。常量链表const 迭代器只能读取节点值不能修改。
2.8.1 测试代码
#include iostreamstruct CustomType {int x;
};int main() {// 创建三个 int 类型的节点分别存储值 1、2、3W::ListNodeint node1(1); W::ListNodeint node2(2); W::ListNodeint node3(3); // 链接节点形成链表node1._next node2;node2._prev node1;node2._next node3;node3._prev node2;// 创建一个非常量迭代器W::ListIteratorint, int, int* it(node1);std::cout *it std::endl; // 输出 1it; // 前向移动std::cout *it std::endl; // 输出 2// 修改节点的值*it 20;std::cout *it std::endl; // 输出 20// 创建一个常量链表节点const W::ListNodeint constNode1(1);const W::ListNodeint constNode2(2);constNode1._next constNode2;// 创建一个常量迭代器W::ListIteratorint, const int, const int* constIt(constNode1);std::cout *constIt std::endl; // 输出 1// 常量迭代器不允许修改值// *constIt 10; // 错误无法修改常量链表节点的值return 0;
}2.8.2 输出结果
1
2
20
12.8.3 解释
非常量链表 使用 it 迭代器遍历链表前向移动并修改节点的值。*it 20 修改了第二个节点的值。 常量链表 使用 constIt 迭代器只能读取节点的值无法修改。如果尝试 *constIt 10编译器会报错禁止修改。 3. list 容器的基本操作
3.1 构造函数
我们将实现多种构造函数允许用户创建空链表、指定大小的链表以及从迭代器区间构造链表。
namespace W {templateclass Tclass list {typedef ListNodeT Node;public:typedef ListIteratorT, T, T* iterator;// 默认构造函数list() { CreateHead(); }// 指定大小的构造函数list(size_t n, const T val T()) {CreateHead();for (size_t i 0; i n; i)push_back(val);}// 迭代器区间构造函数templateclass Iteratorlist(Iterator first, Iterator last) {CreateHead();while (first ! last) {push_back(*first);first;}}// 析构函数~list() {clear();delete _head;}// 头节点初始化void CreateHead() {_head new Node();_head-_next _head;_head-_prev _head;}// 清空链表void clear() {Node* cur _head-_next;while (cur ! _head) {Node* next cur-_next;delete cur;cur next;}_head-_next _head;_head-_prev _head;}private:Node* _head; // 指向头节点的指针};
}3.2 构造函数分析
默认构造函数创建一个空链表并初始化头节点。指定大小构造函数使用 push_back 向链表中插入 n 个值为 val 的节点。迭代器区间构造函数通过一对迭代器 [first, last) 形成的区间构造链表。 4. 插入与删除操作
list 容器的优势在于高效的插入与删除操作。我们将在指定位置插入节点或删除指定节点插入和删除的时间复杂度均为 O(1)。
4.1 插入操作
namespace W {templateclass Tclass list {typedef ListNodeT Node;typedef ListIteratorT, T, T* iterator;public:// 在指定位置前插入新节点iterator insert(iterator pos, const T val) {Node* newNode new Node(val);Node* cur pos._node;newNode-_next cur;newNode-_prev cur-_prev;cur-_prev-_next newNode;cur-_prev newNode;return iterator(newNode);}// 在链表末尾插入新节点void push_back(const T val) { insert(end(), val); }// 在链表头部插入新节点void push_front(const T val) { insert(begin(), val); }};
}4.1.1 插入操作分析
插入效率由于链表的结构插入操作只需调整节点的指针不涉及大规模的内存移动时间复杂度为 O(1)。头尾插入通过 push_back 和 push_front 可以方便地在链表的头部和尾部插入新节点。 4.2 删除操作
namespace W {templateclass Tclass list {typedef ListNodeT Node;typedef ListIteratorT, T, T* iterator;public:// 删除指定位置的节点iterator erase(iterator pos) {Node* cur pos._node;Node* nextNode cur-_next;cur-_prev-_next cur-_next;cur-_next-_prev cur-_prev;delete cur;return iterator(nextNode);}// 删除链表头部节点void pop_front() { erase(begin()); }// 删除链表尾部节点void pop_back() { erase(--end()); }};
}4.2.1 删除操作分析
删除效率删除节点同样是通过调整指针实现时间复杂度为 O(1)。头尾删除通过 pop_front 和 pop_back 实现头部和尾部节点的删除。 5. 反向迭代器的设计
在双向链表中反向迭代器可以通过包装普通迭代器实现。反向迭代器的 对应正向迭代器的 --反之亦然。
namespace W {templateclass Iteratorclass ReverseListIterator {Iterator _it;public:ReverseListIterator(Iterator it) : _it(it) {}auto operator*() { Iterator temp _it; --temp; return *temp; }auto operator-() { return (operator*()); }ReverseListIterator operator() { --_it; return *this; }ReverseListIterator operator(int) { ReverseListIterator temp *this; --_it; return temp; }ReverseListIterator operator--() { _it; return *this; }ReverseListIterator operator--(int) { ReverseListIterator temp *this; _it; return temp; }bool operator(const ReverseListIterator other) const { return _it other._it; }bool operator!(const ReverseListIterator other) const { return !(*this other); }};
}5.1 反向迭代器分析
解引用和指针操作反向迭代器的 operator* 和 operator- 实际上是操作前一个节点。前向和后向移动反向迭代器的 操作是通过调用普通迭代器的 -- 来实现的。 6. 迭代器失效问题
在操作 list 容器时特别是在删除节点的过程中可能会出现迭代器失效问题。迭代器失效是指当某个节点被删除后指向该节点的迭代器变得无效继续使用这个迭代器将导致未定义行为。因此在删除节点后必须使用返回的迭代器进行下一步操作以避免迭代器失效问题。
6.1 删除操作中的迭代器失效
假设我们使用 erase 函数删除链表中的节点。如果我们继续使用之前的迭代器而不更新它程序将会崩溃因为该迭代器指向的内存已经被释放。
void TestIteratorInvalidation() {W::listint lst {1, 2, 3, 4, 5};auto it lst.begin();while (it ! lst.end()) {it lst.erase(it); // 正确使用 erase 返回的新迭代器}
}6.2 错误使用示例
下面的示例展示了错误的迭代器使用方式迭代器在删除操作后没有更新导致其指向了已被释放的内存。
void WrongIteratorUsage() {W::listint lst {1, 2, 3, 4, 5};auto it lst.begin();while (it ! lst.end()) {lst.erase(it); // 错误删除后 it 失效it; // 未更新的迭代器继续操作导致崩溃}
}6.3 解决方案
为了解决迭代器失效问题每次删除节点后都要使用 erase 返回的新迭代器确保迭代器指向的内存有效。
void CorrectIteratorUsage() {W::listint lst {1, 2, 3, 4, 5};auto it lst.begin();while (it ! lst.end()) {it lst.erase(it); // 正确每次使用 erase 返回的新迭代器}
}7. 总结与展望
通过这篇文章我们从零开始模拟实现了一个 list 容器并深入剖析了以下几个方面
双向链表的核心数据结构理解链表节点的 _prev 和 _next 指针以及如何通过它们实现双向遍历。迭代器的设计实现了 list 的正向和反向迭代器支持前向移动、后向移动和解引用操作。模板参数解决 const 和 non-const 场景通过模板参数 Ref 和 Ptr灵活应对 const 链表和非常量链表的不同需求保证代码的安全性和灵活性。插入与删除操作高效的插入和删除操作时间复杂度均为 O(1)体现了链表结构的优势。反向迭代器的实现通过包装普通迭代器设计了一个反向迭代器方便反向遍历链表。迭代器失效问题讲解了迭代器失效的原因及其解决方法避免了未定义行为。
今后读者您可以尝试进一步扩展这篇文章中的 list 容器例如
实现更多的容器操作如 find、sort 等高级操作。实现与 STL 接口兼容的完整 list 容器包括迭代器失效的处理、异常安全的插入与删除操作。性能优化与内存管理如使用自定义的内存池优化链表的节点分配和释放。
通过持续的实践和优化我们能够更深入地理解 C 标准库的实现细节并在开发过程中提高代码的效率和健壮性。 完整的 list 容器实现代码
最后附上完整的代码实现包括链表节点结构、迭代器、插入删除操作等。
namespace W {// 链表节点结构templateclass Tstruct ListNode {T _val;ListNode* _prev;ListNode* _next;ListNode(const T val T()) : _val(val), _prev(nullptr), _next(nullptr) {}};// 正向迭代器templateclass T, class Ref, class Ptrclass ListIterator {typedef ListNodeT Node;public:ListIterator(Node* node nullptr) : _node(node) {}Ref operator*() const { return _node-_val; }Ptr operator-() const { return _node-_val; }ListIterator operator() {_node _node-_next;return *this;}ListIterator operator--() {_node _node-_prev;return *this;}bool operator!(const ListIterator other) const { return _node ! other._node; }private:Node* _node;};// 反向迭代器templateclass Iteratorclass ReverseListIterator {Iterator _it;public:ReverseListIterator(Iterator it) : _it(it) {}auto operator*() { Iterator temp _it; --temp; return *temp; }auto operator-() { return (operator*()); }ReverseListIterator operator() { --_it; return *this; }ReverseListIterator operator(int) { ReverseListIterator temp *this; --_it; return temp; }ReverseListIterator operator--() { _it; return *this; }ReverseListIterator operator--(int) { ReverseListIterator temp *this; _it; return temp; }bool operator(const ReverseListIterator other) const { return _it other._it; }bool operator!(const ReverseListIterator other) const { return !(*this other); }};// list 容器实现templateclass Tclass list {typedef ListNodeT Node;typedef ListIteratorT, T, T* iterator;public:list() { CreateHead(); }list(size_t n, const T val T()) {CreateHead();for (size_t i 0; i n; i)push_back(val);}~list() {clear();delete _head;}iterator begin() { return iterator(_head-_next); }iterator end() { return iterator(_head); }void push_back(const T val) { insert(end(), val); }void push_front(const T val) { insert(begin(), val); }iterator insert(iterator pos, const T val) {Node* newNode new Node(val);Node* cur pos._node;newNode-_next cur;newNode-_prev cur-_prev;cur-_prev-_next newNode;cur-_prev newNode;return iterator(newNode);}iterator erase(iterator pos) {Node* cur pos._node;Node* nextNode cur-_next;cur-_prev-_next cur-_next;cur-_next-_prev cur-_prev;delete cur;return iterator(nextNode);}void pop_front() { erase(begin()); }void pop_back() { erase(--end()); }void clear() {Node* cur _head-_next;while (cur ! _head) {Node* next cur-_next;delete cur;cur next;}_head-_next _head;_head-_prev _head;}private:void CreateHead() {_head new Node();_head-_next _head;_head-_prev _head;}Node* _head;};
}以上就是关于【C篇】揭开 C STL list 容器的神秘面纱从底层设计到高效应用的全景解析的内容啦各位大佬有什么问题欢迎在评论区指正或者私信我也是可以的啦您的支持是我创作的最大动力❤️