目录
本文是模拟STL源码实现"容器list" 的底层结构
STL中的list容器是 双向循环链表,且带头
list的节点
//节点 template <class T> struct ListNode { ListNode<T>* _next; ListNode<T>* _prev; T _date; ListNode(const T& x = T())//节点的值 :_date(x),_next(nullptr),_prev(nullptr) { } };
list的数据成员
对节点这个自定义类型typedef便于书写代码
public: typedef ListNode<T> Node;
数据成员只需要一个头节点即可。
private: Node* _head;
构造函数
list()//无参的链表 { /*_head = new Node(); _head->_next = _head; _head->_prev = _head;*/ empty_init(); } void empty_init() { _head = new Node(); _head->_next = _head; _head->_prev = _head; }
尾插push_back
void push_back(const T& val) { Node* newnode = new Node(val); Node* tail = _head->_prev; tail->_next = newnode; newnode->_prev = tail; newnode->_next = _head; _head->_prev = newnode; }
遍历
list底层并不是连续的空间,所以不能用下标访问。只能用迭代器,但是迭代器需要满足++的操作便于遍历,然而list的迭代器不能是简单的指针,如果是指针,在++操作后得到的值并不符合要求,所以可以对list的节点指针封装为一个自定义类型,++的操作可以用运算符重载解决。
//list的迭代器不能直接用指针替代,指针是内置类型,可以直接++,但是list的迭代器指针如果直接++不满足需求,故对其封装,但是本质上也是指针 template <class T> struct ListIterator { typedef ListNode<T> Node; Node* _node; typedef ListIterator<T> self; ListIterator( Node* node):_node(node)//这里形参不能加const,原因是_node(node)这段代码权限放大了 { } //前置++ self& operator++() { _node = _node->_next; return *this; } //后置++ self operator++(int) { self tmp(*this);//调用拷贝构造,浅拷贝 _node = _node->_next; return tmp; } // bool operator!=(const self& x) { return _node != x._node; } // T& operator*() { return _node->_date; } //前置-- self& operator--() { _node = _node->_prev; return *this; } //后置-- self operator--(int) { self tmp(_node); _node = _node->_prev; return tmp; } };
insert/erase
有了迭代器,其他部分的操作就相当容易了。
先写insert
iterator insert(iterator pos, const T& val) { Node* newnode = new Node(val); Node* prev = pos._node->_prev; Node* cur = pos._node; prev->_next = newnode; newnode->_prev = prev; newnode->_next = cur; cur->_prev = newnode; return newnode; }
这样一来,push_back和push_front都可以复用insert了。关于返回值为什么可以直接返回newnode,原因是此时的迭代器是一个自定义类型,且有一个拷贝构造只有一个参数,参数类型是节点的类型,那么return后,会发生类型转换,调用构造函数初始化一个临时变量,临时变量再拷贝构造给返回值,这在我的文章类和对象的 “隐式转换”标题下有提到。
再写erase
iterator erase(iterator pos) { assert(pos != end()); Node* prev = pos._node->_prev; Node* next = pos._node->_next; Node* cur = pos._node; prev->_next = next; next->_prev = prev; delete cur; return ListIterator<T>(next); }
复用erase写pop_back和pop_front。
iterator erase(iterator pos) { assert(pos != end()); Node* prev = pos._node->_prev; Node* next = pos._node->_next; Node* cur = pos._node; prev->_next = next; next->_prev = prev; delete cur; return next; } void pop_back() { erase(--end()); } void pop_front() { erase(begin()); }
const_iterator
很多人并不真正理解const_iterator,我先来说明const_iterator到底有什么意义
这是一段普通的代码:
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;
那么什么是迭代器iterator?
这是list的大致结构
这是迭代器
可见,迭代器就是指针,但不完全是指针。可以这样理解:迭代器具有指针的属性,访问元素,修改指向的目标空间,++等等。但是指针是内置类型,迭代器可能是内置类型,也可能是自定义类型(比如list的迭代器,内置类型直接装饰的迭代器无法满足需求)
那什么是const_iterator?
迭代器具有指针的属性,那就可以解引用(*),那就可以修改指向的目标空间。但是对于const修饰的list来说,比如某一个函数只有访问list的权限,并没有修改list的权限:
void print_list(const list<int>& lt) { list<int>::const_iterator it = lt.begin(); while (it != lt.end()) { //*it += 10; cout << *it << endl; ++it; }
这个时候的迭代器类型就是const_iterator,不可以是iterator。
这两种写法的区别
const list<int>::iterator it = lt.begin(); list<int>::const_iterator it = lt.begin(); 前者是对iterator加const,把iterator理解为一个类型,那么就说明这是一个常量迭代器,无法改变指向 后者是const_iterator类型,这个类型的变量不能修改指向目标空间的内容,但是可以改变指向
故可以说,iterator和const_iterator就是两个服务于容器的不同类型
可以把iterator和const_iterator的代码分开写成两个自定义类型,但是代码有冗余,因为这两个类型的成员函数只有解引用(*)和(->)会有所不同,可以利用模板参数实现两个类型 。
关于全部代码已上传list.h
在写list和它的迭代器类型的时候,我还有一个启发。关于一个类的成员函数是否要加const,取决于,这个类会不会生成const的对象并且调用这个函数。
拿list举例来说,list类有begin的普通版和const版,是因为list可能会实例化普通对象和const对象。
而iterator这个类,它的成员函数大都不会写const版本,因为iterator是“指针”,而指针几乎用不到const版本,const修饰指针是指针不可以改变指向,这种场景很少出现。