当前位置: 首页 > news >正文

STL之list篇(下)(从底层分析实现list容器,逐步剥开list的外表)

文章目录

  • 前言
    • 一、list的数据结构和类实现需求
      • 1.1 数据结构
      • 1.2 list类实现需求
    • 二、list迭代器的实现
      • 2.1 为什么list不能直接把迭代器当作指针一样使用?
      • 2.2 list迭代器的框架设计
      • 2.3 `*`和`->` 操作符的重载
      • 2.4 `++`和`--` 操作符的重载
      • 2.5 `!=`和`==` 操作符的重载
    • 三、 list的函数实现
      • 3.1 框架设计
      • 3.2 构造函数
      • 3.3 begin()和end()迭代器
        • 思考:如何设计const迭代器?
        • 更新后的迭代器实现:
      • 3.4 指定位置插入`insert`
        • 【实现步骤】:
        • 【实现代码】:
      • 3.5 指定位置删除`erase`
        • 【实现步骤】:
        • 【实现代码】:
      • 3.6 尾插`push_back`
        • 【实现步骤】:
        • 【实现代码】:
        • 【优化代码】:
      • 3.7 复用得到头插`push_front`,尾删`pop_back`,头删`pop_front`
        • 【实现代码】:
      • 3.8 返回链表的大小
    • 四、全部代码以及测试样例
      • 4.1 头文件
      • 4.2 源文件


前言

在编程的世界里,容器是数据存储与管理的基石。其中,list作为一种灵活且功能强大的数据结构,扮演着举足轻重的角色。它不仅能够动态地调整大小,还允许我们在任意位置插入和删除元素,为开发者提供了极大的便利。本文将深入探讨list的奥秘,从其底层实现机制到实际应用场景,全面剖析这一容器的独特魅力。无论您是编程初学者,还是经验丰富的开发者,相信都能从中获得宝贵的启示与收获。让我们一同踏上这段探索之旅,揭开list的神秘面纱。

一、list的数据结构和类实现需求

1.1 数据结构

list 的实现中,底层是通过双向链表结构来存储数据。双向链表中的每个节点不仅包含数据,还包含指向前一个节点和后一个节点的两个指针。以下是节点结构的定义:

template <class T>  
struct list_node  
{  // 指向下一个节点的指针  list_node<T>* _next;  // 指向前一个节点的指针  list_node<T>* _prev;  // 存储节点的值  T _val;  // 构造函数,用于初始化节点  list_node(const T& val = T())  :_next(nullptr) // 将_next初始化为nullptr,表示当前节点没有下一个节点  ,_prev(nullptr) // 将_prev初始化为nullptr,表示当前节点没有前一个节点  ,_val(val)      // 使用给定的值初始化_val  {}  
};

【注意】:

  1. 模板参数template <class T> 表示 list_node 是一个模板结构体,可以接受任何类型 T 作为其节点的值类型。

  2. 成员初始化列表:在构造函数中,使用了成员初始化列表(:_next(nullptr), _prev(nullptr), _val(val))来初始化成员变量。这是一种更高效的初始化方式,特别是对于引用和指针类型。

  3. 默认参数:构造函数中的 const T& val = T() 是一个带有默认参数的构造函数。如果调用构造函数时没有提供参数,它将使用类型 T 的默认构造函数来创建一个临时对象,并用这个临时对象来初始化 _val

1.2 list类实现需求

template <class T>
class list
{typedef list_node<T> Node;
public:// 迭代器iterator的设计"......"// 返回指向链表第一个节点的迭代器iterator begin();// 返回指向链表末尾之后位置的迭代器  iterator end();// 构造函数list();// 拷贝构造函数list(const list<T>& lt);// 赋值运算符重载list<T>& operator=(list<T> lt);// 析构函数~list();// 清空void clear();// 尾插void push_back(const T& x);// 头插void push_front(const T& x);// 尾删void pop_back();// 头删void pop_front();// 指定位置插入iterator insert(iterator pos, const T& x);// 指定位置删除iterator erase(iterator pos);// 返回list的大小size_t size();private:Node* _head; // 指向链表的头节点size_t _size; // 链表大小
};

【注意】:

  • 最后我们需要将数据结构和类全都包含在命名空间里,避免出现命名冲突的情况
namespace xny
{template <class T>struct list_node{//...};template <class T>class list{//...};
}

二、list迭代器的实现

2.1 为什么list不能直接把迭代器当作指针一样使用?

我们就vector与list来比较一下:

std::list(在C++标准库中通常是双向链表或循环双向链表的实现)和std::vector在底层实现和内存布局上有很大的不同,这导致了它们在迭代器使用上的区别。

  1. 内存布局:
    • std::vector是一个动态数组,它在内存中连续存储元素。这意味着std::vector的迭代器可以简单地通过指针(或指针的封装)来实现,因为元素之间的地址是连续的。
    • std::list则是一个链表,其元素在内存中不必连续存储。每个元素(节点)包含数据和指向下一个(以及前一个,对于双向链表)节点的指针。因此,std::list的迭代器需要包含更多信息,通常是一个指向当前节点的指针。
  2. 迭代器失效:
    • std::vector的迭代器在插入或删除元素时可能会失效(如果操作导致内存重新分配),但在读取元素时通常是稳定的。
    • std::list的迭代器在插入或删除节点时通常不会失效(除非删除的是迭代器当前指向的节点),因为链表操作不需要移动其他元素。然而,如果迭代器指向了一个已被删除的节点(例如,通过保留一个已删除节点的迭代器),则使用该迭代器是未定义行为。
  3. 迭代器类型:
    • std::vector的迭代器通常是随机访问迭代器,支持高效的元素访问(通过索引)和迭代器算术(如加减整数)。
    • std::list的迭代器是双向迭代器(对于双向链表)或前向迭代器(对于单向链表),它们不支持随机访问,但支持顺序遍历。

2.2 list迭代器的框架设计

template <class T>
struct list_iterator
{	
typedef list_node<T> Node;	// 更换名称,方便使用
typedef list_iterator self;	Node* _node;// 构造函数list_iterator(Node* node):_node(node){}/*各种函数重载...*/
};

2.3 *-> 操作符的重载

1.解引用*操作符

T& operator*()
{return _node->_val;
}
  • 此函数返回当前迭代器所指向节点中存储的值(_val)的引用。由于返回的是引用类型 T&,调用者可以直接修改该值。这里的 _node 是指向链表节点的指针,而 _val 是节点中存储的数据。

2.**成员访问->**操作符:

T* operator->()
{return &_node->_val;
}
  • 此函数返回当前迭代器所指向节点中存储值的地址,即一个指向 T 类型的指针。这使得调用者能够使用指针的箭头操作符(->)来访问节点中存储的对象的成员。需要注意的是,这里返回的是值的地址,而不是节点本身的地址。

2.4 ++-- 操作符的重载

1.**前置++**操作符

**【注意】:**之前我们已经声明typedef list_iterator self;

self& operator++()
{_node = _node->_next;
}
  • 此函数将迭代器当前指向的节点更新为其下一个节点,并返回迭代器自身的引用。由于返回的是引用,因此可以支持链式调用,如++++it(尽管这种写法并不常见且可能引发误解)。

2.**后置++**操作符

self operator++(int)
{self tmp(*this);_node = _node->_next;return tmp;
}
  • 与前置版本不同,后置递增首先会创建一个迭代器副本tmp,该副本保存了递增前的状态,然后更新当前迭代器指向下一个节点,并返回之前保存的副本。这里的int参数实际上并未使用,它仅作为区分前置与后置递增的语法糖。

3.**前置–**操作符

self operator--()
{_node = _node->_prev;
}
  • 此函数的功能与前置递增类似,但它将迭代器当前指向的节点更新为其前一个节点。

4.**后置–**操作符

self& operator--(int)
{self tmp(*this);_node = _node->_prev;return tmp;
}
  • 后置递减的实现与后置递增类似,也是先创建迭代器副本,再更新当前迭代器的指向,并返回副本。

2.5 !=== 操作符的重载

1.!= 操作符

bool operator!=(const self& it) const
{return _node != it._node;
}

2.**==**操作符

bool operator==(const self& it) const
{return _node == it._node;
}
  • 为什么需要添加第二个const
  1. 临时对象:当调用lt.end()时,end()函数通常会返回一个迭代器对象,这个对象是作为临时值返回的。在C++中,临时对象具有常量性,即你不能通过它们调用非const成员函数。因此,为了使比较操作符能够接受lt.end()返回的临时迭代器作为参数,这些操作符必须是const的。
  2. 避免权限放大:即使比较操作符本身不修改对象的状态,将它们声明为非const成员函数也会允许调用者通过非常量的迭代器对象调用它们。这可能会引发权限放大的问题,因为调用者可能会错误地假设这些函数能够修改对象的状态。通过将比较操作符声明为const,你明确地表明这些函数不会修改它们的参数,这有助于防止潜在的错误。
  3. 代码一致性:在C++编程中,将不会修改对象状态的成员函数声明为const是一种良好的编程习惯。这有助于保持代码的一致性和可预测性,使得其他开发者能够更容易地理解和使用你的类。

三、 list的函数实现

3.1 框架设计

template <class T>
class list
{typedef list_node<T> Node;
public:typedef list_iterator<T> iterator;/*...各种函数*/private:Node* _head;size_t _size;
};   

3.2 构造函数

list()
{_head = new Node;_head->next = nullptr;_head->prev = nullptr;_size = 0;
}

注意

  • 这里的头节点(_head)实际上是一个哑节点(dummy node)或哨兵节点(sentinel node),它本身不存储数据,仅仅作为链表的开始和结束的标记。这种设计可以简化链表操作(如插入和删除)的边界条件处理。

3.3 begin()和end()迭代器

iterator begin()
{return _head->next;// return iterator(_head->next);// 单参数会隐式类型转换
}iterator end()
{return _head;// return iterator(_head);
}// 那常对象呢?
思考:如何设计const迭代器?
  • vector一样:typedef const_list_iterator<T> const_iterator;

在 vector 中,const_iterator 通过 const 修饰符即可实现不可修改的迭代器,这是因为 vector 的底层存储是连续的内存块,通过 const 限制访问的值即可。而 list 的底层是双向链表,迭代器不仅需要访问链表节点的值,还需要操作链表的前驱和后继节点(即 _prev 和 _next 指针)。直接使用 const 修饰迭代器无法满足这些需求,因为 const 限制了对链表结构的必要修改。

  • 为什么不能简单使用 const 修饰?

const 修饰的迭代器会限制所有成员的修改,包括迭代器内部的 _node 指针。如果我们对 const 迭代器执行 ++-- 操作,这些操作会修改 _node,而 const 禁止这种修改。

【解决方案】:

这里我们可以模仿库里面的操作,将模板改为以下方式:

template <class T, class Ref, class Ptr>

并在list类中声明:

typedef list_iterator<T, T&, T*> iterator;
typedef list_iterator<T, const T&, const T*> const_iterator;

【解释】:

iterator:

  • T 是迭代器将要操作的元素类型。
  • T& 是对元素的非 const 引用类型,允许通过迭代器修改元素的值。
  • T* 是指向元素的指针类型,用于迭代器的内部实现,以便能够访问链表中的下一个节点。

const_iterator:

  • T 同样是迭代器将要操作的元素类型。
  • const T& 是对元素的 const 引用类型,通过迭代器不能修改元素的值。
  • const T* 是指向 const 元素的指针类型,确保迭代器不会修改链表中的元素。
更新后的迭代器实现:
template <class T, class Ref, class Ptr>
struct list_iterator
{typedef list_node<T> Node;typedef list_iterator self;Node* _node;list_iterator(Node* node):_node(node){}Ref operator*(){return _node->_val;}Ptr operator->(){return &_node->_val;}self& operator++(){_node = _node->_next;return *this;}self operator++(int){self tmp(*this);_node = _node->_next;return tmp;}self& operator--(){_node = _node->_prev;return *this;}self operator--(int){list_iterator tmp(*this);_node = _node->_prev;return tmp;}bool operator!=(const self& it)	const {return _node != it._node;}bool operator==(const self& it)	const{return _node == it._node;}
};

3.4 指定位置插入insert

这个函数接受一个迭代器 pos,该迭代器指向链表中的一个位置,以及一个要插入的值 x。函数应该返回指向新插入节点的迭代器。

【实现步骤】:

步骤 1: 获取当前节点和前一个节点

  • 使用迭代器 pos 获取当前节点 cur
  • 获取前一个节点 prev。如果 cur 是头节点,则 prev 应该是 nullptr(但在这个函数中,pos 不应该指向头节点本身,而是指向链表中的一个有效位置)。然而,为了简化逻辑,我们可以假设头节点是一个哑节点,或者我们有一个特殊的处理方式。在这个例子中,我们将不处理哑节点的情况,而是假设 pos 指向一个有效的数据节点或链表末尾之后的位置(在这种情况下,cur 将是 nullptr 的下一个节点,即实际插入位置的前一个节点的 next)。

步骤 2: 创建新节点

  • 使用要插入的值 x 创建一个新节点 newnode

步骤 3: 插入新节点

  • 如果 curnullptr(意味着 pos 指向链表末尾之后的位置),则将新节点插入到链表末尾。这通常意味着将新节点的 _next 设置为 nullptr,并将其 _prev 设置为链表的最后一个节点。但由于我们假设 pos 是一个有效的插入位置,这种情况不太可能发生(除非在 end() 之后调用 insert,这通常是不允许的)。然而,为了完整性,我们可以检查这一点,并相应地调整逻辑。
  • 在大多数情况下,将新节点插入到 prevcur 之间。这涉及更新 prev_nextnewnode_next_prev、以及 cur_prev

步骤 4: 更新链表大小

  • 增加 _size 以反映新插入的节点。

步骤 5: 返回迭代器

  • 创建一个指向新节点的迭代器,并将其返回。
【实现代码】:
iterator insert(iterator pos, const T& x)
{// 1.获取当前节点和前一个节点Node* cur = pos._node;Node* prev = cur->_prev;// 2.创建新节点Node* newnode = new Node(x);// 3.插入新节点prev->_next = newnode;newnode->_prev = prev;cur->_prev = newnode;newnode->_next = cur;// 4.更新链表大小++_size;// 5.返回迭代器return newnode;
}

3.5 指定位置删除erase

这个函数接受一个迭代器 pos,该迭代器指向链表中的一个位置函数应该返回指向删除节点之后节点的迭代器。

【实现步骤】:

步骤 1: 检查节点有效性

  • 使用 assert 断言来确保传递给 erase 函数的迭代器 pos 是有效的,即它不等于 end() 迭代器。这是因为 end() 迭代器通常指向链表末尾的下一个位置(一个不存在的节点),因此不能从中删除任何元素。

步骤 2: 获取当前节点及其邻居

  • 通过迭代器 pos 获取当前要删除的节点 cur
  • 获取当前节点的前一个节点 prev 和后一个节点 next

步骤 3: 更新链表连接

  • 将前一个节点 prev_next 指针指向当前节点的后一个节点 next
  • 将后一个节点 next_prev 指针指向当前节点的前一个节点 prev

这样,您就从链表中移除了当前节点 cur,因为它不再被前一个和后一个节点所引用。

步骤 4: 释放内存

  • 使用 delete 操作符释放当前节点 cur 所占用的内存。这是非常重要的,因为如果不这样做,将会导致内存泄漏。

步骤 5: 更新链表大小

  • 将链表的大小 _size 减一,以反映已经删除了一个节点。

步骤 6: 返回新的迭代器位置

  • 通常情况下,erase 函数会返回一个指向被删除节点之后节点的迭代器。在这个实现中,我们返回了指向 next 节点的迭代器。这是因为 next 节点现在是删除操作后当前位置的有效节点。
  • 如果 nextnullptr(意味着被删除的节点是链表的最后一个节点),则根据需求,您可能希望返回一个特殊的迭代器,比如 end(),或者抛出一个异常来表示这种特殊情况。然而,在这个实现中,我们假设调用者知道他们是否在删除链表的最后一个节点,并相应地处理返回的迭代器。
【实现代码】:
iterator erase(iterator pos)
{// 1.检查节点有效性,记得包含头文件:#include<cassert>assert(pos != end());// 2.获取当前节点及其邻居Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;// 3.更新链表连接prev->_next = next;next->_prev = prev;// 4.释放内存delete cur;// 5.更新链表大小--_size;// 6.返回新的迭代器位置return next;
}

3.6 尾插push_back

【实现步骤】:
  1. 获取尾节点
  2. 创建新节点
  3. 更新链表连接
【实现代码】:
void push_back(const T& x)
{// 1.获取尾节点Node* tail = _head->_prev;// 2.创建新节点Node* newnode = new Node(x);// 3.更新链表连接tail->_next = newnode;newnode->_prev = tail;newnode->_next = _head;_head->_prev = newnode;
}

【思考】: 在我们已经实现了insert函数之后,再这么写会不会有点冗余呢?

【优化代码】:
void push_back(const T& x)
{// 很经典的类似于运算符重载的复用操作insert(end(), x);
}

3.7 复用得到头插push_front,尾删pop_back,头删pop_front

【实现代码】:
// 头插
void push_front(const T& x)
{insert(begin(), x);
}// 尾删
void pop_back()
{erase(--end());
}// 头删
void pop_front()
{erase(begin());
}

3.8 返回链表的大小

size_t size()
{return _size;
}

没想到吧!如此简单是为什么呢?原因就是我们每次执行插入删除操作的时候都进行了链表大小的更改,所以不需要进行任何其他操作我们就能得到链表的大小啦!

四、全部代码以及测试样例

4.1 头文件

#pragma once
#include <cassert> namespace xny
{template <class T>struct list_node{list_node<T>* _next;list_node<T>* _prev;T _val;list_node(const T& val = T()):_next(nullptr),_prev(nullptr),_val(val){}};// typedef list_iterator<T, T&, T*> iterator;// typedef list_iterator<T, const T&, const T*> const_iterator;template <class T, class Ref, class Ptr>struct list_iterator{typedef list_node<T> Node;typedef list_iterator self;Node* _node;list_iterator(Node* node):_node(node){}Ref operator*(){return _node->_val;}Ptr operator->(){return &_node->_val;}self& operator++(){_node = _node->_next;return *this;}self operator++(int){self tmp(*this);_node = _node->_next;return tmp;}self& operator--(){_node = _node->_prev;return *this;}self operator--(int){list_iterator tmp(*this);_node = _node->_prev;return tmp;}bool operator!=(const self& it)	const // 为什么加const?原因:在调用测试样例时,有it != lt.end(),此时传入的参数是lt.end(),而end()是传值返回一个拷贝的临时变量,出作用域就会销毁,而临时变量具有常性,所以加const接收值不会造成权限放大的问题{return _node != it._node;}bool operator==(const self& it)	const{return _node == it._node;}};template <class T>class list{typedef list_node<T> Node;public:typedef list_iterator<T, T&, T*> iterator;typedef list_iterator<T, const T&, const T*> const_iterator;// 如何设计const迭代器?// typedef const_list_iterator<T> const_iterator;(T* const ptr1)// 这样设计const迭代器是不行的,因为const迭代器期望指向内容不能修改// 这样设计是迭代器本身不能修改// 应该是const T* ptr2iterator begin(){return _head->_next;// return iterator(_head->_next);// 单参数会隐式类型转换}iterator end(){return _head;// return iterator(_head);}const_iterator begin() const{return _head->_next;// return const_iterator(_head->_next);}const_iterator end() const{return _head;//return const_iterator(_head);}void empty_init(){_head = new Node;_head->_prev = _head;_head->_next = _head;_size = 0;}list(){empty_init();}// lt2(lt1)list(const list<T>& lt){empty_init();for (auto& e: lt){push_back(e);}}void swap(list<T>& lt){std::swap(_head, lt._head);std::swap(_size, lt._size);}list<T>& operator=(list<T> lt){swap(lt);return *this;}~list(){clear();delete _head;_head = nullptr;}void clear(){iterator it = begin();while (it != end()){it = erase(it);	// 相当于it++}_size = 0;}void push_back(const T& x){/*Node* tail = _head->_prev;Node* newnode = new Node(x);tail->_next = newnode;newnode->_prev = tail;newnode->_next = _head;_head->_prev = newnode;*/insert(end(), x);}void push_front(const T& x){insert(begin(), x);}void pop_back(){erase(--end());}void pop_front(){erase(begin());}iterator insert(iterator pos, const T& x){Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(x);prev->_next = newnode;newnode->_prev = prev;cur->_prev = newnode;newnode->_next = cur;++_size;return newnode;}iterator erase(iterator pos){assert(pos != end());Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;prev->_next = next;next->_prev = prev;delete cur;--_size;return next;}size_t size(){/*size_t sz = 0;iterator it = begin();while (it != end()){++sz;++it;}return sz;*/return _size;}private:Node* _head;size_t _size;};void test_list1(){list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);list<int>::iterator it = lt.begin();while (it != lt.end()){cout << *it << " ";++it;}cout << endl;for (auto e : lt){cout << e << " ";}cout << endl;}struct A {A(int a1 = 0, int a2 = 0):_a1(a1),_a2(a2){}int _a1;int _a2;};void test_list2(){list<A> lt;lt.push_back(A(1, 1));lt.push_back(A(2, 2));lt.push_back(A(3, 3));lt.push_back(A(4, 4));auto it = lt.begin();while (it != lt.end()){// cout << *(it)._a1 << " " << *(it)._a2 << endl;cout << it->_a1 << " " << it->_a2 << endl;++it;}// 严格来说,it->->_a1才是符合语法的,由于it.operator->()返回的是A*,再调用*的重载,里面又包含了一个->,所以严格来说有两个->// 但是因为运算符重载要求可读性,那么编译器特殊处理,省略了一个->}void test_list3(){list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);lt.push_front(5);lt.push_front(6);lt.push_front(7);lt.push_front(8);for (auto e : lt){cout << e << " ";}cout << endl;lt.pop_front();lt.pop_back();for (auto e : lt){cout << e << " ";}cout << endl;lt.clear();lt.push_back(10);lt.push_back(20);lt.push_back(30);lt.push_back(40);for (auto e : lt){cout << e << " ";}cout << endl;cout << lt.size() << endl;}void test_list4(){list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);for (auto e : lt){cout << e << " ";}cout << endl;list<int> lt1(lt);for (auto e : lt1){cout << e << " ";}cout << endl;list<int> lt2;lt2.push_back(10);lt2.push_back(20);lt2.push_back(30);lt2.push_back(40);for (auto e : lt2){cout << e << " ";}cout << endl;lt1 = lt2;for (auto e : lt1){cout << e << " ";}cout << endl;}}

4.2 源文件

#include <iostream>
using namespace std;#include "list.h"int main() 
{xny::test_list1();cout << endl << endl;xny::test_list2();cout << endl << endl;xny::test_list3();cout << endl << endl;xny::test_list4();return 0;
}

相关文章:

STL之list篇(下)(从底层分析实现list容器,逐步剥开list的外表)

文章目录 前言一、list的数据结构和类实现需求1.1 数据结构1.2 list类实现需求 二、list迭代器的实现2.1 为什么list不能直接把迭代器当作指针一样使用&#xff1f;2.2 list迭代器的框架设计2.3 *和-> 操作符的重载2.4 和-- 操作符的重载2.5 !和 操作符的重载 三、 list的函…...

视频去水印的3个技巧,教你无痕去水印

许多视频平台为了推广自身品牌或者广告用途&#xff0c;会在视频上添加水印。这些水印不仅影响了视频的美观&#xff0c;还可能限制了内容的传播范围。幸运的是&#xff0c;有几种简单而有效的方法可以帮助我们去除视频中的水印&#xff0c;同时保持视频的原始画质和观感。以下…...

LSTM模型改进实现多步预测未来30天销售额

关于深度实战社区 我们是一个深度学习领域的独立工作室。团队成员有&#xff1a;中科大硕士、纽约大学硕士、浙江大学硕士、华东理工博士等&#xff0c;曾在腾讯、百度、德勤等担任算法工程师/产品经理。全网20多万粉丝&#xff0c;拥有2篇国家级人工智能发明专利。 社区特色…...

八LAMP搭建

# LAMP ## 一、知识回顾 ### FTP samba nfs 特点 - 借用Linux用户作为映射用户&#xff0c;进行权限管理 - 软件本身还有管理控制权限 #### 客户端连接到服务器后进行读写执行等操作 &#xff0c;必须同时具有: - 软件许可的权限 vsftpd: anon upload enableYES - 文件…...

Windows——解除Windows系统中文件名和目录路径的最大长度限制

第一步&#xff1a;打开本地组策略编辑器 按下Win R键打开运行窗口&#xff0c;输入 gpedit.msc 并回车&#xff0c;打开本地组策略编辑器。 第二步&#xff1a;开启 长路径设置 第三步&#xff1a;重启计算机...

黑名单与ip禁令是同一个东西吗

黑名单和IP禁令相关&#xff0c;但它们并不是完全相同的概念。以下是它们之间的区别&#xff1a; 黑名单 定义&#xff1a;黑名单通常是一个包含不允许或被禁止的用户、IP地址、域名或其他实体的列表。用途&#xff1a;用于阻止特定用户或实体访问某个系统或服务。黑名单可以…...

FuTalk设计周刊-Vol.075

国庆75周年&#xff0c;也是第75期周刊&#xff5e; 祝大家国庆快乐&#xff5e;&#xff01; #AI漫谈 热点捕手 1.万字深剖&#xff01;13位AI巨擘联袂&#xff0c;1.6万字解码生成式AI产品「全攻略」 “生成式人工智能产品”主题论坛&#xff0c;邀请到了来自腾讯、商汤科…...

PE节表中是否存在misc.VirtualSize 比SizeofRawData还要大的情况

确实是存在的,这是win10自带记事本,可以看到 确实是大.所以在申请imagebuffer的时候,还是需要比较大小.但是在还原的时候.只考虑sizeofRawData即可>...

栈及笔试题

目录 栈的实现 1、数组栈 2、链式栈 栈的创建 栈的打印 内存泄漏 栈溢出 练习 有效的括号 栈的实现 栈后入先出 1、数组栈 &#xff08;最佳实现&#xff0c;且访问数据的时候CPU告诉访存命中率比较高&#xff0c;因为地址连续存放&#xff0c;访问时CPU从cache里一…...

【工程测试技术】第3章 测试装置的基本特性,静态特性和动态特性,一阶二阶系统的特性,负载效应,抗干扰性

目录 3.1 概述 1测量装置的静态特性 2.标准和标准传递 3.测量装置的动态特性 4.测量装置的负载特性 5.测量装置的抗干扰性 1.线性度 2.灵敏度 3.回程误差 4.分辨力 5.零点漂移和灵敏度漂移 3.3.1 动态特性的数学描述 1.传递函数 2.频率响应函数 3.脉冲响应函数 …...

ireport 5.1 中文生辟字显示不出来,生成PDF报字体找不到

ireport生成pdf里文字不显示。本文以宋体中文字不显示为例。 问题&#xff1a;由浅入深一步一步分析 问题1、预览正常&#xff0c;但生成pdf中文不显示 报告模板编辑后&#xff0c;预览正常&#xff0c;但生成pdf中文不显示。以下是试验过程&#xff1a; 先编辑好一个报告单模…...

给Ubuntu虚拟机设置静态IP地址(固定IP)

查看 为Ubuntu虚拟机配置静态IP地址&#xff08;固定IP&#xff09;的方法经过亲自测试&#xff0c;已被证实有效。 这里你记得网关就可以了&#xff0c;等下要用 查看配置前的网络信息 ifconfig 查看网关 route -n 配置 配置网络文件 cd /etc/netplan/ ls 查看自己的文件的名…...

spring boot文件上传之x-file-storage

spring boot文件上传之x-file-storage 今天看到一个文件上传的开源组件x-file-storage&#xff0c;官方地址如下&#xff1a; https://x-file-storage.xuyanwu.cn/#/ 该组件官网是这样介绍的&#xff0c;如下&#xff1a; 一行代码将文件存储到本地、FTP、SFTP、WebDAV、阿…...

Object.values() 、 Object.keys()

拿到当前对象里面的value值 // 假设你有一个对象 const myObject {name: Kimi,age: 30,country: Moon };// 获取对象的所有值 const values Object.values(myObject);// 输出值数组 console.log(values); // ["Kimi", 30, "Moon"] 如果你需要在 Vue 组…...

脸爱云管理系统存在任意文件上传漏洞

漏洞描述 脸爱云一脸通智慧管理平台是一套功能强大、运行稳定、操作简单方便、用户界面美观的一脸通系统。该平台整合了人脸识别技术和智能化解决方案&#xff0c;可以实现识别和管理个体身份&#xff0c;为各种场景提供便捷的身份验证和管理功能。其存在任意文件上传漏洞&…...

elasticsearch_exporter启动报错 failed to fetch and decode node stats

最近把服务器迁移到了ubuntu系统&#xff0c;结果发现在centos还正常运行的elasticsearch_exporter&#xff0c;用systemd启动后一直报错 failed to fetch and decode node stats 在网上翻了大半年&#xff0c;竟然都无解&#xff01;这种报错&#xff0c;很明显就是你的ES密码…...

Git 使用方法

简介 Git常用命令 Git 全局设置 获取Git 仓库 方法二用的比较多 将仓库链接复制 在 git base here ----> git clone 仓库链接 工作区、暂存区、版本库 Git 工作区中文件中的状态 本地仓库的操作 远程仓库操作 git pull 将代码推送到远程仓库 1. git add 文件名 ---放…...

生产环境升级mysql流程及配置主从服务

之前写到过mysql升级8.4的文章, 因此不再介绍mysql的安装过程 避免服务器安装多个mysql引起冲突的安装方法_安装两个mysql会冲突吗-CSDN博客 生产环境升级mysql8.4.x流程 安装mysql 参考之前文章: 避免服务器安装多个mysql引起冲突的安装方法_安装两个mysql会冲突吗-CSDN博客…...

论软件体系结构的演化

摘要 2022年3月&#xff0c;我加入了公司的新智慧公交平台项目研发团队&#xff0c;并担任系统架构师角色&#xff0c;负责系统整体架构的设计与评审。该项目采用了物联网三层架构模型&#xff0c;其中设备接入层和网络交互层基于公司的中台战略&#xff0c;我们有效复…...

【go入门】常量

目录 定义枚举iota思考题 定义 go语言常量的定义和其他语言类似&#xff0c;常量中的数据类型只能是布尔型&#xff0c;数字型&#xff08;整型、浮点型、复数&#xff09;和字符串型 常量的定义方式和变量一样&#xff0c;只不过变量定义使用 var 关键字&#xff0c;而常量定…...

2.1 HuggingFists系统架构(二)

部署架构 上图为HuggingFists的部署架构。从架构图可知&#xff0c;HuggingFists主要分为服务器(Server)、计算节点(Node)以及数据库(Storage)三部分。这三部分可以分别部署在不同的机器上&#xff0c;以满足系统的性能需求。为部署方便&#xff0c;HuggingFists社区版将这三部…...

工具类:JWT

工具类&#xff1a;JWT 依赖JwtUtil.java 依赖 <!-- 创建、解析 和 验证JSON Web Tokens (JWT)--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependenc…...

王道-计网

2 采用滑动窗口机制对两个相邻结点A(发送方)和B(接收方)的通信过程进行流量控制。假定帧的序号长度为3比特,发送窗口与接收窗口的大小均为7,当A发送了编号为0、1、2、3这4个帧后,而B接收了这4个帧,但仅应答了0、1两个帧,A继续发送4、5两个帧,且这两个帧已进入B的接收…...

【机器学习(十)】时间序列案例之月销量预测分析—Holt-Winters算法—Sentosa_DSML社区版

文章目录 一、Holt-Winters算法原理(一) 加法模型(二) 乘法模型(三) 阻尼趋势 二、Holt Winters算法优缺点优点缺点 三、Python代码和Sentosa_DSML社区版算法实现对比(一) 数据读入和统计分析(二) 数据预处理(三) 模型训练和模型评估(四) 模型可视化 四、总结 一、Holt-Winters…...

Webpack优化问题

目录 打包流程swcthread-loaderhash升级插件 打包流程 webpack 的打包流程大致可以分为以下几个步骤&#xff1a; 初始化&#xff1a;webpack 通过配置文件和 Shell 参数&#xff0c;初始化参数&#xff0c;确定入口文件、输出路径、加 载器、插件等信息。接下来读取配置文件…...

yjs10——pandas的基础操作

1.pandas读入文件——pd.read_cvs() data pd.read_csv("E:/机器学习/data/salary.csv") 注意&#xff1a;1.是pd.read_cvs&#xff0c;不要顺手写成np.read_cvs 2.路径的斜杠方向是/&#xff0c;不是\&#xff0c;如果直接从电脑粘贴路径&#xff0c;路径写法是\&am…...

Squaretest单元测试辅助工具使用

1、idea安装插件 Squaretest 然后关掉idea 2、安装字节码软件&#xff08;jclasslib&#xff09; 3、找到idea里面的Squaretest安装目录 找到包含TestStarter的jar包 4、打开 com.squaretest.c.f 打开后选择常量池 5、找到第16个修改 Long value值&#xff0c;修改的数字即为使…...

MFU简介

1、缩写 MFU - Mask Field Utilization&#xff08;光刻掩膜版有效利用比例&#xff09; GDPW - Gross Die Per Wafer&#xff0c;每张wafer上die的数量 2、什么是MASK 在光刻机中&#xff0c;光源&#xff08;紫外光、极紫外光&#xff09;透过mask曝光在晶圆上形成图…...

十分钟实现内网连接,配置frp

十分钟实现内网连接&#xff0c;配置frp 一.frp是什么&#xff1f;其实是一款实现外网连接内网的一个工具&#xff0c;个人理解&#xff0c;说白了就像是teamviwer一样&#xff0c;外网能访问内网。 利用处于内网或防火墙后的机器&#xff0c;对外网环境提供 http 或 https 服…...

解决MySQL命令行中出现乱码问题

在MySQL命令行中遇到乱码问题通常是由于字符编码设置不正确导致的。以下是一些解决步骤&#xff1a; 1. **检查和设置字符集**&#xff1a; 首先&#xff0c;您需要确保MySQL服务器、客户端和数据库使用的是正确的字符集。您可以通过执行以下命令来查看当前的字符集设置&…...