【C++】13.list的模拟实现
首先,我们需要把链表管理起来,也就是把一个个节点管理起来,但是每个节点的信息我们也需要管理,例如节点的前驱指针和后驱指针,以及节点的值,所以我们这里先封装两个类来管理节点和链表。
namespace Ro
{template<class T>struct list_node{list_node(const T& val = T()):_data(val),prev(nullptr),next(nullptr){}T _data;list_node<T>* prev;list_node<T>* next;};template<class T>class list{typedef list_node<T> Node;public:list(){_head = new Node;_head->prev = _head;_head->next = _head;_size = 0;}private:Node* _head;size_t _size;};
};
先将list的模子给写好,同时介绍以下几点:
list_node:
在list中我们需要经常访问list_node中的成员变量,所以需要将list_node中的成员变量公有,干脆使用结构体struct,因为struct默认公有。
注意:成员变量的类型list_node<T>需要显示实例化,不可以直接list_node,虽然之前在函数模板章节有讲过模板也可以不显式的写,让编译器自动推导类型,但是那是函数模板可以让编译器推导,在这里是类模板,类模板是不能自动推导类型的,所以需要显式实例化。
另外在list_node中我们不需要显式定义拷贝构造函数和析构函数,只需要写构造函数
原因如下:
1. 默认行为已足够
- 如果节点类只包含简单的数据成员(如基本数据类型或指针),默认的拷贝构造函数和析构函数已经能够正确工作。
- 拷贝构造函数:默认拷贝构造函数会逐成员拷贝
data
、next
和prev
指针。对于指针成员,这只会复制指针的值(即地址),而不会复制指针指向的对象。 - 析构函数:默认析构函数会自动销毁
data
(如果T
是一个非指针类型且需要销毁),但不会自动释放next
和prev
指针指向的内存(因为它们只是指针,不负责管理内存)。
2. 避免不必要的复杂性
- 显式定义拷贝构造函数和析构函数可能会引入复杂性,尤其是当节点类包含动态分配的资源时。
- 如果手动定义拷贝构造函数,需要确保深拷贝逻辑正确实现,否则可能导致悬空指针或双重释放等问题。
- 如果手动定义析构函数,需要确保释放所有动态分配的资源,否则可能导致内存泄漏。
- 通过依赖默认的拷贝构造函数和析构函数,可以避免这些潜在问题。
3. 链表本身的内存管理
- 在实现一个链表时,通常由链表类本身负责管理节点的内存分配和释放,而不是由节点类负责。
- 例如,链表的插入和删除操作会负责创建和销毁节点,而节点类只需要存储数据和指针。
- 因此,节点类不需要显式定义析构函数来释放内存。
4. 性能考虑
- 显式定义拷贝构造函数和析构函数可能会引入额外的开销,尤其是在节点类被频繁拷贝或销毁的情况下。
- 依赖默认的拷贝构造函数和析构函数可以避免这种开销,尤其是在节点类只包含简单数据成员的情况下。
5. 指针语义的合理性
- 在链表节点中,指针成员(如
next
和prev
)通常只是指向其他节点的指针,而不是拥有这些节点的所有权。 - 因此,默认的浅拷贝行为(即复制指针值)是合理的,因为链表类本身会管理节点的生命周期。
6. C++ 的规则五(Rule of Five)
- 根据 C++ 的规则五,如果一个类需要显式定义拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符或析构函数中的任何一个,那么通常需要显式定义所有这些函数。
- 如果节点类不需要显式定义这些函数中的任何一个(因为默认行为已经足够),那么就没有必要显式定义它们。
7. 现代 C++ 的智能指针
- 如果需要更复杂的内存管理,可以在链表实现中使用智能指针(如
std::unique_ptr
或std::shared_ptr
)来管理节点的生命周期。 - 例如,可以使用
std::unique_ptr<list_node<T>>
来管理节点的内存,从而避免手动管理内存的复杂性。
list:
我们这里实现的双向带头循环链表,所以我们需要一个指向哨兵位头节点的指针,方便我们管理链表,另外为方便得到list的size(),省去遍历一遍链表的繁琐,我们定义两个成员变量_head和_size。同时在构造函数中,我们初始化头节点,并把list_node<T>typedef一下为Node。
接下来我们正式来模拟实现list
1. push_back()
还是老规矩,我们先来实现尾插,让list能够跑起来
void push_back(const T& data)
{Node* newnode = new Node(data);Node* ptail = _head->prev;ptail->next = newnode;newnode->next = _head;newnode->prev = ptail;_head->prev = newnode;_size++;
}
测试一下:
void test_list1()
{list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);
}
2.迭代器实现
在string和vector中,迭代器是原生指针typedef来的,但是在list中这样是不行的,因为string和vector是连续的空间,++就能得到下一个数据的地址,对迭代器解引用就能得到数据,但是list空间是不连续的,直接++是不行的,直接解引也是不行的,因为此时指针指向的是一个结构体,对结构体解引用得到的是该结构体的左值引用,但是我们想做到解引用直接拿到结构体中存的数据。
那要怎么做呢?在之前我们有提过,迭代器是一个像原生指针一样的东西,我们要让迭代器能够模拟指针一样的操作,所以这里我们可以将迭代器封装成一个类,通过operator++,operator*等运算符重载来模拟指针的行为。
template<class T>
struct list_iterator
{typedef list_node<T> Node;typedef list_iterator<T> Self;Node* _node;list_iterator(Node* node):_node(node){ }};
我们需要获取list_node中的值和前驱指针以及后驱指针,所以我们可以让指向list_node的指针_node作为成员变量,同时迭代器++的返回值同样是迭代器,所以我们也对迭代器类型也typedef一下
2.1 operator*()
T& operator*()
{return _node->_data;
}
直接返回节点的数据
2.2 operator++()
operator++分为前置++和后置++,在前面的章节中我们有讲过,前置++和后置++,可以通过占位符来区分。
前置++:
Self& operator++()
{_node = _node->next;return *this;
}
后置++:
Self operator++(int)
{Self tmp(*this);//先储存原来的值_node = _node->next;//再++return tmp;//返回原来的值
}
后置++需要返回++前的指针,所以我们先拷贝构造储存一下++前的指针,再++,然后返回tmp
2.3 operator--()
有++就有--,和++一样也分为前置--和后置--
前置--:
//前置--
Self& operator--()
{_node = _node->prev;return *this;
}
后置--:
//后置--
Self operator--(int)
{Self tmp(*this);_node = _node->prev;return tmp;
}
2.4 operator!=()和operator==()
迭代器是有比较的,例如我们在使用迭代器遍历容器的时候经常会这样用:it != lt.end()
所以还需要比较运算符
bool operator!=(const Self& s) const
{return _node != s._node;
}bool operator==(const Self& s) const
{return _node == s._node;
}
2.5 operator->()
我们知道对结构体的解引用还有箭头运算符,如果我们list_node中的数据_data是自定义类型,例如是一个日期类Date的话,虽然我们可以 { (*it)._year,(*it)._month 和 (*it)._day }这样的操作来访问,但是对于 . 这种对结构体成员访问,使用 -> 访问结构体成员还是更为普遍的
T* operator->()
{return &_node->_data;
}
直接取结构体的地址返回就行,因为结构体指针能够使用->运算符访问结构体成员
2.6 begin()和end()
typedef list_iterator<T> iterator;
iterator begin()
{//return iterator(_head->next);return _head->next;
}iterator end()
{//return iterator(_head);return _head;
}
这里我们可以返回一个iterator的临时对象,不过这里也可以直接返回_head->next,让它自己走隐式类型转换,begin()指向第一个数据,end()指向最后一个数据的下一个位置,最后一个数据的下一个位置就是头节点
2.7 测试
迭代器的基本功能已经实现的差不多了,接下来我们来测试一下
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;//范围forfor (auto e : lt){cout << e << ' ';}cout << endl;
}
再来测试一下重载的箭头运算符
void test_list2()
{struct AA{int a1 = 1;int a2 = 2;};list<AA> lt;lt.push_back(AA());lt.push_back(AA());lt.push_back(AA());lt.push_back(AA());list<AA>::iterator it = lt.begin();while (it != lt.end()){//特殊处理,本来应该是两个->,但是为了可读性省略了一个->cout << it.operator->()->a1 << ':' << it.operator->()->a2 << endl;cout << it->a1 << ':' << it->a2 << endl;it++;}}
需要注意的是这里本来应该是两个->才合理的,第一个operator->()返回的是AA*的指针,第二个->是访问结构体AA的成员,但是为了可读性,省略了一个->
2.8 const迭代器
上面实现的迭代器是普通迭代器可读可写,但是还有const迭代器,可读不可写。
那const迭代器要怎么实现呢?其实只需要复用一下迭代器的代码,然后在修改一下细节就行
const迭代器:
template<class T>
struct const_list_iterator
{typedef list_node<T> Node;typedef const_list_iterator<T> Self;Node* _node;const_list_iterator(Node* node):_node(node){ }//前置++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){Self tmp(*this);_node = _node->prev;return tmp;}bool operator!=(const Self& s) const{return _node != s._node;}bool operator==(const Self& s) const{return _node == s._node;}const T* operator->(){return &_node->_data;}const T& operator*(){return _node->_data;}
};
这里我们只需要将返回值修改,返回const迭代器,const T&和const T*。这样就可以限制写的功能
在list中,我们同样typedef一下const迭代器
template<class T>
class list
{typedef list_node<T> Node;
public:typedef list_iterator<T> iterator;typedef const_list_iterator<T> const_iterator;list(){_head = new Node;_head->prev = _head;_head->next = _head;_size = 0;}iterator begin(){//return iterator(_head->next);return _head->next;}iterator end(){//return iterator(_head);return _head;}const_iterator begin() const{return _head->next;}const_iterator end() const{return _head;}void push_back(const T& data){Node* newnode = new Node(data);Node* ptail = _head->prev;ptail->next = newnode;newnode->next = _head;newnode->prev = ptail;_head->prev = newnode;_size++;}
private:Node* _head;size_t _size;
};
注意:const迭代器是const迭代器修饰的对象是const,可读不可写,不是迭代器不可写,同样const迭代器的begin()和end()需要用const修饰,与const迭代器兼顾,同时防止意外修改容器中的元素
I 测试一下
这里我们发现报错了,为什么呢?
因为这里我们定义的lt并非const对象,所以 lt.begin() 和 lt.end() 调用的是普通迭代器,但是我们把普通迭代器赋值给const迭代器的对象 it ,造成权限的放大(it的权限放大为普通迭代器),在之前的章节中,我们有讲过权限可以缩小,但是不能放大,所以这里会报错,同时这里 const迭代器对象 it 和普通迭代器 lt.end() 是不能比较的,因为两个不同类型的对象不能比较,所以这里会报错没有匹配的 != 运算符
那怎么解决呢?由于我们目前实现的list只是一些简单的功能,像拷贝构造等还没有实现,不然可以拷贝构造一个const对象
像这样的话就不会出问题,但是我们还没实现拷贝构造,那我们干脆实现一个print的函数专门打印,后面测试的时候也可以直接调用
template<class Container>
void print(const Container& con)
{typename Container::const_iterator it = con.begin();while (it != con.end()){//*it += 10;cout << *it << ' ';it++;}cout << endl;//范围forfor (auto e : con){cout << e << ' ';}cout << endl;
}
在形参处使用const,所以这里con是const对象,我们要使用const迭代器
这里我们自定义了一个Container容器类,这样我们传不同的容器都能打印,不止于list
注意这里需要用typename来声明一下Container::const_iterator是一个类,不然编译器会分不清这是类还是静态成员变量
如果我们对const迭代器的对象修改(*it += 10),编译器会报错
只读不写的话const迭代器就不会报错
II 巧用模板实现迭代器
虽然我们直接cv一份代码修改一下细节就能实现const迭代器,但是这样写代码太冗余了,而且普通迭代器和const迭代器只是在返回值类型上不同,其余都一样,那有没有什么更好的方法实现呢?我们来看看STL3.0中是怎么做的
我们可以看到在STL3.0中使用了3个类模板参数,这样就可以让编译器给我们实例化出两个不同的类,这样写确实很妙,那我们也这样模拟岂不美哉
template<class T, class Ref, class Ptr>
struct list_iterator
{typedef list_node<T> Node;typedef list_iterator<T, Ref, Ptr> Self;Node* _node;list_iterator(Node* node):_node(node){ }//前置++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){Self tmp(*this);_node = _node->prev;return tmp;}bool operator!=(const Self& s){return _node != s._node;}bool operator==(const Self& s){return _node == s._node;}Ptr operator->(){return &_node->_data;}Ref operator*(){return _node->_data;}
};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;
}
这样我们传什么样的类模板,就能实例化出不同的迭代器
3.list的增删查改
3.1 insert()
iterator insert(iterator pos, const T& data)
{Node* newnode = new Node(data);Node* cur = pos._node;//pos位置的节点Node* pre = cur->prev;//在pos前插入,先找到pos前的节点//pre newnode curpre->next = newnode;newnode->next = cur;newnode->prev = pre;cur->prev = newnode;_size++;return newnode;
}
在pos位置前插入,我们要先找到pos位置和pos位置前的节点,然后再将新节点插入其中
测试一下:
void test_list4()
{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();int k = 3;while (k--){it++;}lt.insert(it, 20);print(lt);
}
insert实现好了,我们可以把之前的push_back()直接复用insert()的代码
push_back:
在头节点前插入
void push_back(const T& data)
{/*Node* newnode = new Node(data);Node* ptail = _head->prev;ptail->next = newnode;newnode->next = _head;newnode->prev = ptail;_head->prev = newnode;_size++;*/insert(end(), data);
}
push_front:
在begin()前插入
void push_front(const T& data)
{insert(begin(), data);
}
3.2 erase()
删除pos位置的数据
iterator erase(iterator pos)
{assert(pos != end());Node* nxt = pos._node->next;Node* pre = pos._node->prev;pre->next = nxt;nxt->prev = pre;delete pos._node;_size--;return nxt;
}
注意:不能把哨兵位头节点删除了,所以我们这里加一个断言,如果删除的是头节点就断言报错
删除pos位置的数据后,pos位置的迭代器就会失效,所以我们这里返回pos位置的下一个位置的迭代器,这里我们返回nxt节点让它自己走隐式类型转换
测试一下:
删除所有偶数
void test_list5()
{list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);lt.push_back(5);list<int>::iterator it = lt.begin();while (it != lt.end()){if (*it % 2 == 0) it = lt.erase(it);else it++;}print(lt);
}
pop_back:
直接复用erase(),删除头节点前的数据
void pop_back()
{erase(_head->prev);
}
pop_front:
删除头节点后的数据
void pop_front()
{erase(_head->next);
}
3.3 size()和empty()
size_t size() const
{return _size;
}bool empty() const
{return _size == 0;
}
3.4 front() 和 back()
T& front()
{return *begin();
}T& back()
{return *(--end());
}const T& front() const
{return *begin();
}const T& back() const
{return *(--end());
}
比较简单就不多介绍了
3.5 clear()
void clear()
{auto it = begin();while (it != end()){it = erase(it);}
}
清除所有节点,但不能清除头节点,所有我们可以直接复用erase
4.list的拷贝构造和赋值重载
4.1 析构函数
~list()
{clear();delete _head;_head = nullptr;
}
这里直接复用clear(),将所有节点清除,最后释放头节点。
4.2拷贝构造
和string,vector一样,list也有涉及深浅拷贝的问题,如果不写自己的深拷贝的话,走编译器自己默认的浅拷贝,那么两个对象指向的就是同一份链表,会导致析构两次。
这里我们可以试一下浅拷贝:
void test_list7()
{list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);lt.push_back(5);list<int> lt2(lt);print(lt);print(lt2);
}
直接运行崩溃了,所以还是要动手完成自己的深拷贝
但是拷贝构造也得要有自己的头节点,所以我们要先空初始化,创建一个头节点,那我们干脆直接将构造函数中的初始化头节点封装为一个空初始化的函数,在拷贝构造之前先调用空初始化创造自己的头节点
void empty_init()
{_head = new Node;_head->prev = _head;_head->next = _head;_size = 0;
}
不过我们要将空初始化给私有,因为我们不希望外面可以调用这个接口
//构造函数
list()
{empty_init();
}//拷贝构造
list(const list<T>& l)
{empty_init();for (auto e : l){push_back(e);}
}
空初始化之后,在遍历链表,尾插重新创建一个自己的链表。
4.3赋值重载
list<T>& operator=(list<T> tmp)
{swap(_head, tmp._head);return *this;
}
直接使用现代写法
4.4其余构造函数
可以看到这里还有两种构造函数,一个是构造n个值为value的链表,另一个是迭代器区间构造
我们都来实现一下
list(int n, const T& value = T())
{empty_init();for (int i = 0; i < n; i++) push_back(value);
}template<class InputIterator>
list(InputIterator first, InputIterator last)
{empty_init();while (first != last){push_back(*first);first++;}
}
迭代器区间构造在vector模拟实现时有讲过,这里也不多介绍了
代码如下
list.h:
#pragma once
#include <iostream>
#include <assert.h>using namespace std;namespace Ro
{template<class T>struct list_node{list_node(const T& val = T()):_data(val),prev(nullptr),next(nullptr){}T _data;list_node<T>* prev;list_node<T>* next;};template<class T, class Ref, class Ptr>struct list_iterator{typedef list_node<T> Node;typedef list_iterator<T, Ref, Ptr> Self;Node* _node;list_iterator(Node* node):_node(node){ }//前置++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){Self tmp(*this);_node = _node->prev;return tmp;}bool operator!=(const Self& s){return _node != s._node;}bool operator==(const Self& s){return _node == s._node;}Ptr operator->(){return &_node->_data;}Ref operator*(){return _node->_data;}};//template<class T>//struct const_list_iterator//{// typedef list_node<T> Node;// typedef const_list_iterator<T> Self;// Node* _node;// const_list_iterator(Node* node)// :_node(node)// { }// //前置++// 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)// {// Self tmp(*this);// _node = _node->prev;// return tmp;// }// bool operator!=(const Self& s) const// {// return _node != s._node;// }// bool operator==(const Self& s) const// {// return _node == s._node;// }// const T* operator->()// {// return &_node->_data;// }// const T& operator*()// {// return _node->_data;// }//};template<class T>class list{typedef list_node<T> Node;void empty_init(){_head = new Node;_head->prev = _head;_head->next = _head;_size = 0;}public:typedef list_iterator<T, T&, T*> iterator;typedef list_iterator<T, const T&, const T*> const_iterator;//构造函数list(){empty_init();}list(int n, const T& value = T()){empty_init();for (int i = 0; i < n; i++) push_back(value);}//迭代器区间构造template<class InputIterator>list(InputIterator first, InputIterator last){empty_init();while (first != last){push_back(*first);first++;}}//拷贝构造list(const list<T>& l){empty_init();for (auto e : l){push_back(e);}}//析构~list(){clear();delete _head;_head = nullptr;}//赋值重载-现代写法list<T>& operator=(list<T> tmp){swap(_head, tmp._head);return *this;}iterator begin(){//return iterator(_head->next);return _head->next;}iterator end(){//return iterator(_head);return _head;}const_iterator begin() const{return _head->next;}const_iterator end() const{return _head;}void clear(){auto it = begin();while (it != end()){it = erase(it);}}T& front(){return *begin();}T& back(){return *(--end());}const T& front() const{return *begin();}const T& back() const{return *(--end());}size_t size() const{return _size;}bool empty() const{return _size == 0;}void pop_back(){erase(_head->prev);}void pop_front(){erase(_head->next);}iterator erase(iterator pos){assert(pos != end());Node* nxt = pos._node->next;Node* pre = pos._node->prev;pre->next = nxt;nxt->prev = pre;delete pos._node;_size--;return nxt;}iterator insert(iterator pos, const T& data){Node* newnode = new Node(data);Node* cur = pos._node;//pos位置的节点Node* pre = cur->prev;//在pos前插入,先找到pos前的节点//pre newnode curpre->next = newnode;newnode->next = cur;newnode->prev = pre;cur->prev = newnode;_size++;return newnode;}void push_front(const T& data){insert(begin(), data);}void push_back(const T& data){/*Node* newnode = new Node(data);Node* ptail = _head->prev;ptail->next = newnode;newnode->next = _head;newnode->prev = ptail;_head->prev = newnode;_size++;*/insert(end(), data);}private:Node* _head;size_t _size;};template<class Container>void print(const Container& con){typename Container::const_iterator it = con.begin();while (it != con.end()){//*it += 10;cout << *it << ' ';it++;}cout << endl;//范围forfor (auto e : con){cout << e << ' ';}cout << endl;}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;//范围forfor (auto e : lt){cout << e << ' ';}cout << endl;}void test_list2(){struct AA{int a1 = 1;int a2 = 2;};list<AA> lt;lt.push_back(AA());lt.push_back(AA());lt.push_back(AA());lt.push_back(AA());list<AA>::iterator it = lt.begin();while (it != lt.end()){//特殊处理,本来应该是两个->,但是为了可读性省略了一个->cout << it.operator->()->a1 << ':' << it.operator->()->a2 << endl;cout << it->a1 << ':' << it->a2 << endl;it++;}}void test_list3(){list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);print(lt);}void test_list4(){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();int k = 3;while (k--){it++;}lt.insert(it, 20);print(lt);}void test_list5(){list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);lt.push_back(5);list<int>::iterator it = lt.begin();while (it != lt.end()){if (*it % 2 == 0) it = lt.erase(it);else it++;}print(lt);}void test_list6(){list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);lt.push_back(5);cout << lt.front() << endl;cout << lt.back() << endl;//print(lt);}void test_list7(){list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);lt.push_back(5);list<int> lt2(2);//lt2 = lt;print(lt);print(lt2);}
};
相关文章:
【C++】13.list的模拟实现
首先,我们需要把链表管理起来,也就是把一个个节点管理起来,但是每个节点的信息我们也需要管理,例如节点的前驱指针和后驱指针,以及节点的值,所以我们这里先封装两个类来管理节点和链表。 namespace Ro {te…...
Springfox + Swagger 的完整配置及同类框架对比的详细说明
以下是 Springfox Swagger 的完整配置及同类框架对比的详细说明: 一、Springfox Swagger 配置详解 1. 添加依赖 在 pom.xml 中添加以下依赖: <!-- Springfox Swagger 2 --> <dependency><groupId>io.springfox</groupId>…...
实现支付宝沙箱环境搭建
1.介绍 在业务开发的过程中,有时会涉及到一些支付相关的功能,这个时候就需要接入第三方支付接口,而由于开发中需要不断进行测试,使用真实的账号进行支付就有些不值得,所以支付宝为我们提供了第三方SDK,供我…...
element-ui transfer 组件源码分享
transfer 穿梭框组件源码简单分享,主要从以下几个方面: 1、transfer 组件页面结构。 2、transfer 组件属性。 3、transfer 组件方法。 4、transfer 组件事件。 5、transfer slot 挂载。 一、组件页面结构。 二、组件属性。 2.1 value / v-model 绑…...
【最新版】沃德代驾源码全开源+前端uniapp
一.系统介绍 基于ThinkPHPUniapp开发的代驾软件。系统源码全开源,代驾软件的主要功能包括预约代驾、在线抢单、一键定位、在线支付、车主登记和代驾司机实名登记等。用户可以通过小程序预约代驾服务,系统会估算代驾价格并推送附近代驾司机供用户选择&…...
【无标题】spark安装部署
Spark 4种部署模式的另外2种,分别是Yarn、windows模式。 二、 实验准备工作: 1. 三台linux虚拟机 2. spark的压缩包 三、 实验步骤 Spark-yarn 1. 解压缩文件,并重命名为spark-yarn。 tar zxvf spark-3.0.0-bin-hadoop3.2.tgz mv spar…...
【异常解决】Spring Boot 返回排序后的 Map 但前端接收顺序不对的解决方案
博主介绍:✌全网粉丝22W,CSDN博客专家、Java领域优质创作者,掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域✌ 技术范围:SpringBoot、SpringCloud、Vue、SSM、HTML、Nodejs、Python、MySQL、PostgreSQL、大数据、物…...
中兴云电脑W102D_晶晨S905X2_2+16G_mt7661无线_安卓9.0_线刷固件包
中兴云电脑W102D_晶晨S905X2_216G_mt7661无线_安卓9.0_线刷固件包 准备工作: 工具和设备在开始刷机之前,确保你已经准备好以下物品:双公头USB线:选择一根30-50厘米长的USB线,长度适中,方便操作,…...
深度学习激活函数与损失函数全解析:从Sigmoid到交叉熵的数学原理与实践应用
目录 前言一、sigmoid 及导数求导二、tanh 三、ReLU 四、Leaky Relu五、 Prelu六、Softmax七、ELU八、极大似然估计与交叉熵损失函数8.1 极大似然估计与交叉熵损失函数算法理论8.1.1 伯努利分布8.1.2 二项分布8.1.3 极大似然估计总结 前言 书接上文 PaddlePaddle线性回归详解…...
Kotlin中实现静态
实现“类似静态” class Util {fun action1() {}//使用companion object关键字会在类的内部创建一个伴生类,每个类都允许有一个伴生类//而action2作为伴生类中的方法,可以直接通过类名进行调用,实现类似“静态”的效果companion object {fun…...
Android 回显
//执行 private void playRunTime(String cmd) throws Exception { Process p Runtime.getRuntime().exec(cmd); InputStream is p.getInputStream(); BufferedReader reader new BufferedReader(new InputStreamReader(is)); String l…...
基于大模型的胃食管反流病全周期预测与诊疗方案研究
目录 一、引言 1.1 研究背景与意义 1.2 研究目的与创新点 二、胃食管反流病概述 2.1 疾病定义与分类 2.2 流行病学特征 2.3 发病机制 三、大模型技术原理与应用基础 3.1 大模型简介 3.2 适用于胃食管反流病预测的大模型类型 3.3 数据收集与预处理 四、大模型在胃食…...
class文件(二)
字段表集合: 用于描述接口或类中声明的变量 包括类级变量以及实例级变量,不包括方法内部声明的局部变量 字段的修饰符包括: 作用域:public、private、protected修饰符 实例还是类变量:static 可变性:fin…...
Django 实现电影推荐系统:从搭建到功能完善(附源码)
前言:本文将详细介绍如何使用 Django 构建一个电影推荐系统,涵盖项目的搭建、数据库设计、视图函数编写、模板渲染以及用户认证等多个方面。🔗软件安装、环境准备 ❤ 【作者主页—📚阅读更多优质文章、获取更多优质源码】 目录 一…...
UML2.0中的14种图简介,并借助AI生成UML图
UML2.0中的14种图简介,并借助AI生成UML图 绘制流程结构图(Structure Diagrams)1. 类图(Class Diagram):2. 对象图(Object Diagram):3. 组件图(Component Diag…...
Jsoup、Selenium 和 Playwright 的含义、作用和区别
文章目录 一、Jsoup1. 含义2. 作用3. 核心特性4. 适用场景 二、Selenium1. 含义2. 作用3. 核心特性4. 适用场景 三、Playwright1. 含义2. 作用3. 核心特性4. 适用场景 四、Jsoup、Selenium 和 Playwright 的区别五、适用场景对比六、总结 Jsoup、Selenium 和 Playwright 都是用…...
服务器如何修复SSL证书错误?
修复服务器上的SSL证书错误需要根据具体错误类型逐步排查和解决。以下是常见的步骤和解决方案: --- ### **1. 确认错误类型** 首先检查浏览器或工具(如OpenSSL)报错的具体信息,常见错误包括: - **证书过期**…...
AD9253链路训练
传统方式 参考Xilinx官方文档xapp524。对于AD9253器件 - 125M采样率 - DDR模式,ADC器件的DCO采样时钟(500M Hz)和FCO帧时钟是中心对齐的,适合直接采样。但是DCO时钟不能直接被FPGA内部逻辑使用,需要经过BUFIO和BUFR缓冲后,得到s_b…...
VAE-LSTM异常检测模型复刻报告
VAE-LSTM异常检测模型复刻报告 复刻背景 本报告记录了我复刻VAE-LSTM异常检测模型的完整过程。原论文提出了一种结合变分自编码器(VAE)和长短期记忆网络(LSTM)的异常检测方法,用于时间序列数据。 环境配置 复刻过程中使用的环境配置如下: Python 3.…...
有哪些信誉良好的脂多糖供应商推荐?
一般描述 Sigma-Aldrich的脂多糖 (LPS) 是糖脂,由连接单个或多个脂肪酸的碳水化合物单元组合而成,存在于革兰氏阴性菌细胞壁中。LPS是外膜的重要组成部分,结构由脂质A、葡萄糖胺基磷脂、短核寡糖和O-抗原(远端多糖)组…...
初识分布式事务原理
事务是指符合ACID特性的操作就是事务,在同一个数据库中,如果要分别对表A和表B进行插入和删除操作,如果其中一个操作执行失败,可以对当前数据库进行回滚,使其回滚到执行操作前的状态,但是现有的系统架构都是…...
文件上传过程中出现EOFException的解决方案
文件上传过程中出现EOFException的解决方案 项目场景: 项目是一个考试测评系统,包含学生答题截图上传功能。学生通过前端界面提交答题截图,后端服务接收并处理这些图片文件,存储到MinIO对象存储中。 问题描述 前端调用’提交答…...
智能文档解析系统架构师角色定义
(根据专业写作类任务要求,以系统架构师角色定义范式进行结构化呈现) 智能文档解析系统架构师角色定义 核心技能模块 1. 多模态语义解析引擎 支持中英双语对齐的异构数据解码遵循ISO/IEC 30140标准的JSON语法校验体系 ├─ 智能字段补全机…...
翻倍缠论系统+拐点多空雷达,组合指标的使用操盘技术
如上图,单独从副图指标信号来说,在标记①的位置,开始出现副图指标【拐点多空雷达】转多的买点,红柱开始出现就是跟进做多的第一买点。 在标记②的位置,副图指标出现红柱持续,而黑色的柱线开始出现…...
Linux脏页相关参数
参数 以下是Linux内核中与脏页(Dirty Page)相关的各个参数及其含义的详细说明: 1. vm.dirty_background_bytes 含义:系统内存中脏页数量的绝对字节阈值(单位为字节),当脏页达到此值时&#x…...
C++20 module下的LVGL模拟器
ARM GCC:14.2 Toolchain:MSVC 前篇:使用SDL2搭建简易LVGL模拟器_lvgl sdl-CSDN博客 故事 从前 我所用的单片机工程本身是为了电赛设计的,由于电赛的特性,需要使用同一个实验平台通过不同外设的“排列组合”来实现不…...
Go全栈_Golang、Gin实战、Gorm实战、Go_Socket、Redis、Elasticsearch、微服务、K8s、RabbitMQ全家桶
Go全栈全家桶包含: 1、【零基础入门】Go语言核心编程零基础入门实战,B站学习地址分享: 【2025年新版】Go语言教程 2、GolangGinGorm仿小米商城企业级项目实战 3、Golang仿小米商城高并发微服务实战 4、Golang RabbitMQ高并发秒杀、抢购、预约…...
STM32提高篇: 蓝牙通讯
STM32提高篇: 蓝牙通讯 一.蓝牙通讯介绍1.蓝牙技术类型 二.蓝牙协议栈1.蓝牙芯片架构2.BLE低功耗蓝牙协议栈框架 三.ESP32-C3中的蓝牙功能1.广播2.扫描3.通讯 四.发送和接收 一.蓝牙通讯介绍 蓝牙,是一种利用低功率无线电,支持设备短距离通信的无线电技…...
Linux系统编程---精灵进程与守护进程
1、前言 精灵进程又称守护进程、后台进程,在英文中称为 daemon 进程。精灵进程是运行在一个相对干净的环境、不受终端影响、常驻内存的进程,和神话中的精灵一样,拥有不死不灭的特性,长期稳定提供某种功能或服务。 在Linux系统中&a…...
《让机器人读懂你的心:情感分析技术融合奥秘》
机器人早已不再局限于执行简单机械的任务,人们期望它们能像人类伙伴一样,理解我们的喜怒哀乐,实现更自然、温暖的互动。情感分析技术,正是赋予机器人这种“理解人类情绪”能力的关键钥匙,它的融入将彻底革新机器人与人…...
科技项目必须进行验收测试吗?项目验收测试服务机构有哪些?
在现代科技迅猛发展的背景下,各类科技项目层出不穷,从智能硬件到软件系统,乃至工业自动化解决方案,项目的质量直接关系到企业的信誉、用户体验和市场竞争力。那么科技项目必须进行验收测试吗? 简短且明确的回答是:必…...
7.7 Axios+Redux+JWT全链路实战:打通前后端API通信最佳实践
Axios+Redux+JWT全链路实战:打通前后端API通信最佳实践 连接前端与后端 API:全链路数据交互设计指南 关键词:前后端通信架构设计、RESTful API 开发、Axios 请求拦截、Redux 状态管理、JWT 认证集成 1. 前后端通信架构设计原则 我们采用分层架构实现前后端解耦,通过 RES…...
零基础入门 Verilog VHDL:在线仿真与 FPGA 实战全流程指南
摘要 本文面向零基础读者,全面详解 Verilog 与 VHDL 两大主流硬件描述语言(HDL)的核心概念、典型用法及开发流程。文章在浅显易懂的语言下,配合多组可在线验证的示例代码、PlantUML 电路结构图,让你在 EDA Playground 上动手体验数字电路设计与仿真,并深入了解从 HDL 编写…...
[蓝桥杯 2025 省 Python B] 最多次数
import sysdef max_times() -> int:s sys.stdin.readline().strip()checked {l,q,b} # set(),不存在键值对,识别为set()n len(s)time 0i 0while i < n - 2:sec s[i:i3]if set(sec) checked:i 3time 1else:i 1sys.…...
HTTP相关
目录 一、HTTP状态码 1XX信息性状态码 2XX成功状态码 3XX重定向状态码 4XX客户端错误状态码 5XX服务器错误状态码 二、GET/POST/PUT/DELETE请求 2.1GET 2.2POST 2.3PUT 2.4DELETE 2.5RESTful API例子 三、RESTful API 3.1什么是RESTful API 3.2RESTful API中的关…...
使用rclone迁移minio文件
文章目录 一、rclone简介1、工具说明2、核心特点2.1、跨平台支持2.2、多存储支持2.3、加密与安全2.4、增量同步与断点续传2.5、高性能 3、适用场景3.1、云存储迁移3.2、备份与同步3.3、跨云协作3.4、数据加密归档 二、常用命令1、基础操作2、文件传输3、文件管理4、高级功能5、…...
基于Java与MAVLink协议的多无人机(Cube飞控)集群控制与调度方案问题
基于Java与MAVLink协议的多无人机(Cube飞控)集群控制与调度方案问题 背景需求: 我们目前有一个基于Cube飞控的无人机系统,需实现以下核心功能: 多机通信:通过MAVLink协议同时连接并控制多架无人机&#x…...
Super-Vlan和MUX-Vlan的原理、配置、区别
Super-Vlan 原理 Super-Vlan也叫Aggregate-Vlan。 一般的三层交换机中,通常是采用一个VLAN对应一个vlanif接口的方式实现广播域之间的互通,这在某些情况下导致了IP地址的浪费。因为一个VLAN对应的子网中,子网号、子网定向广播地址、子网缺…...
数据一致性问题剖析与实践(二)——单机事务的一致性问题
一、前言 我们一般讲到单机事务,离不开的就是数据库,其最重要的定义就是,要么全部成功执行,要么全部不执行,保证安全的状态转化。 之前我们讨论了几种场景的一致性问题 冗余数据存储中的一致性问题分布式共识中的一…...
VUE Element-ui Message 消息提示组件自定义封装
为了让message 信息提示的更加方便快捷,减少不同地方的调用,避免代码的重复,特意再官方message 组件的基础上二次封装,使代码更加的优雅和高效。 实现效果: 代码组件: 封装成 message.js 文件,…...
HSTL详解
一、HSTL的基本定义 HSTL(High-Speed Transceiver Logic) 是一种针对高速数字电路设计的差分信号接口标准,主要用于高带宽、低功耗场景(如FPGA、ASIC、高速存储器接口)。其核心特性包括: 差分信号传输&…...
【PCB工艺】运放电路中的负反馈机制
通过运算方法器电路设计详细解释负反馈机制(Negative Feedback) 负反馈 是控制系统、电子电路、神经系统等多个领域中非常核心的概念。特别在运算放大器(Op-Amp)电路中,负反馈是实现精确控制和高稳定性的关键机制。 …...
玩转Docker | 使用Docker部署Neko自托管浏览器
玩转Docker | 使用Docker部署Neko自托管浏览器 前言一、Neko介绍简介主要特点二、系统要求环境要求环境检查Docker版本检查检查操作系统版本三、部署Neko服务下载镜像创建容器创建容器检查容器状态检查服务端口安全设置四、访问Neko服务访问Neko首页登录Neko五、基本使用设置键…...
聊聊自动化用例的维护
自动化测试中的农药悖论:为何长期维护至关重要 自动化测试常被视为"一次编写,永久有效"的解决方案,但随着时间的推移,即使设计最精良的测试套件也会逐渐失效。这种现象被称为农药悖论(Pesticide Paradox&am…...
OpenCV 图形API(60)颜色空间转换-----将图像从 YUV 色彩空间转换为 RGB 色彩空间函数YUV2RGB()
操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 将图像从 YUV 色彩空间转换为 RGB。 该函数将输入图像从 YUV 色彩空间转换为 RGB。Y、U 和 V 通道值的常规范围是 0 到 255。 输出图像必须是 8…...
Docker配置带证书的远程访问监听
一、生成证书和密钥 1、准备证书目录和生成CA证书 # 创建证书目录 mkdir -p /etc/docker/tls cd /etc/docker/tls # 生成CA密钥和证书 openssl req -x509 -newkey rsa:4096 -keyout ca-key.pem \ -out ca-cert.pem -days 365 -nodes -subj "/CNDocker CA" 2、为…...
监督学习(Supervised Learning)与无监督学习(Unsupervised Learning)
监督学习与无监督学习是机器学习的两大核心范式,主要区别在于数据是否包含明确的“标签”(目标输出)。 1. 监督学习(Supervised Learning) 定义: 数据形式:输入数据&…...
批量将多个 Excel 表格中的某张图片替换为新的图片
对于 Excel 文档,相信大家都不陌生,我们可以在 Excel 单元格中插入各种各样的图片,我们也可以将 Excel 表格中的图片替换为新的图片,常规的做法我们都是通过 Office 来进行单个处理的,但是如果我们遇到批量处理的场景&…...
数据一致性问题剖析与实践(三)——分布式事务的一致性问题
一、前言 之前我们讨论了几种场景的一致性问题 冗余数据存储中的一致性问题分布式共识中的一致性问题单机事务中的一致性问题 本文将围绕分布式事务中的一致性问题展开讨论。 二、分布式环境的最大难题 相对于单机环境,分布式环境中,一致性问题最大…...
分布式理论和事务
微服务和分布式 微服务 是一种软件架构风格,它将应用程序拆分成一系列小型、独立的服务,每个服务专注于单一功能,彼此通过轻量级通信机制(如 API)进行交互。微服务通常是松耦合的,可以独立开发、部署和扩展…...