阅读了很多大厂招聘的面经,发现越是受欢迎的企业,越重视程序员对代码底层实现的考察.所以笔者决定从C++ STL内置容器开始,逐个分析其底层实现逻辑和源码,从而对编程有更为深刻的认识.之前的每次阅读源码的尝试,都因为源码内容多而杂,变量命名怪异而且繁多等等原因而放弃,希望这次我能坚持下来.本次分析的是C++ STL list的源码.
首先要了解list的本质. list是一个双向链表,但也有循环列表的部分性质,这意味着它从任意位置存取元素的速度会比较快,但是随机查询某个位置的元素会比具有一段连续空间的顺序表vector慢.阅读list源码的本质就是弄明白三件事:1.list数据结构 2.迭代器的定义,特别是重载运算符的操作.3.list操作方法的实现.
首先是list节点的定义部分.
struct _List_node_base //list节点的基础实现 { _List_node_base* _M_next; //定义前驱节点 _List_node_base* _M_prev; //定义后继节点 } template<typename _Tp> //类模板 struct _List_node : public __detail::_List_node_base { _Tp _M_data; //节点存储的数据 }
从这里就可以看出list的双向链表本质了。这一步先是定义了list的基类_List_node_base,实现了前驱和后继指针的定义。然后根据该基类继承实现了_List_node类,在两个指针的基础上,定义了本节点存储的值value。
需要注意其中的一些语法:
template<typename _Tp>
这是一个类模板,由于用户定义的list可能存取任何可能性的变量类型,所以使用template定义一个类模板,根据此类模板传入value的值,保证了List定义的一般性.可以看到后面定义了一个_Tp类型的变量_M_data
struct _List_node : public __detail::_List_node_base { ... }
定义了_List_node类,继承了_List_node_base类并使用了__detail命名空间
然后是list的一些定义
template<typename _Tp, typename _Alloc> class _List_base { protected: //使用traits方法,得到_List_node<_Tp>的内存分配器 typedef typename _Alloc::template rebind<_List_node<_Tp> >::other _Node_alloc_type; typedef typename _Alloc::template rebind<_Tp>::other _Tp_alloc_type; struct _List_impl : public _Node_alloc_type { //数据成员只有头节点 __detail::_List_node_base _M_node; _List_impl() : _Node_alloc_type(), _M_node() { } _List_impl(const _Node_alloc_type& __a) : _Node_alloc_type(__a), _M_node() { } }; _List_impl _M_impl; //分配节点的接口 _List_node<_Tp>* _M_get_node() { return _M_impl._Node_alloc_type::allocate(1); } _List_base() : _M_impl() { _M_init(); } //list初始化,在其为空时,头指针和尾指针都指向自身 void _M_init() { this->_M_impl._M_node._M_next = &this->_M_impl._M_node; this->_M_impl._M_node._M_prev = &this->_M_impl._M_node; } //从第一个节点开始依次删除并释放内存。 template<typename _Tp, typename _Alloc> void _List_base<_Tp, _Alloc>:: _M_clear() { typedef _List_node<_Tp> _Node; _Node* __cur = static_cast<_Node*>(_M_impl._M_node._M_next); while (__cur != &_M_impl._M_node) { _Node* __tmp = __cur; __cur = static_cast<_Node*>(__cur->_M_next); _M_get_Node_allocator().destroy(__tmp); _M_put_node(__tmp); } }
可以看到list除了具有双向链表的性质,也具有循环链表的性质,相当于一个双向循环链表.在其不为空时,头节点不存储数据.
由于list是一个链表,并不是一块连续的存储空间顺序存储,所以默认的迭代器是不能实现的,需要重写一个迭代器来满足遍历操作。以下为源码。
template<typename _Tp> struct _List_iterator { //定义类型,便于调用 typedef _List_iterator<_Tp> _Self; typedef _List_node<_Tp> _Node; typedef ptrdiff_t difference_type; typedef std::bidirectional_iterator_tag iterator_category; typedef _Tp value_type; typedef _Tp* pointer; typedef _Tp& reference; _List_iterator() : _M_node() { } explicit _List_iterator(__detail::_List_node_base* __x) : _M_node(__x) { } // Must downcast from _List_node_base to _List_node to get to _M_data. //重载运算符 reference operator*() const { return static_cast<_Node*>(_M_node)->_M_data; } pointer operator->() const { return std::__addressof(static_cast<_Node*>(_M_node)->_M_data); } _Self& operator++() { _M_node = _M_node->_M_next; return *this; } _Self operator++(int) { _Self __tmp = *this; _M_node = _M_node->_M_next; return __tmp; } _Self& operator--() { _M_node = _M_node->_M_prev; return *this; } _Self operator--(int) { _Self __tmp = *this; _M_node = _M_node->_M_prev; return __tmp; } // 指向list节点指针 __detail::_List_node_base* _M_node; };
对*,->,++,--等运算符进行了重载,使其能满足用户的需求.
list的操作方法有很多,这里只摘取部分.
iterator begin() _GLIBCXX_NOEXCEPT { return iterator(this->_M_impl._M_node._M_next); } iterator end() _GLIBCXX_NOEXCEPT { return iterator(&this->_M_impl._M_node); } Bool empty() const _GLIBCXX_NOEXCEPT { return this->_M_impl._M_node._M_next == &this->_M_impl._M_node; } void push_front(const value_type& __x) { this->_M_insert(begin(), __x); } void push_back(const value_type& __x) { this->_M_insert(end(), __x); } template<typename... _Args> void _M_insert(iterator __position, _Args&&... __args) { _Node* __tmp = _M_create_node(std::forward<_Args>(__args)...); __tmp->_M_hook(__position._M_node); } template<typename _Tp, typename _Alloc> typename list<_Tp, _Alloc>::iterator list<_Tp, _Alloc>:: insert(iterator __position, const value_type& __x) { _Node* __tmp = _M_create_node(__x); __tmp->_M_hook(__position._M_node); return iterator(__tmp); }
阅读源码可以看到end指向头节点,begin指向第一个数据节点,begin和end组成一个前闭后开区间。 所以,如果判断List是否为空,判断end与begin是否指向同一个结点即可.了解这些特性,List的常用方法就可以逐个实现了,这里不再赘述.