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 {}
};
【注意】:
-
模板参数:
template <class T>表示list_node是一个模板结构体,可以接受任何类型T作为其节点的值类型。 -
成员初始化列表:在构造函数中,使用了成员初始化列表(
:_next(nullptr), _prev(nullptr), _val(val))来初始化成员变量。这是一种更高效的初始化方式,特别是对于引用和指针类型。 -
默认参数:构造函数中的
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在底层实现和内存布局上有很大的不同,这导致了它们在迭代器使用上的区别。
- 内存布局:
std::vector是一个动态数组,它在内存中连续存储元素。这意味着std::vector的迭代器可以简单地通过指针(或指针的封装)来实现,因为元素之间的地址是连续的。std::list则是一个链表,其元素在内存中不必连续存储。每个元素(节点)包含数据和指向下一个(以及前一个,对于双向链表)节点的指针。因此,std::list的迭代器需要包含更多信息,通常是一个指向当前节点的指针。
- 迭代器失效:
std::vector的迭代器在插入或删除元素时可能会失效(如果操作导致内存重新分配),但在读取元素时通常是稳定的。std::list的迭代器在插入或删除节点时通常不会失效(除非删除的是迭代器当前指向的节点),因为链表操作不需要移动其他元素。然而,如果迭代器指向了一个已被删除的节点(例如,通过保留一个已删除节点的迭代器),则使用该迭代器是未定义行为。
- 迭代器类型:
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?
- 临时对象:当调用
lt.end()时,end()函数通常会返回一个迭代器对象,这个对象是作为临时值返回的。在C++中,临时对象具有常量性,即你不能通过它们调用非const成员函数。因此,为了使比较操作符能够接受lt.end()返回的临时迭代器作为参数,这些操作符必须是const的。- 避免权限放大:即使比较操作符本身不修改对象的状态,将它们声明为非
const成员函数也会允许调用者通过非常量的迭代器对象调用它们。这可能会引发权限放大的问题,因为调用者可能会错误地假设这些函数能够修改对象的状态。通过将比较操作符声明为const,你明确地表明这些函数不会修改它们的参数,这有助于防止潜在的错误。- 代码一致性:在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: 插入新节点
- 如果
cur是nullptr(意味着pos指向链表末尾之后的位置),则将新节点插入到链表末尾。这通常意味着将新节点的_next设置为nullptr,并将其_prev设置为链表的最后一个节点。但由于我们假设pos是一个有效的插入位置,这种情况不太可能发生(除非在end()之后调用insert,这通常是不允许的)。然而,为了完整性,我们可以检查这一点,并相应地调整逻辑。 - 在大多数情况下,将新节点插入到
prev和cur之间。这涉及更新prev的_next、newnode的_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节点现在是删除操作后当前位置的有效节点。 - 如果
next是nullptr(意味着被删除的节点是链表的最后一个节点),则根据需求,您可能希望返回一个特殊的迭代器,比如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
【实现步骤】:
- 获取尾节点
- 创建新节点
- 更新链表连接
【实现代码】:
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不能直接把迭代器当作指针一样使用?2.2 list迭代器的框架设计2.3 *和-> 操作符的重载2.4 和-- 操作符的重载2.5 !和 操作符的重载 三、 list的函…...
视频去水印的3个技巧,教你无痕去水印
许多视频平台为了推广自身品牌或者广告用途,会在视频上添加水印。这些水印不仅影响了视频的美观,还可能限制了内容的传播范围。幸运的是,有几种简单而有效的方法可以帮助我们去除视频中的水印,同时保持视频的原始画质和观感。以下…...
LSTM模型改进实现多步预测未来30天销售额
关于深度实战社区 我们是一个深度学习领域的独立工作室。团队成员有:中科大硕士、纽约大学硕士、浙江大学硕士、华东理工博士等,曾在腾讯、百度、德勤等担任算法工程师/产品经理。全网20多万粉丝,拥有2篇国家级人工智能发明专利。 社区特色…...
八LAMP搭建
# LAMP ## 一、知识回顾 ### FTP samba nfs 特点 - 借用Linux用户作为映射用户,进行权限管理 - 软件本身还有管理控制权限 #### 客户端连接到服务器后进行读写执行等操作 ,必须同时具有: - 软件许可的权限 vsftpd: anon upload enableYES - 文件…...
Windows——解除Windows系统中文件名和目录路径的最大长度限制
第一步:打开本地组策略编辑器 按下Win R键打开运行窗口,输入 gpedit.msc 并回车,打开本地组策略编辑器。 第二步:开启 长路径设置 第三步:重启计算机...
黑名单与ip禁令是同一个东西吗
黑名单和IP禁令相关,但它们并不是完全相同的概念。以下是它们之间的区别: 黑名单 定义:黑名单通常是一个包含不允许或被禁止的用户、IP地址、域名或其他实体的列表。用途:用于阻止特定用户或实体访问某个系统或服务。黑名单可以…...
FuTalk设计周刊-Vol.075
国庆75周年,也是第75期周刊~ 祝大家国庆快乐~! #AI漫谈 热点捕手 1.万字深剖!13位AI巨擘联袂,1.6万字解码生成式AI产品「全攻略」 “生成式人工智能产品”主题论坛,邀请到了来自腾讯、商汤科…...
PE节表中是否存在misc.VirtualSize 比SizeofRawData还要大的情况
确实是存在的,这是win10自带记事本,可以看到 确实是大.所以在申请imagebuffer的时候,还是需要比较大小.但是在还原的时候.只考虑sizeofRawData即可>...
栈及笔试题
目录 栈的实现 1、数组栈 2、链式栈 栈的创建 栈的打印 内存泄漏 栈溢出 练习 有效的括号 栈的实现 栈后入先出 1、数组栈 (最佳实现,且访问数据的时候CPU告诉访存命中率比较高,因为地址连续存放,访问时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里文字不显示。本文以宋体中文字不显示为例。 问题:由浅入深一步一步分析 问题1、预览正常,但生成pdf中文不显示 报告模板编辑后,预览正常,但生成pdf中文不显示。以下是试验过程: 先编辑好一个报告单模…...
给Ubuntu虚拟机设置静态IP地址(固定IP)
查看 为Ubuntu虚拟机配置静态IP地址(固定IP)的方法经过亲自测试,已被证实有效。 这里你记得网关就可以了,等下要用 查看配置前的网络信息 ifconfig 查看网关 route -n 配置 配置网络文件 cd /etc/netplan/ ls 查看自己的文件的名…...
spring boot文件上传之x-file-storage
spring boot文件上传之x-file-storage 今天看到一个文件上传的开源组件x-file-storage,官方地址如下: https://x-file-storage.xuyanwu.cn/#/ 该组件官网是这样介绍的,如下: 一行代码将文件存储到本地、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 组…...
脸爱云管理系统存在任意文件上传漏洞
漏洞描述 脸爱云一脸通智慧管理平台是一套功能强大、运行稳定、操作简单方便、用户界面美观的一脸通系统。该平台整合了人脸识别技术和智能化解决方案,可以实现识别和管理个体身份,为各种场景提供便捷的身份验证和管理功能。其存在任意文件上传漏洞&…...
elasticsearch_exporter启动报错 failed to fetch and decode node stats
最近把服务器迁移到了ubuntu系统,结果发现在centos还正常运行的elasticsearch_exporter,用systemd启动后一直报错 failed to fetch and decode node stats 在网上翻了大半年,竟然都无解!这种报错,很明显就是你的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月,我加入了公司的新智慧公交平台项目研发团队,并担任系统架构师角色,负责系统整体架构的设计与评审。该项目采用了物联网三层架构模型,其中设备接入层和网络交互层基于公司的中台战略,我们有效复…...
【go入门】常量
目录 定义枚举iota思考题 定义 go语言常量的定义和其他语言类似,常量中的数据类型只能是布尔型,数字型(整型、浮点型、复数)和字符串型 常量的定义方式和变量一样,只不过变量定义使用 var 关键字,而常量定…...
【力扣数据库知识手册笔记】索引
索引 索引的优缺点 优点1. 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。2. 可以加快数据的检索速度(创建索引的主要原因)。3. 可以加速表和表之间的连接,实现数据的参考完整性。4. 可以在查询过程中,…...
MongoDB学习和应用(高效的非关系型数据库)
一丶 MongoDB简介 对于社交类软件的功能,我们需要对它的功能特点进行分析: 数据量会随着用户数增大而增大读多写少价值较低非好友看不到其动态信息地理位置的查询… 针对以上特点进行分析各大存储工具: mysql:关系型数据库&am…...
关于iview组件中使用 table , 绑定序号分页后序号从1开始的解决方案
问题描述:iview使用table 中type: "index",分页之后 ,索引还是从1开始,试过绑定后台返回数据的id, 这种方法可行,就是后台返回数据的每个页面id都不完全是按照从1开始的升序,因此百度了下,找到了…...
c++ 面试题(1)-----深度优先搜索(DFS)实现
操作系统:ubuntu22.04 IDE:Visual Studio Code 编程语言:C11 题目描述 地上有一个 m 行 n 列的方格,从坐标 [0,0] 起始。一个机器人可以从某一格移动到上下左右四个格子,但不能进入行坐标和列坐标的数位之和大于 k 的格子。 例…...
sqlserver 根据指定字符 解析拼接字符串
DECLARE LotNo NVARCHAR(50)A,B,C DECLARE xml XML ( SELECT <x> REPLACE(LotNo, ,, </x><x>) </x> ) DECLARE ErrorCode NVARCHAR(50) -- 提取 XML 中的值 SELECT value x.value(., VARCHAR(MAX))…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...
Docker 本地安装 mysql 数据库
Docker: Accelerated Container Application Development 下载对应操作系统版本的 docker ;并安装。 基础操作不再赘述。 打开 macOS 终端,开始 docker 安装mysql之旅 第一步 docker search mysql 》〉docker search mysql NAME DE…...
消息队列系统设计与实践全解析
文章目录 🚀 消息队列系统设计与实践全解析🔍 一、消息队列选型1.1 业务场景匹配矩阵1.2 吞吐量/延迟/可靠性权衡💡 权衡决策框架 1.3 运维复杂度评估🔧 运维成本降低策略 🏗️ 二、典型架构设计2.1 分布式事务最终一致…...
aardio 自动识别验证码输入
技术尝试 上周在发学习日志时有网友提议“在网页上识别验证码”,于是尝试整合图像识别与网页自动化技术,完成了这套模拟登录流程。核心思路是:截图验证码→OCR识别→自动填充表单→提交并验证结果。 代码在这里 import soImage; import we…...
跨平台商品数据接口的标准化与规范化发展路径:淘宝京东拼多多的最新实践
在电商行业蓬勃发展的当下,多平台运营已成为众多商家的必然选择。然而,不同电商平台在商品数据接口方面存在差异,导致商家在跨平台运营时面临诸多挑战,如数据对接困难、运营效率低下、用户体验不一致等。跨平台商品数据接口的标准…...
