C++STL-list 底层实现
目录
一、实现框架
二、list_node节点类的模拟实现
节点构造函数
三、list_iterator迭代器的模拟实现
迭代器类的模板参数说明
构造函数
*运算符重载
++运算符的重载
--运算符的重载
==运算符的重载
!=运算符的重载
list的模拟实现
默认成员函数
构造函数
拷贝构造函数
赋值运算符重载函数
析构函数
迭代器相关函数
begin和end
插入、删除函数
push_back
insert
erase
push_front
pop_back
pop_front
size
swap
clear
一、实现框架
namespace lzg
{//模拟实现list当中的结点类template<class T>struct list_node{//成员变量T _data;list_node<T>* _next;list_node<T>* _prev;list_node(const T x& = T()):_data(x),_next(nullptr),_prev(nullptr){}};//模拟实现list迭代器//这里写的一种是模板,最后由编译器实例化对应的类型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){}//各种运算符重载Ref operator*();Ptr operator->();Self& operator++();Self& operator--();Self operator++(int);Self operator--(int);bool operator!=(const Self& s);bool operator==(const Self& s);};template<class T>class list{typedef list_node<T> Node;public://迭代器有两种(const和非const),通过传递的参数(有无const)来实例化对应迭代器typedef list_iterator<T, T&, T*> iterator;typedef list_iterator<T, const T&, const T*> const_iterator;//初始化空的带哨兵位的双向循环链表void list_empty();list();//默认构造函数(调用list_empty();)list(const list<T>& lt);//拷贝构造函数list(size_t n, const T& val = T());//初始化n个值为val的链表list<T>& operator=(list<T> lt); //赋值重载~list(); //析构//迭代器相关函数iterator begin();iterator end();const_iterator begin() const;const_iterator end() const;//链表的操作函数void push_front(const T& x);void pop_front();void pop_back();void push_back(const T& x);iterator insert(iterator pos, const T& val);iterator erase(iterator pos);void clear();void swap(list<T>& tmp);size_t size();private:Node* _head;size_t _size;};}
二、list_node节点类的模拟实现
list的底层是带头双向循环链表
因此,我们若要实现list,则首先需要实现一个结点类。而一个结点需要存储的信息有:数据、前一个结点的地址、后一个结点的地址,于是该结点类的成员变量也就出来了(数据、前驱指针、后继指针)
而对于该结点类的成员函数来说,我们只需实现一个构造函数即可。因为该结点类只需要根据数据来构造一个结点即可,而结点的释放则由list的析构函数来完成。
节点构造函数
list_node(const T x& = T()):_data(x),_next(nullptr),_prev(nullptr)
{}
传值时利用匿名构造来实现,这样节点就能存储任意类型的值
三、list_iterator迭代器的模拟实现
在之前string和vector中模拟实现迭代器我们都是在类里面完成的,并没有单独拎出来封装成一个类,这是因为在前两者中,物理地址空间是连续的可以完成自增自减,但链表不一样,两个节点的地址不是连续的不能通过+1来实现从一个节点到另一个节点的操作。
你可能还会问,那么为什么不在list里面封装形成一个内部类,这也是不合理的,这样会
降低灵活性:难以共享迭代器实现细节(如const和非const迭代器)
增加耦合:迭代器实现与特定容器绑定
模板特化困难:不利于针对不同迭代器类别进行优化
迭代器类的模板参数说明
这里我们所实现的迭代器类的模板参数列表当中为什么有三个模板参数?
template<class T, class Ref, class Ptr>
我们是仿照c++底层源码实现的,Ref是reference(引用)的缩写,Ptr是pointer(指针)的缩写
我们在list中typedef了两种迭代器
typedef _list_iterator<T, T&, T*> iterator;
typedef _list_iterator<T, const T&, const T*> const_iterator;
传递的形参没有加const(<T, T&, T*> )也就是对应普通迭代器( iterator;)
加上了const修饰那么就是const迭代器 (const_iterator)
可以理解我们写的是一个迭代器模板,通过传递的参数不同由编译器帮我们实例化出是普通迭代器还是const迭代器,如果不用Ref和Ptr我们就要写两种迭代器并且它们是高度相似的
构造函数
迭代器类实际上就是对结点指针进行了封装,其成员变量就只有一个,那就是结点指针,其构造函数直接根据所给结点指针构造一个迭代器对象即可。
list_iterator(Node* node):_node(node)
{}
*运算符重载
当我们使用解引用操作符时,是想得到该位置的数据内容。因此,我们直接返回当前结点指针所指结点的数据即可,但是这里需要使用引用返回,因为解引用后可能需要对数据进行修改。
Ref operator*()
{return _node->_data;//访问节点中存储的数据
}
->运算符重载
Ptr operator->()
{return &_node->_data;
}
++运算符的重载
typedef list_iterator<T, Ref, Ptr> Self;
前置++
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的模拟实现
默认成员函数
构造函数
list是一个带头双向循环链表,在构造一个list对象时,直接申请一个头结点,并让其前驱指针和后继指针都指向自己即可。
void list_empty()
{_size = 0;_head = new Node();_head->_next = _head;_head->_prev = _head;
}list()
{list_empty();
}
拷贝构造函数
拷贝构造函数就是根据所给list容器,拷贝构造出一个新对象。对于拷贝构造函数,我们先调用list_empty函数把链表初始化为空,再通过复用push_back函数一个个尾插到新构造的容器后面即可。
//lt2(lt1)
list(const list<T>& lt)
{list_empty();for (auto& e : lt){push_back(e);//将容器lt当中的数据一个个尾插到新构造的容器后面}
}
赋值运算符重载函数
对自定义类型传值传参要调用对应的拷贝构造函数,我们通常加上&(引用)是为了减少传值传参带来的拷贝冗余,但是我们这里故意不用引用,那么lt就是lt1的拷贝,通过交换lt来实现赋值,这样把lt1赋值给了lt2,并且我们交换的是lt(lt1的拷贝)并不会影响lt的数据,并且出函数时,临时对象lt会自动析构
// lt2=lt1
list<T>& operator=(list<T> lt)
{ swap(lt); return *this;
}
析构函数
//析构函数
~list()
{clear(); //清理容器delete _head; //释放头结点_head = nullptr; //头指针置空
}
clear的实现在后面,并且clear也是复用erase函数
迭代器相关函数
begin和end
begin函数返回的是第一个有效数据的迭代器,end函数返回的是最后一个有效数据的下一个位置的迭代器。
由于返回类型是iterator (list_iterator<T,T&,T*>) 如果写出return _head->_next,编译器不会自动将指针转为自定义类类型,这里我们用迭代器的匿名构造返回地址
iterator begin()
{return iterator(_head->_next);
}iterator end()
{return iterator(_head);
}
当然,还需要重载一对用于const对象的begin函数和end函数
const_iterator begin() const
{ return const_iterator(_head->_next);
}
const_iterator end() const
{return const_iterator(_head);
}
插入、删除函数
push_back
画图后操作一目了然,完成insert函数后就能复用
void push_back(const T& x)
{/*Node* new_node = new Node(x);Node* tail = _head->_prev;tail->_next = new_node;new_node->_prev = tail;new_node->_next = _head;_head->_prev = new_node;*/insert(end(), x);
}
insert
iterator insert(iterator pos, const T& val)
{Node* cur = pos._node; //pos是一个类需要的是里面的_node值Node* newnode = new Node(val);Node* prev = cur->_prev;//prev newnode curnewnode->_next = cur;cur->_prev = newnode;newnode->_prev = prev;prev->_next = newnode;_size++;return iterator(newnode);
}
erase
iterator erase(iterator pos)
{assert(pos != end());Node* del = pos._node;Node* prev = del->_prev;Node* next = del->_next;//prev del nextprev->_next = next;next->_prev = prev;delete del;_size--;return iterator(next);
}
push_front
void push_front(const T& x)
{insert(begin(), x);
}
pop_back
void pop_back()
{erase(--end());
}
pop_front
void pop_front()
{erase(begin());
}
size
为了方便在list中我们还定义了_size来表明链表中插入节点的个数,并且在上面insert和erase函数中都更新了对_size的操作,所以我们直接返回就行了
size_t size()
{return _size;
}
swap
交换两个链表的哨兵位节点就能交换两个链表
void swap(list<T>& tmp)
{swap(_head,tmp._head);swap(_size, tmp._size);
}
clear
clear函数用于清空容器,我们通过遍历的方式,逐个删除结点,只保留头结点即可。
void clear()
{auto it = begin();while (it != end()){it = erase(it);}
}