【C++篇】从零实现 `list` 容器:细粒度剖析与代码实现

文章目录

  • 从零实现 `list` 容器:细粒度剖析与代码实现
      • 前言
      • 1. `list` 的核心数据结构
        • 节点结构分析
      • 2 迭代器设计与实现
        • 2.1 为什么 `list` 需要迭代器?
        • 2.2 实现一个简单的迭代器
        • 2.3 测试简单迭代器
          • 解释:
        • 2.4 增加后向移动和 `->` 运算符
          • 关键点:
          • 实现代码:
        • 2.5 测试前后移动和 `->` 运算符
          • 目的:
          • 测试代码:
          • 输出:
        • 2.6 为什么不能简单使用 `const` 修饰?
          • 问题解释:
          • 直接使用 `const` 修饰的限制:
          • 错误示例:直接使用 `const` 修饰
          • 错误代码:
          • 错误分析:
        • 2.6 正确解决方案:使用模板参数区分 `const` 和 `non-const`
          • 为什么需要模板参数?
          • 使用模板参数的好处:
          • 正确的模板泛化代码实现:
          • 关键点解释:
        • 2.7 测试泛化后的迭代器
          • 测试场景:
          • 测试代码:
          • 输出:
        • 2.8 迭代器设计分析
      • 3. `list` 容器的基本操作
        • 3.1 构造函数
        • 构造函数分析
      • 4. 插入与删除操作
        • 4.1 插入操作
        • 插入操作分析
        • 4.2 删除操作
        • 删除操作分析
      • 5. 反向迭代器
      • 6. 迭代器失效问题
      • 结论

从零实现 list 容器:细粒度剖析与代码实现

接上篇【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器

💬 欢迎讨论:学习过程中有问题吗?随时在评论区与我交流。你们的互动是我创作的动力!

👍 支持我:如果你觉得这篇文章对你有帮助,请点赞、收藏并分享给更多朋友吧!
🚀 一起成长:欢迎分享给更多对计算机视觉和图像处理感兴趣的小伙伴,让我们共同进步!

本文详细介绍如何从零开始实现一个 C++ list 容器,帮助读者深入理解 list 的底层实现,包括核心数据结构、迭代器的设计、以及常见的插入、删除等操作。从初学者到进阶开发者都能从中受益。

前言

在 C++ 标准模板库 (STL) 中,list 是一种双向链表容器,适合频繁插入和删除操作。它与 vector 的主要区别在于不支持随机访问,且插入、删除时无需移动其他元素。这使得 list 在某些场景下具有独特优势,例如大量元素的动态操作。

为了更好地理解 list 的工作原理,我们将在本篇博客中模拟实现一个简单版的 list,同时分析每个步骤背后的原理及其易错点。


1. list 的核心数据结构

我们首先要理解 list 的底层是由双向链表实现的。双向链表中的每个节点不仅包含数据,还包含两个指针,分别指向前一个节点和后一个节点。以下是节点结构的定义:

namespace W {// 定义链表节点template<class T>struct ListNode {T _val;               // 节点存储的值ListNode* _prev;      // 指向前一个节点ListNode* _next;      // 指向后一个节点ListNode(const T& val = T()) : _val(val), _prev(nullptr), _next(nullptr) {}};
}
节点结构分析
  1. _val:存储节点的值。
  2. _prev_next:分别指向前后节点,便于在链表中进行前后遍历和插入、删除操作。

2 迭代器设计与实现

2.1 为什么 list 需要迭代器?

在 C++ 中,vector 是一种动态数组,元素在内存中是连续存储的,因此我们可以使用下标快速访问元素。例如,vec[0] 就可以直接访问到 vector 的第一个元素。这是因为 vector 中的每个元素都可以通过下标计算出准确的内存地址。

然而,list 的底层是链表结构,链表节点在内存中不是连续存放的。因此,链表不能像数组那样通过下标随机访问元素。每个节点都通过指针链接到前一个节点(_prev)和后一个节点(_next)。这意味着,如果你想访问链表中的某个节点,必须从链表的起始位置开始,一个节点一个节点地遍历。这时我们就需要迭代器来方便地遍历链表。

迭代器的作用类似于一个指针,它指向链表中的某个节点,并允许我们通过类似指针的方式访问和操作链表节点。与普通指针不同,迭代器提供了更高级的功能,并且能够保持更好的接口一致性,因此它成为了 STL 容器中访问元素的核心工具。


2.2 实现一个简单的迭代器

为了实现最基本的链表迭代器,首先我们需要定义链表节点的结构。

namespace W {// 链表节点定义template<class T>struct ListNode {T _val;               // 节点中存储的值ListNode* _prev;      // 指向前一个节点ListNode* _next;      // 指向后一个节点ListNode(const T& val = T()) : _val(val), _prev(nullptr), _next(nullptr) {}};
}

我们可以看到,ListNode 是一个模板结构,它包含:

  • _val:存储链表节点的值。
  • _prev:指向链表中前一个节点的指针。
  • _next:指向链表中下一个节点的指针。

接下来,我们定义一个最基本的迭代器 ListIterator。它内部保存了一个指向 ListNode 的指针 _node,并且支持以下基本操作:

  1. 解引用:通过 *it 访问节点中的值。
  2. 前向移动:通过 ++it 访问链表中的下一个节点。
  3. 比较:通过 it != end() 判断两个迭代器是否相等。
namespace W {template<class T>class ListIterator {typedef ListNode<T> 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.3 测试简单迭代器

为了测试这个简单的迭代器,我们先创建几个链表节点,并将它们相互连接,形成一个链表。接着使用迭代器遍历链表,输出每个节点的值。

#include <iostream>int main() {// 创建三个节点,分别存储值 1、2、3W::ListNode<int> node1(1);      W::ListNode<int> node2(2);      W::ListNode<int> node3(3);      // 链接节点形成链表node1._next = &node2;  // node1 的下一个节点是 node2node2._prev = &node1;  // node2 的前一个节点是 node1node2._next = &node3;  // node2 的下一个节点是 node3node3._prev = &node2;  // node3 的前一个节点是 node2// 创建迭代器,指向第一个节点W::ListIterator<int> it(&node1);// 使用迭代器遍历链表并输出每个节点的值while (it != nullptr) {std::cout << *it << std::endl;  // 输出当前节点的值++it;  // 前向移动到下一个节点}return 0;
}

输出:

1
2
3
解释:
  • it 初始指向第一个节点 node1
  • 每次 *it 解引用获取当前节点的值,++it 将迭代器移动到链表中的下一个节点,直到链表结束。

2.4 增加后向移动和 -> 运算符

我们之前实现的迭代器只能向前移动。然而,list双向链表,因此我们还需要增加后向移动 -- 的功能,以便迭代器可以从链表的末尾向前遍历。同时,为了让迭代器像指针一样工作,我们还要重载 -> 运算符,以支持通过 -> 访问节点的成员变量。

关键点:
  1. _val 是基本数据类型(如 int)时,可以直接通过 *it 来获取节点的值,而不需要使用 *(it->)。虽然 *(it->) 语法上是正确的,但显得繁琐且不必要。

    为什么 *(it->) 是正确的?
    因为 it-> 是在调用 operator->(),返回 _val 的指针,然后 *(it->) 解引用该指针。语法上是没有问题的,但通常我们直接使用 *it 更简洁。

  2. _val 是自定义类型时,可以使用 it->x 直接访问自定义类型的成员变量 x。编译器会将 it->x 优化为 it.operator->()->x,让访问更加方便。

实现代码:
namespace W {template<class T>class ListIterator {typedef ListNode<T> Node;  // 使用 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 测试前后移动和 -> 运算符
目的:

通过一个测试程序验证迭代器的前后移动功能,并使用 -> 运算符访问节点的值。我们将测试基本数据类型和自定义类型的情况,展示如何在不同数据类型下使用迭代器。

测试代码:
  1. 对于 int 类型,可以直接使用 *it 访问节点的值,而不需要使用 *(it->)。虽然 *(it->) 语法上是正确的,但没有必要,因为直接 *it 就能得到节点的值。

  2. 对于自定义类型 CustomType,可以通过 it->x 来访问自定义类型 CustomType 中的成员变量 x

#include <iostream>struct CustomType {int x;
};int main() {// 创建三个节点,分别存储值 1、2、3W::ListNode<int> node1(1);      W::ListNode<int> node2(2);      W::ListNode<int> node3(3);      // 链接节点形成链表node1._next = &node2;node2._prev = &node1;node2._next = &node3;node3._prev = &node2;// 创建迭代器,初始指向第二个节点W::ListIterator<int> it(&node2);// 对于 int 类型,直接使用 *it 访问节点的值std::cout << *it << std::endl;  // 输出 2// 使用 it-> 访问 CustomType 的成员变量W::ListNode<CustomType> customNode1({1});W::ListNode<CustomType> customNode2({2});customNode1._next = &customNode2;customNode2._prev = &customNode1;W::ListIterator<CustomType> customIt(&customNode1);// 访问自定义类型 CustomType 的成员变量 xstd::cout << customIt->x << std::endl;  // 输出 1return 0;
}
输出:
2
1

2.6 为什么不能简单使用 const 修饰?
问题解释:

vector 中,const_iterator 可以简单地通过 const 修饰实现,因为 vector连续内存存储的结构。const 只需要防止修改元素的值即可。但在 list 中,情况要复杂得多。list双向链表,迭代器不仅需要访问节点的值,还需要操作链表的前驱和后继节点(prevnext)。简单使用 const 修饰的迭代器无法完全满足 list 的需求。

直接使用 const 修饰的限制:
  • const 修饰的迭代器会使得一些必要的操作(如前向或后向移动)无法进行。
  • 例如:直接对 const 迭代器执行 ++-- 操作,会导致编译错误,因为这些操作需要修改迭代器的内部状态(指针),但 const 修饰符禁止任何修改。

错误示例:直接使用 const 修饰

为了更清楚地说明问题,以下是一个错误示例,展示了为什么简单使用 const 修饰符会导致问题。

错误代码:
#include <iostream>template<class T>
struct ListNode {T _val;ListNode* _prev;ListNode* _next;ListNode(T val) : _val(val), _prev(nullptr), _next(nullptr) {}
};template<class T>
class ListIterator {typedef ListNode<T> 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、3ListNode<int> node1(1), node2(2), node3(3);// 链接节点形成链表node1._next = &node2;node2._prev = &node1;node2._next = &node3;node3._prev = &node2;// 尝试创建一个常量迭代器const ListIterator<int> constIt(&node1);// 错误1:前向移动时,编译器报错,因为 ++ 操作符不能对 const 迭代器操作++constIt;  // 编译错误// 错误2:解引用操作也无法进行修改*constIt = 5;  // 编译错误
}
错误分析:
  1. ++constIt 无法使用:由于 const 修饰,迭代器不能修改其指向的节点,因此 ++ 操作无法进行,因为它需要修改迭代器的内部状态。

  2. *constIt = 5 无法修改值:同样,由于 const 修饰符,迭代器不能修改节点的值,因此编译器会报错。

总结:为什么不能简单使用 const 修饰?

  • 限制过多:简单使用 const 修饰符会导致一些必要的操作无法进行,例如前向和后向移动操作 ++--,因为 const 禁止对迭代器内部状态的修改。
  • 需要灵活区分:我们需要通过模板参数 RefPtr 来灵活区分哪些操作需要保持常量,哪些操作允许修改。这使得迭代器可以在常量和非常量链表中都能正确工作。
  • 代码复用:使用模板参数使得我们只需要一套迭代器代码就可以处理 constnon-const 的情况,提高了代码的简洁性和复用性。

2.6 正确解决方案:使用模板参数区分 constnon-const
为什么需要模板参数?

为了应对上面提到的问题,我们可以通过模板参数来区分 constnon-const 的情况。模板参数 RefPtr 可以控制迭代器的行为,使得它在常量链表和非常量链表中都能够正常工作:

  • Ref:控制解引用 * 返回的是非常量引用 T& 还是常量引用 const T&
  • Ptr:控制通过 -> 操作符返回的是非常量指针 T* 还是常量指针 const T*
使用模板参数的好处:
  • 灵活性:可以根据需要处理 constnon-const 的迭代器场景。
  • 保证安全性:对于常量链表,保证不能修改元素的值;而对于非常量链表,可以进行修改操作。
  • 代码复用:通过模板参数,只需要编写一套迭代器代码,既可以用于常量链表,也可以用于非常量链表。

正确的模板泛化代码实现:

通过模板参数 RefPtr,我们可以实现支持 constnon-const 两种迭代器的代码。

namespace W {template<class T, class Ref, class Ptr>class ListIterator {typedef ListNode<T> 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;  // 迭代器指向的链表节点};
}
关键点解释:
  1. 模板参数 RefPtr:这两个参数分别用于控制 operator*operator-> 的返回值类型:

    • Ref 用于控制解引用操作的返回类型,可以是 T&const T&
    • Ptr 用于控制 -> 操作符的返回类型,可以是 T*const T*
  2. 前向与后向移动:我们定义了 operator++operator--,这些操作修改了迭代器的内部状态,允许它前向或后向遍历链表节点。


2.7 测试泛化后的迭代器
测试场景:
  1. 对非常量链表:我们测试 int 类型节点,通过 *it 解引用获取节点值。
  2. 对自定义类型 CustomType 的链表:通过 it->x 来访问自定义类型 CustomType 的成员变量 x
  3. 对常量链表:我们测试 const 迭代器,确保无法通过迭代器修改链表节点的值。
测试代码:
#include <iostream>struct CustomType {int x;
};int main() {// 创建三个 int 类型的节点,分别存储值 1、2、3W::ListNode<int> node1(1);      W::ListNode<int> node2(2);      W::ListNode<int> node3(3);      // 链接节点形成链表node1._next = &node2;node2._prev = &node1;node2._next = &node3;node3._prev = &node2;// 创建一个非常量迭代器W::ListIterator<int, int&, int*> it(&node1);std::cout << *it << std::endl;  // 输出 1++it;  // 前向移动std::cout << *it << std::endl;  // 输出 2// 创建自定义类型 CustomType 的链表节点W::ListNode<CustomType> customNode1({1});W::ListNode<CustomType> customNode2({2});customNode1._next = &customNode2;customNode2._prev = &customNode1;// 创建自定义类型的迭代器W::ListIterator<CustomType, CustomType&, CustomType*> customIt(&customNode1);std::cout << customIt->x << std::endl;  // 输出 1// 创建一个常量链表const W::ListNode<int> constNode1(1);const W::ListNode<int> constNode2(2);constNode1._next = &constNode2;// 创建一个常量迭代器W::ListIterator<int, const int&, const int*> constIt(&constNode1);std::cout << *constIt << std::endl;  // 输出 1// 常量迭代器不允许修改值// *constIt = 5;  // 错误:无法修改常量链表节点的值return 0;
}
输出:
1
2
1
1

2.8 迭代器设计分析

通过以上步骤,我们设计了一个功能完整的 list 迭代器,支持以下功能:

  • 指针操作*->,可以访问节点的值和自定义类型的成员变量。
  • 前向与后向移动++--,可以在链表中双向遍历。
  • 支持 const 和非 const:通过模板参数 RefPtr,迭代器能够根据链表是否为常量链表返回不同类型的值或指针,确保常量链表不能被修改。
  • 代码复用:同一套代码可以处理常量链表和非常量链表,极大地提高了代码的灵活性和复用性。

3. list 容器的基本操作

现在我们有了节点和迭代器,接下来实现 list 的核心操作,包括构造、插入、删除和访问元素等。

3.1 构造函数

我们将实现多种构造函数,允许用户创建空链表、指定大小的链表,以及从迭代器区间构造链表。

namespace W {template<class T>class list {typedef ListNode<T> Node;public:typedef ListIterator<T, 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);}// 迭代器区间构造函数template<class Iterator>list(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;  // 指向头节点的指针};
}
构造函数分析
  • 默认构造函数:创建一个空链表,并初始化头节点。
  • 指定大小构造函数:使用 push_back 插入 n 个值为 val 的节点。
  • 区间构造函数:从迭代器区间 [first, last) 中构造链表。

4. 插入与删除操作

list 容器的优势在于高效的插入与删除操作。我们将在指定位置插入节点,或删除指定节点,插入和删除时间复杂度均为 O(1)。

4.1 插入操作
namespace W {template<class T>class list {typedef ListNode<T> Node;typedef ListIterator<T, 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); }};
}
插入操作分析
  • 插入效率:在链表中插入时,仅需调整前后节点的指针,不涉及元素移动,因此效率为 O(1)。
  • 头尾插入:通过 push_backpush_front,实现头部和尾部插入。
4.2 删除操作
namespace W {template<class T>class list {typedef ListNode<T> Node;typedef ListIterator<T, 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()); }};
}
删除操作分析
  • 删除效率:与插入类似,删除操作仅涉及指针调整,不需要移动元素,效率为 O(1)。
  • 头尾删除:通过 pop_frontpop_back 实现头部和尾部删除操作。

5. 反向迭代器

在双向链表中,反向迭代器可以通过包装普通迭代器实现。反向迭代器的 ++ 相当于正向迭代器的

--,反之亦然。

namespace W {template<class Iterator>class 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); }};
}

6. 迭代器失效问题

当我们删除一个节点时,指向该节点的迭代器会失效。如果继续使用该迭代器,会导致未定义行为。因此,在删除操作后,我们需要使用 erase 返回的迭代器。

void TestIteratorInvalidation() {W::list<int> lst = {1, 2, 3, 4, 5};auto it = lst.begin();while (it != lst.end()) {it = lst.erase(it);  // 正确:使用 erase 返回的新迭代器}
}

结论

通过本次模拟实现,我们深入剖析了 C++ list 的核心功能,从双向链表的数据结构,到迭代器的设计,再到插入和删除操作的高效实现。希望通过这篇文章,大家对 list 有了更为深入的理解,并能在实际开发中灵活运用。

以上就是关于【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器的内容啦,各位大佬有什么问题欢迎在评论区指正,或者私信我也是可以的啦,您的支持是我创作的最大动力!❤️

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

多模态——基于XrayGLM的X光片诊断的多模态大模型

0.引言 近年来&#xff0c;通用领域的大型语言模型&#xff08;LLM&#xff09;&#xff0c;如ChatGPT&#xff0c;已在遵循指令和生成类似人类的响应方面取得了显著成就。这些成就不仅推动了多模态大模型研究的热潮&#xff0c;也催生了如MiniGPT-4、mPLUG-Owl、Multimodal-G…

Synchronized和 ReentrantLock有什么区别?

目录 一、java中的线程同步 二、Synchronized 使用方式 底层原理 synchronized 同步代码块的情况 synchronized 修饰方法的情况 总结 synchronized 和 volatile 有什么区别&#xff1f; 三、ReentrantLock 底层原理 使用方式 四、Synchronized和 ReentrantLock有什…

GPIO端口的使用

目录 一. 前言 二. APB2外设时钟使能寄存器 三. GPIO端口的描述 四. GPIO端口使用案例 一. 前言 基于库函数的开发方式就是使用ST官方提供的封装好的函数。而如果没有添加库函数&#xff0c;那就是基于寄存器的开发方式&#xff0c;这种方式一般不是很推荐。因为由于ST对寄存…

docker pull 超时的问题如何解决

docker不能使用&#xff0c;使用之前的阿里云镜像失败。。。 搜了各种解决方法&#xff0c;感谢B站UP主 <iframe src"//player.bilibili.com/player.html?isOutsidetrue&aid113173361331402&bvidBV1KstBeEEQR&cid25942297878&p1" scrolling"…

维护左边枚举右边

前言&#xff1a;一开始遇到这个题目的时候没啥思路&#xff0c;但是当我看到值域在1000的时候我想着直接暴力从右边枚举不就行了吗&#xff0c;时间复杂度刚刚好&#xff0c;试一下就过了 正解应该是啥呢&#xff0c;其实也是维护一遍&#xff0c;运行另外一边 O ( n ) O(n)…

所有测试人,下半年的新方向(大模型),赢麻了!!!

现在做测试&#xff0c;真的挺累的。 现在测试越来越难做&#xff0c;晋升困难&#xff0c;工资迟迟不涨……公司裁员&#xff0c;测试首当其冲&#xff01;&#xff01; 做测试几年了&#xff0c;还没升职&#xff0c;就先到了“职业天花板”。 想凭工作几年积累的经验&…

Linux进程:fork函数深度剖析

目录 一、初识fork函数 二、fork函数的返回值 三、fork之后&#xff0c;父子进程谁先运行 四、fork的使用示例 一、初识fork函数 在linux中fork函数时非常重要的函数&#xff0c;它从已存在进程中创建一个新进程。新进程为子进程&#xff0c;而原进程为父进程。 进程调用fork…

Apache POI快速入门

介绍 Apache POl是一个处理Miscrosoft Office各种文件格式的开源项目。简单来说就是&#xff0c;我们可以使用 POI 在 Java 程序中对Miscrosoft Office各种文件进行读写操作。 一般情况下&#xff0c;POI都是用于操作 Excel 文件。 Apache POI的主要组件包括&#xff1a; HS…

监控易监测对象及指标之:全面监控Sybase_New数据库

随着企业数据量的不断增长和业务的复杂化&#xff0c;数据库的稳定性和性能成为了保障业务连续性的关键因素。Sybase_New数据库作为众多企业选择的数据管理解决方案&#xff0c;其稳定性和性能对于企业的运营至关重要。 为了确保Sybase_New数据库的稳定运行和高效性能&#xff…

单体到微服务架构服务演化过程

架构服务化 聊聊从单体到微服务架构服务演化过程 单体分层架构 在 Web 应用程序发展的早期&#xff0c;大部分工程是将所有的服务端功能模块打包到单个巨石型&#xff08;Monolith&#xff09;应用中&#xff0c;譬如很多企业的 Java 应用程序打包为 war 包&#xff0c;最终会形…

基于vue框架的刺梨销售管理系统pgl49(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。

系统程序文件列表 项目功能&#xff1a;用户,刺梨分类,刺梨产品,刺梨新闻 开题报告内容 基于Vue框架的刺梨销售管理系统开题报告 一、引言 随着现代农业的快速发展和消费者对健康食品需求的日益增长&#xff0c;刺梨这一富含营养价值的水果逐渐受到市场的青睐。然而&#x…

学习Python的难点分析

一、语法灵活性带来的困惑 缩进规则 Python使用缩进来表示代码块&#xff0c;而不是像其他编程语言&#xff08;如C或Java&#xff09;使用大括号。这虽然使代码看起来简洁&#xff0c;但对于初学者来说可能会造成困扰。例如&#xff1a; if True:print("This is insid…

网站建设中,营销型网站与普通网站有什么区别

营销型网站与普通网站在建站目的、交互设计以及结构优化等方面存在区别。以下是具体分析&#xff1a; 建站目的 营销型网站&#xff1a;以销售和转化为主要目标&#xff0c;通过专业的市场分析和策划来吸引潜在客户&#xff0c;并促使其采取购买行动。普通网站&#xff1a;通常…

零基础学Servlet

零基础学Servlet 一。介绍&#xff1a; servlet是一种比较古老的编写网站的方式&#xff0c;在2010年之前比较流行&#xff0c;在此之后&#xff0c;有一堆大佬创造了Spring&#xff08;一种框架&#xff09;&#xff0c;Spring是针对Servlet进行进一步封装&#xff0c;从而让…

OpenSource - 开源日历库tui.calendar

文章目录 强大且灵活的开源日历库推荐&#xff1a;tui.calendar多视图支持&#xff1a; Monthly, Weekly, Daily and Various View Types支持拖拽: Dragging and Resizing a Schedule事件管理支持多语言集成与扩展高度定制化其他功能地址总结 强大且灵活的开源日历库推荐&#…

关于Chrome浏览器F12调试,显示未连接到互联网的问题

情况说明 最近笔者更新下电脑的Chrome浏览器&#xff0c;在调试前端代码的时候&#xff0c;遇到下面一个情况&#xff1a; 发现打开调试面板后&#xff0c;页面上显示未连接到互联网&#xff0c;但实际电脑网络是没有问题的&#xff0c;关闭调试面板后&#xff0c;网页又能正…

防砸安全鞋这样挑,舒适又安心!

在复杂多变的工作环境中&#xff0c;安全始终放在首位&#xff0c;特别是对于那些在工地、车间等危险环境中工作的朋友们来说&#xff0c;一双好的防砸安全鞋无疑是工作中的“守护神”。然而&#xff0c;市面上的防砸安全鞋种类繁多&#xff0c;如何挑选一双既舒适又安心的鞋子…

汇川技术即将在工博会发布类博途全集成自动化平台,德国研发中心功不可没,投资数亿打造工业利器

在即将拉开帷幕的9月中国工博会上&#xff0c;汇川技术将向世界展示其自主研发的全新成果——IFA&#xff0c;一个全集成、全场景覆盖的工厂自动化软件平台。这一平台的发布&#xff0c;不仅标志着汇川技术在工业自动化领域的又一里程碑式突破&#xff0c;更彰显了其与国际工业…

VS code Jupyter notebook 导入文件目录问题

VS code Jupyter notebook 导入文件目录问题 引言正文引言 这几天被 VS code 中 Jupyter Notebook 中的文件导入折磨的死去活来。这里特来说明一下放置于不同文件夹下的模块该如何被导入。 正文 首先,我们需要按下 Ctrl + , 键打开设置,然后搜索 notebook file root。在如…