【C++】list模拟实现


🚀 作者简介:一名在后端领域学习,并渴望能够学有所成的追梦人。
🚁 个人主页:不 良
🔥 系列专栏:🛸C++  🛹Linux
📕 学习格言:博观而约取,厚积而薄发
🌹 欢迎进来的小伙伴,如果小伙伴们在学习的过程中,发现有需要纠正的地方,烦请指正,希望能够与诸君一同成长! 🌹


文章目录

  • 定义结点类
  • lis类的成员变量和构造函数
  • list的迭代器
  • list类常用接口模拟实现
    • insert函数
    • erase函数
    • clear函数
    • 构造函数
    • 拷贝构造函数
    • 赋值运算符重载
    • 析构函数
  • 对比vector和list
  • list的反向迭代器

定义结点类

list相当于带头节点的双向链表,我们定义节点时要用类模板参数,同时定义_next_prev指针和数据_data使用struct定义类,因为节点类要能够被访问,而struct的默认访问权限就是public;构造函数缺省值要使用匿名对象,保证无论是自定义类型还是内置类型都能够构造成功。

//定义节点
template<class T>
struct list_node {list_node<T>* _next;list_node<T>* _prev;T _data;//节点构造函数,缺省值使用匿名对象list_node(const T& x = T()):_next(nullptr),_prev(nullptr),_data(x){}
};

lis类的成员变量和构造函数

list是带头节点的双向链表,所以成员变量我们只需要定义一个头结点_head,构造函数就是让头节点指向自己。

注意成员变量类型为类名 + 模板参数

template<class T>
class list {typedef list_node<T> node;public:list(){_head = new node;_head->_prev = _head;_head->_next = _head;}private:node* _head;
};

list的迭代器

vector和string的底层物理空间是连续的,我们可以通过指针的++或–来移动找到对应的元素,然而list的底层物理空间是不连续的,所以模拟实现list迭代器时,如果使用++--操作,++--的只是一个指针,并不能找到对应的位置。我们可以封装一个list迭代器类,实际上就是对结点指针进行封装,将各种运算符进行重载,使得在list类中能够像vector和string一样能够直接使用迭代器,而不用关心底层实现。

//迭代器类
template<class T>
struct __list_iterator {typedef list_node<T> node;typedef __list_iterator<T> self;node* _node;//构造一个迭代器,用结点的指针构造__list_iterator(node* n):_node(n){}T& operator*(){return _node->_data;}self& operator++(){_node = _node->_next;return *this;}//不相等比较的就是结点的指针bool operator!=(const self& s){return _node != s._node;}
};

测试:

void test_list1()
{list<int> l1;l1.push_back(1);l1.push_back(1);l1.push_back(1);l1.push_back(1);list<int>::iterator it = l1.begin();while (it != l1.end()){cout << *it << " ";++it;}cout << endl;
}

在上面的测试代码中list<int>::iterator it = l1.begin();这句话调用了拷贝构造,因为我们没有实现所以调用的就是编译器自动生成的即浅拷贝,这里之所以能够使用浅拷贝是因为迭代器类中没有写析构函数,而且不需要释放结点(结点的指针不属于迭代器,仅仅是使用list结点,没有拥有list结点,没有权利释放list的对象,结点的析构交给链表。迭代器只是一个工具)。

完善一些后置++、后置–之后的代码:

//迭代器类
template<class T>
struct __list_iterator {typedef list_node<T> node;typedef __list_iterator<T> self;node* _node;//构造一个迭代器,用结点的指针构造__list_iterator(node* n):_node(n){}//重载解引用操作符*T& operator*(){return _node->_data;}//重载前置++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;}
};

当我们实现上述代码之后我们就可以在list类中实现begin和end函数:

iterator begin()
{return iterator(_head->_next);
}
iterator end()
{return iterator(_head);
}

当我们想要接收const对象时就需要先实现const_iterator:如下的代码

void print_list(const list<int>& l1)
{list<int>::iterator it = l1.begin();while (it != l1.end()){(*it) *= 2;cout << *it << " ";++it;}cout << endl;
}

我们在list类中实现下面的代码中编译能够通过:

iterator begin() const
{return iterator(_head->_next);
}
iterator end() const
{return iterator(_head);
}

但是为什么使用const修饰之后还能构造普通迭代器?这里的const修饰的是*this,也就是this指针指向的内容,但是这里this指针指向的内容还是一个指针,修饰的是指针本身即修饰的_head不能被改变,并不是_head指向的内容,指针本身不能改变,但是可以拷贝给别人,也就是说可以将这个指针传到迭代器类中。

虽然上述代码可以通过编译了,但是并不能达到我们想要的效果,仍然能够改变list中的元素值,为什么呢?因为构造出来的迭代器是普通迭代器,可读可修改。

库中const对象调用的是const函数,返回的是const迭代器。

库中函数声明:

     	iterator begin();
const_iterator begin() const;

const迭代器和普通迭代器的区别是:const迭代器本身可以修改,const迭代器指向的内容不可以被修改。

那我们可不可以用下面这种方式定义const迭代器呢?

typedef __list_iterator<T> iterator;   //T*
typedef const iterator const_iterator;  // T* const,而我们想要的是迭代器内容不能被修改,const T*

不可以,普通迭代器对标的是指针即T*,而const加在iterator前面保护迭代器本身不能被修改相当于保护指针不被修改即 T* const,而我们想要的是迭代器指向的内容不能被修改即const T*

那我们想实现const迭代器应该怎么实现呢?很简单,只需要将普通迭代器代码稍加修改:

//const迭代器类
template<class T>
struct __list_const_iterator {typedef list_node<T> node;typedef __list_const_iterator<T> self;node* _node;//构造一个迭代器,用结点的指针构造__list_const_iterator(node* n):_node(n){}//重载解引用操作符*const T& operator*(){return _node->_data;}//重载前置++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;}
};

再将list类中的begin函数和end函数重载:

const_iterator begin() const
{return const_iterator(_head->_next);
}
const_iterator end() const
{return const_iterator(_head);
}

此时我们再将print_list代码修改如下即可实现我们想要的功能:

void print_list(const list<int>& l1)
{list<int>::const_iterator it = l1.begin();while (it != l1.end()){//(*it) *= 2;cout << *it << " ";++it;}cout << endl;
}

观察发现,const迭代器类和普通迭代器类中只有*重载运算符函数的返回值不一样,我们可以通过增加一个模板参数来控制返回值

//迭代器类
template<class T,class Ref> //增加一个模板参数
struct __list_iterator {typedef list_node<T> node;typedef __list_iterator<T,Ref> self;node* _node;//构造一个迭代器,用结点的指针构造__list_iterator(node* n):_node(n){}//重载解引用操作符*Ref operator*(){return _node->_data;}
};

然后在list类中通过模板参数控制返回值

template<class T>class list {//将节点类型重命名为nodetypedef list_node<T> node;public://将迭代器类重命名为iteratortypedef __list_iterator<T,T&> iterator;typedef __list_iterator<T,const T&> const_iterator;private:node* _head;}; 

image-20230703212948556

模板参数传引用或者传指针都可以,模板参数不同类型也就不同。

库里面的list迭代器类中一共有3个模板参数:

template<class T,class Ref,class Ptr>

我们要知道:

1.迭代器要么就是原生指针;

2.迭代器要么就是自定义类型对原生指针的封装,模拟指针的行为(因为node*本身的++不符合需求,不是连续的空间)。

我们还需要重载一个->,自定义类型的指针要用->,内置类型用解引用*就可以。

如下面的测试代码:一般的类最好都将默认构造加上,并且给上缺省值。

struct AA {int _a1;int _a2;AA(int a1 = 0, int a2 = 0):_a1(a1),_a2(a2){}
}; 
void test_list2()
{list<AA> l1;l1.push_back(AA(1, 2));l1.push_back(AA(1, 2));l1.push_back(AA(1, 2));list<AA>::iterator it = l1.begin();while (it != l1.end()){cout << *it << " ";++it;}cout << endl;
}

运行结果报错信息如下:

image-20230704160949768

我们可以通过重载流插入<<和流提取>>运算符解决问题,也可以通过下面的方式访问:

struct AA {int _a1;int _a2;AA(int a1 = 0, int a2 = 0):_a1(a1),_a2(a2){}
}; 
void test_list2()
{list<AA> l1;l1.push_back(AA(1, 2));l1.push_back(AA(1, 2));l1.push_back(AA(1, 2));list<AA>::iterator it = l1.begin();while (it != l1.end()){cout << (*it)._a1 << " " << (*it)._a2 << endl;++it;}cout << endl;
}

但是这种方法有点麻烦,当我们有一个AA* ptr类型的指针时,我们通常使用->进行解引用:

AA* ptr = new AA(3,4);
cout << ptr->_a1 << " " << ptr->_a2 << endl;

所以迭代器也要去重载->,因为模拟的是指针的行为,所以也要支持使用->解引用。

在迭代器类中实现重载->

//重载->,这里返回的相当于AA*
T* operator->()
{return &_node->_data;
}

然后测试代码就可以使用->:

void test_list2()
{list<AA> l1;l1.push_back(AA(1, 2));l1.push_back(AA(1, 2));l1.push_back(AA(1, 2));list<AA>::iterator it = l1.begin();while (it != l1.end()){//cout << it.operator->()->_a1  << it.operator->()->_a2 << endl;//可优化为下面这样:cout << it->_a1 << " " << it->_a2 << endl;++it;}cout << endl;
}

image-20230704163309760

而当是const迭代器的时候返回值就是const T*,所以我们可以在迭代器类中再加一个模板参数Ptr用来表示->返回值:

//迭代器类
template<class T,class Ref,class Ptr>
struct __list_iterator {typedef list_node<T> node;typedef __list_iterator<T,Ref,Ptr> self;//重载->,这里返回的相当于AA*Ptr operator->(){return &_node->_data;}
}

在list类中通过模板参数控制返回值:

template<class T>
class list {//将节点类型重命名为nodetypedef list_node<T> node;public://将迭代器类重命名为iteratortypedef __list_iterator<T,T&,T*> iterator;typedef __list_iterator<T,const T&,const T*> const_iterator;
}

list类常用接口模拟实现

链表中只要实现了insert和erase,头插头删和尾插尾删都可以实现,所以我们先实现insert和erase函数。

insert函数

在pos位置之前插入一个新结点。

//在pos位置之前插入一个新结点
void insert(iterator pos,const T& val)
{node* cur = pos._node;node* prev = cur->_prev;node* new_node = new node(val);prev->_next = new_node;new_node->_prev = prev;new_node->_next = cur;cur->_prev = new_node;
}

list的迭代器不会失效,因为pos的指向不会改变,且它们的位置关系没有改变。

测试代码:迭代器pos的指向都是同一个位置。

void test_list1()
{list<int> l1;l1.push_back(1);list<int>::iterator it = l1.begin();while (it != l1.end()){cout << *it << " ";++it;}//输出1cout << endl;list<int>::iterator pos = l1.begin();l1.insert(pos, 5);l1.insert(pos, 6);l1.insert(pos, 7);it = l1.begin();while (it != l1.end()){cout << *it << " ";++it;}//输出5 6 7 1cout << endl;
}

有了插入之后头插和尾插就可以复用insert:

//尾插
void push_back(const T& x )
{//node* tail = _head->_prev;//这就是尾//node* new_node = new node(x);//tail->_next = new_node;//new_node->_prev = tail;//new_node->_next = _head;//_head->_prev = new_node;insert(end(), x);
}
//头插
void push_front(const T& x)
{insert(begin(), x);
}

erase函数

删除pos位置的结点。

void erase(iterator pos)
{//头结点不能被删除assert(pos != _head);//记录pos位置结点的前后结点node* prev = pos._node->_prev;node* next = pos._node->_next;//将pos结点移出listprev->_next = next;next->_prev = prev;//释放结点delete pos._node;
}

注意erase删除之后迭代器会失效。

迭代器失效是指迭代器所指向的节点失效,list中即节点被删除了,erase函数执行后,it所指向的节点被删除,因此it无效,在下一次使用it时,必须先给it赋值。

复用erase实现头删和尾删:

//尾删
void pop_back()
{erase(--end());
}
//头删
void pop_front()
{erase(begin());
}

注意引入模板参数之后类名已经不能做类型了,类名加模板参数才是类型。

**迭代器不需要关心模板参数是什么,直接使用就可以。vector的迭代器不一定是原生指针,有可能是被封装的。**我们可以通过typeid().name()函数查看类型。

image-20230704172721950

重载运算符可以由我们自己实现,关于迭代器失效问题具体问题具体对待。

list<int> l1;
//使用迭代器
list<int>::iterator it = l1.begin();
while (it != l1.end())
{cout << *it << " ";++it;
}//直接使用结点来定义
list_node<int>* pnode = l1._head->_next;

上面代码中分别使用迭代器和结点来直接定义,如果没有访问限定符的限制,那么上面两个一样吗?从物理空间上看一样,但是我们不能使用pnode遍历,因为++的结果不一样,*pnode是原生指针的解引用,pnode++只是加了一个指针,因为空间不连续并不一定能找到下一个结点。

既然erase之后迭代器失效,那么就要对erase进行修改,要有返回值:返回当前位置的下一个元素。

iterator erase(iterator pos)
{//头结点不能被删除assert(pos != _head);//记录pos位置结点的前后结点node* prev = pos._node->_prev;node* next = pos._node->_next;//将pos结点移出listprev->_next = next;next->_prev = prev;//释放结点delete pos._node;//返回下一个结点return iterator(next);
}

clear函数

清空list

void clear()
{iterator it = begin();while (it != end()){//it = erase(it);//erase会返回当前元素的下一个位置。erase(it++);}
}

我们也可以使用erase(it++),因为后置++返回的是++之前的值,erase的不是it,是返回的it的拷贝。

迭代器中后置++:

//重载后置++
self operator++(int)
{self tmp(*this);_node = _node->_next;return tmp;
}

构造函数

默认构造函数

要push_back的前提要有哨兵位的头结点,所以要先建立一个头结点。我们可以学库里直接建立一个empty_init函数,用来建立头节点,用来初始化,构造函数调用就可以了。

void empty_init()
{_head = new node;_head->_next = _head;_head->_prev = _head;
}
list()
{empty_init();
}

有迭代器区间的构造函数

template<class Iterator>
list(Iterator first, Iterator last)
{empty_init();while (first != last){push_back(*first);++first;}
}

const对象可不可以调用构造函数?

可以,const对象定义的时候没有const属性,如:const int n = 2,否则const对象怎么初始化。

拷贝构造函数

注意也要给定头节点,先进行初始化,调用empty_init函数,再通过尾插将被拷贝list内容拷贝到新容器中。

传统写法:

void empty_init()
{_head = new node;_head->_next = _head;_head->_prev = _head;
}list(const list<T>& lt)
{empty_init();for ( auto e : lt){push_back(e);}
}

现代写法:

//先实现交换函数,交换两个链表的头节点
void swap(list<T>& tmp)
{std::swap(_head, tmp._head);
}list(const list<int>& lt)
{empty_init();//初始化list<T> tmp(lt.begin(), lt.end());//使用迭代器区间构造swap(tmp);//调用swap函数交换头节点
}

赋值运算符重载

void swap(list<T>& tmp)
{std::swap(_head, tmp._head);
}
//l1 = l2list<T>& operator=(list<T> lt)
{swap(lt);return *this;
}

通过传值传参调用拷贝构造函数构造出一个临时对象,然后通过交换头节点实现赋值。不能使用引用,使用引用就相当于将l3和l1的头节点交换,就改变了l3的值,相当于两者交换了并不能正确完成拷贝。

析构函数

~list()
{//先将list中数据清空clear();//再将头结点删除delete _head;//指向空_head = nullptr;
}

对比vector和list

vector与list都是STL中非常重要的序列式容器,由于两个容器的底层结构不同,导致其特性以及应用场景不同,其主要不同如下:

vectorlist
底层结构动态顺序表,一段连续的空间带头结点的双向循环链表
随机访问支持随机访问,访问任意元素的效率O(1)不支持随机访问,访问某个元素的效率O(N)
插入和删除任意位置插入和删除效率低,需要移动元素,时间复杂度为O(N),插入时可能需要增容(开辟新空间,拷贝元素,释放旧空间),导致效率更低任意位置插入和删除效率高,不需要移动元素,时间复杂度为O(1)
空间利用率底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高底层节点动态开辟,小节点容易造成内存碎片,空间利用率低, 缓存利用率低
迭代器原生态指针对原生态指针(节点指针)进行封装
迭代器失效在插入元素时,要给所有的迭代器重新赋值,因为插入 元素有可能会导致重新扩容,致使原来迭代器失效,删除时,当前迭代器需要重新赋值否则会失效插入元素不会导致迭代器失效, 删除元素时,只会导致当前迭代 器失效,其他迭代器不受影响
使用场景需要高效存储,支持随机访问,不关心插入删除效率大量插入和删除操作,不关心随机访问

list的反向迭代器

正向迭代器的++运算符重载是向后走,即向着_next指针走;而反向迭代器的++运算符重载是沿着_prev方向走,我们可以通过将list迭代器的代码改造一下:修改的地方主要就是将正向迭代器的重载++运算符中改成_prev,重载--运算符中改为_next

list的反向迭代器代码如下:

//反向迭代器
template<class T, class Ref, class Ptr>
struct __list_reverse_iterator {typedef list_node<T> node;typedef __list_reverse_iterator<T, Ref, Ptr> self;node* _node;//构造一个迭代器,用结点的指针构造__list_reverse_iterator(node* n):_node(n){}Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}self& operator++(){_node = _node->_prev;return *this;}//后置++self operator++(int){self tmp(*this);_node = _node->_prev;return tmp;}self& operator--(){_node = _node->_next;return *this;}self operator--(int){self tmp(*this);_node = _node->_next;return tmp;}//不相等比较的就是结点的指针bool operator!=(const self& s){return _node != s._node;}bool operator==(const self& s){return _node == s._node;}
};

然后在list类中将反向迭代器类重命名以及rbegin和rend函数:

template<class T>
class list {//将节点类型重命名为nodetypedef list_node<T> node;
public:void empty_init(){_head = new node;_head->_next = _head;_head->_prev = _head;}//将迭代器类重命名为iteratortypedef __list_iterator<T,T&,T*> iterator;typedef __list_iterator<T,const T&,const T*> const_iterator;//将反向迭代器类重命名为 reverse_iteratortypedef __list_reverse_iterator<T, T&, T*> reverse_iterator;typedef __list_reverse_iterator<T, const T&, const T*> const_reverse_iterator;list(){empty_init();}list(const list<T>& lt){empty_init();for (auto e : lt){push_back(e);}}iterator begin(){return iterator(_head->_next);}iterator end(){return iterator(_head);}const_iterator begin() const{return const_iterator(_head->_next);}const_iterator end() const{return const_iterator(_head);}//反向迭代器对应函数reverse_iterator rbegin(){return reverse_iterator(_head->_prev);}reverse_iterator rend(){return reverse_iterator(_head);}const_reverse_iterator rbegin() const{return const_reverse_iterator(_head->_prev);}const_reverse_iterator rend() const{return const_reverse_iterator(_head); }
private:node* _head;
}; 

测试:

void test_list()
{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()){cout << *it << " ";++it;}cout << endl;//反向迭代器list<int>::reverse_iterator rit = lt.rbegin();while (rit != lt.rend()){cout << (*rit) << " ";++rit;}cout << endl;
}

输出结果:

image-20230704211417861

上面begin和end、rbegin和rend的指向刚开始的位置如下:

image-20230704212756291

但是库中的反向迭代器是通过正向迭代器完成的, 库中这种方法使反向迭代器作为一种正向迭代器的适配器模式,可以生产出任何容器的反向迭代器,只要正向迭代器可以正常工作,那么反向迭代器就能够正常的工作。库中begin和end、rbegin和rend的指向刚开始的位置如下:

image-20230704230641835

如果是上面这样刚开始头结点的位置不能解引用即rbegin位置不能解引用,此时我们如果想要实现倒序遍历就要对*重新进行重载,在重载*函数内让其对上一个位置进行解引用。

重载解引用返回的时候,返回值是个问题,我们可以再定义两个模板参数解决。此时我们就不用再用上面那种反向迭代器实现,可以在新的头文件中定义:

#pragma once
namespace Niu {template<class Iterator, class Ref, class Ptr>struct ReverseIterator {typedef ReverseIterator<Iterator, Ref, Ptr> Self;Iterator _cur;//我去用正向迭代器去构造一个方向迭代器ReverseIterator(Iterator it):_cur(it){}//重载*Ref operator*(){Iterator tmp = _cur;//拷贝构造--tmp;return *tmp;//返回上一个结点}Self& operator++(){--_cur;return *this;}Self& operator--(){++_cur;return *this;}bool operator!=(const Self& s){//反向迭代器和正向迭代器都是指针可以直接判断return  _cur != s._cur;}};
}

在类list中定义:

typedef ReverseIterator<iterator, T&,  T*> reverse_iterator;
typedef ReverseIterator<iterator, const T&, const T*> const_reverse_iterator;

此时list类中的rbegin和rend函数也要修改:

reverse_iterator rbegin()
{return reverse_iterator(end());
}
reverse_iterator rend()
{return reverse_iterator(begin());
}

此时再测试:

void test_list()
{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()){cout << *it << " ";++it;}cout << endl;//反向迭代器list<int>::reverse_iterator rit = lt.rbegin();while (rit != lt.rend()){cout << (*rit) << " ";++rit;}cout << endl;
}

测试结果:

image-20230704234036262

我们使用模板,只要实现了一个迭代器,所有的迭代器都出来了,但是要求必须是双向迭代器,支持--才能使用,如list和vector。

list可以拷贝一份正向迭代器改造成反向迭代器,但是vector不能解决,因为vector本身就是内置类型。此时我们通过模板给vector的正向迭代器可以得到vector的反向迭代器:

namespace Niu {template<class T>class vector {public:typedef T* iterator;typedef const T* const_iterator;typedef ReverseIterator<iterator, T&, T*> reverse_iterator;typedef ReverseIterator<iterator, const T&, const T*> const_reverse_iterator;reverse_iterator rbegin(){return reverse_iterator(end());}reverse_iterator rend(){return reverse_iterator(begin());}……};
}

测试:

void test_vector2()
{vector<int> v;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);v.push_back(5);//正向遍历vector<int>::iterator it = v.begin();while (it != v.end()){cout << *it << " ";++it;}cout << endl;//反向迭代器vector<int>::reverse_iterator rit = v.rbegin();while (rit != v.rend()){cout << (*rit) << " ";++rit;}cout << endl;
}

测试结果:

image-20230704233816526

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【大语言模型】15分钟快速掌握LangChain以及ChatGLM

10分钟快速掌握LangChain LangChain简介LangChain中的核心概念1. Components and Chains2. Prompt Templates and Values3. Example Selectors4. Output Parsers5. Indexes and Retrievers6. Chat Message History7. Agents and Tookits LangChain的代码结构1. LangChain中提供…

二叉树前中后序的非递归实现

前言 &#xff1a; 递归我们会有一些问题的 为什么有递归就一定有非递归呢&#xff1f;&#xff1f;首先递归是有一定缺陷的 递归真正的缺陷是&#xff0c;每一个程序运行起来呢都是一个线程的形式&#xff0c;但是每一个线程都会有独立的栈空间&#xff0c;但是栈空间是很…

性能优化(一)JMeter使用

简介&#xff1a; jmeter 是 apache 公司基于 java 开发的一款开源压力测试工具&#xff0c;体积小&#xff0c;功能全&#xff0c;使用方便&#xff0c;是一个比较轻量级的测试工具&#xff0c;使用起来非常简 单。因为 jmeter 是 java 开发的&#xff0c;所以运行的时候必须…

Tkinter_鼠标选中样式

前言 使用tkinter库创建窗口应用程序示例&#xff0c;包含不同鼠标样式标签。 一、方法 import tkinter as tkclass Mouse_Style:def __init__(self):self.root tk.Tk()self.root.title(样式)self.root.geometry("200x5201100150")self.interface()def interface(…

Hbase drop 表卡住没有响应

在实际工作中遇到过重新创建一个hbase的hive外部表&#xff0c;在 disable table_name; drop table_name 在drop table_name卡住 最后有提示报错。 建议各位查看下表有无lock的情况&#xff0c; 查看和释放hbase lock可以通过如下方式来查看 pid获取&#xff1a; 在 Ma…

使用Go 语言的三个原因

几个星期前&#xff0c;我一个朋友问我&#xff1a;“为什么要关心 Go 语言”&#xff1f; 因为他们知道我热衷于 Go 语言&#xff0c;但他们想知道为什么我认为其他人也应该关心。有三个原因&#xff1a;安全性、生产力和并发性。有些语言可以涵盖一个也有可能是两个方面&…

网络互联设备(集线器、交换机、路由器)

文章目录 一、中继器和集线器二、网桥和交换机1&#xff09;交换机的三种转发方式2&#xff09;交换机的分类3&#xff09;交换机互连方式 三、路由及路由器1&#xff09;路由器的作用2&#xff09;路由器的逻辑结构3&#xff09;路由器的硬件组成4&#xff09;路由器启动过程 …

免费 Selenium各大浏览器驱动【谷歌chrme、火狐Firefox、IE浏览器】

aardio群 625494397 废话不多说 直接开整&#xff01; 竟然还有脸收费 服了 下载对应版本的浏览器驱动 目标网址 应用场景 Selenium库涉及到 安装selenium库 pip install selenium-i https://mirrors.aliyun.com/pypi/simple/下载对应浏览器驱动 https://registry.npmmirror.c…

STM32 DSP库CUBEMX配置+FFT频率计算

文章目录 前言一、DSP库添加1.1 加一个define1.2 添加文件路径1.3 主函数 二、FFT运算求频率2.1 初始版本版本2 总结 前言 使用DSP中的函数加快计算。 本文首先讲述如何通过添加dsp库。 再讲述使用DSP库进行实数FFT运算。&#xff08;FFT运算用到了前面讲述的STM32CubeMX-ADC …

【C++】C/C++内存管理

目录 一、C/C内存管理 二、C语言中动态内存管理方式:malloc/calloc/realloc/free 三、C中动态内存管理 1、new/delete操作内置类型 2、new和delete操作自定义类型 四、operator new与operator delete函数(重点) 五、new和delete的实现原理 1、内置类型 2、自定义类型 六、…

kalibr使用照片生成数据

rosrun kalibr kalibr_bagcreater --folder /home/zf/calib_ws/data/hikvision_chessboard_0707/left --output-bag /home/zf/test.bag 生成照片的目录底下要求有cam0,另外照片的数据格式必须得要求有时间:1639644559400963.png

【GaussDB(DWS)】数据分布式存储-三种类型的表

toc 一、环境说明 华为数据仓库服务DWS&#xff0c;集群版本8.1.3.320集群拓扑结构&#xff1a; 二、数据分布式方式 DWS采用水平分表的方式&#xff0c;将业务数据表的元组打散存储到各个节点内。这样带来的好处在于&#xff0c;查询中通过查询条件过滤不必要的数据&#…