C++STL之list详解

一.list的定义与使用

1.list的定义

list即链表,并且是带头结点的双向循环链表。在模拟实现时我们会重点讲解list结点结构,迭代器结构和链表整体结构。

2.list的使用

1.list的构造

可以看到list的构造方式与vector的构造方式类别上差不多

|-----------------------------------------------------------|-----------------------------|
| 构造函数( (constructor)) | 接口说明 |
| list (size_type n, const value_type& val = value_type()) | 构造的list中包含n个值为val的元素 |
| list() | 构造空的list |
| list (const list& x) | 拷贝构造函数 |
| list (InputIterator first, InputIterator last) | 用[first, last)区间中的元素构造list |

cpp 复制代码
list<int> lt1;
lt1.push_back(1);
lt1.push_back(2);
lt1.push_back(3);
lt1.push_back(4);

list<int> lt2(lt1);

print_container(lt1);
print_container(lt2);

list<int> lt3;
lt3.push_back(10);
lt3.push_back(20);
lt3.push_back(30);
lt3.push_back(40);

lt1 = lt3;
print_container(lt1);
print_container(lt3);

//隐式类型转换可以匹配下面提到的initializer_list
// 直接构造
list<int> lt0({ 1,2,3,4,5,6 });
// 隐式类型转换
list<int> lt1 = { 1,2,3,4,5,6,7,8 };
const list<int>& lt3 = { 1,2,3,4,5,6,7,8 };

2.list的迭代器

list指针不是一个简单的原生指针,因为list的空间不是连续的内存空间,++或者--等操作需要进行具体的封装,因此这里的list迭代器是一个类类型,后面会详细讲解。实际上迭代器本身就具有封装属性,用户在使用迭代器时的行为都没有太大区别,但因容器本身结构差异较大,底层对迭代器的实现也不尽相同。

|---------------|-------------------------------------------------------------------------|
| 函数声明 | 接口说明 |
| begin + end | 返回第一个元素的迭代器+返回最后一个元素下一个位置的迭代器 |
| rbegin + rend | 返回第一个元素的reverse_iterator,即end位置,返回最后一个元素下一个位置的reverse_iterator,即begin位置 |

3.list的容量管理

|-------|------------------------------|
| 函数声明 | 接口说明 |
| empty | 检测list是否为空,是返回true,否则返回false |
| size | 返回list中有效节点的个数 |

4.list的元素访问

|-------|--------------------|
| 函数声明 | 接口说明 |
| front | 返回list的第一个节点中值的引用 |
| back | 返回list的最后一个节点中值的引用 |

5.list的增删查改

|------------|------------------------------|
| 函数声明 | 接口说明 |
| push_front | 在list首元素前插入值为val的元素 |
| pop_front | 删除list中第一个元素 |
| push_back | 在list尾部插入值为val的元素 |
| pop_back | 删除list中最后一个元素 |
| insert | 在list position 位置中插入值为val的元素 |
| erase | 删除list position位置的元素 |
| swap | 交换两个list中的元素 |
| clear | 清空list中的有效元素 |

3.list中出现的迭代器失效问题

相较于其他地址空间连续的容器,list的迭代器失效问题就少了不少,但依然存在。list的底层结构为带头结点的双向循环链表,因此在list中进行插入时是不会导致list的迭代器失效的,只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响。

解决方法也很简单,只需要更新新的迭代器位置即可

cpp 复制代码
void TestListIterator()
{ i
nt array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
list<int> l(array, array+sizeof(array)/sizeof(array[0]));
auto it = l.begin();
while (it != l.end())
{
l.erase(it++); // it = l.erase(it);
}
}

二.list的模拟实现

cpp 复制代码
#pragma once
#include<assert.h>
#include<iostream>

namespace wjh {
	//结点
	template<class T>
	struct list_node {
		//带头双向循环链表
		//一个结点需要记录前后结点
		T _data;
		list_node<T>* _next;
		list_node<T>* _prev;

		list_node(const T&data=T())
			:_data(data),_next(nullptr),_prev(nullptr)
		{}
	};

	//迭代器是一个类类型
	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*() const
		{
			return _node->_data;
		}

        Ptr operator->() const
        {
            return &_node->_data;
        
        }

		Self& operator++()
		{
			_node = _node->_next;
			return *this;
		}

		Self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}
		// 后置++
		Self operator++(int) {
            Self tmp = *this;
            _node = _node->_next;
            return tmp;
		}

        Self operator--(int) {
            Self tmp = *this;
            _node = _node->_prev;
            return tmp;
		}
		//注意这里比较传入的是迭代器,而不是Node对象??
		bool operator==(const Self& rhs) const
		{
			return _node == rhs._node;
		}

        bool operator!=(const Self& rhs) const
        {
            return _node != rhs._node;
        }


	};

	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;

		iterator begin()
		{
			return _head->next;
		}

		iterator end()
		{
			return _head;
		}

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

		const_iterator end() const
		{
			return _head;
		}

		// 初始化的一个统一方法,创建一个头结点
		//并且将两指针指向头结点自身
		void empty_init()
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
			_size = 0;
		}

		list() {
            empty_init();
		}
		
		//花括号列表 {1, 2, 3} 会被转换为 initializer_list<int>
		list(initializer_list<T> il) {
            empty_init();
            for (auto &e : il) {
                push_back(e);
            }
		}

		//拷贝构造
		list(const list<T> &l) { 
			empty_init();
			for (auto &e : l) {
				push_back(e);
			}
        }

		void swap(list<T>& lt)
		{
			std::swap(_head, lt._head);
			std::swap(_size, lt._size);
		}

		//重载=,利用swap交换资源
        list<T> &operator=(const list<T> &lt) {
			swap(lt);
			return *this;
        }

		void clear() {
			auto it=begin();
			while (it!=end()) {
				//防止迭代器失效,更新it
                it=erase(it);
            }
		}

        ~list() {
			clear();
            delete _head;
            _head=nullptr;
		}
		//头插尾插
		void push_back(const T &x) { 
            insert(end(),x);
		}

		void push_front(const T &x) {
            insert(begin(),x);
		}

		iterator insert(iterator pos, const T &x) { 
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(x);

			// prev newnode cur
			newnode->_next = cur;
			cur->_prev = newnode;
			newnode->_prev = prev;
			prev->_next = newnode;

			++_size;

			return newnode;
		}

		void pop_back()
		{
			erase(--end());
		}

		void pop_front()
		{
			erase(begin());
		}

		iterator erase(iterator pos)
		{
			assert(pos != end());

			Node* prev = pos._node->_prev;
			Node* next = pos._node->_next;

			prev->_next = next;
			next->_prev = prev;
			delete pos._node;

			--_size;

			return next;
		}

		size_t size() const
		{
			return _size;
		}

		bool empty() const
		{
			return _size == 0;
		}
	private:
		Node* _head;
		size_t _size;
	};
}

三.知识补充

1.由list的模拟实现我们可以看到,list的迭代器是用一个类进行封装的。因为链表的地址空间并不连续,它自然也不支持用表达式做参数传入erase等函数

cpp 复制代码
it = lt.begin();
lt.erase(it + 3);

2.在vector我们就讲过,迭代器按找功能分可以分为普通迭代器,反向迭代器以及const版本的普通迭代器和反向迭代器4种。我们可以再按性质给迭代器分为三类:单向迭代器(如forward_list单链表),双向迭代器(list/map/set)以及随机迭代器(vector/deque/string)。而不同的算法(这里用sort举例)需要的迭代器不同,究其根本是因为各个容器的底层结构不同,可传入连续空间的迭代器的sort自然无法应用在不连续空间的迭代器。

cpp 复制代码
// 不支持,要求随机迭代器
list<int> lt={1,2,3,4,5};
//只能使用list自己的sort
sort(lt.begin(), lt.end());

3.再说回迭代器失效,在list中只会出现删除情况下的迭代器失效,但也仅是当前元素的迭代器失效,其他的迭代器仍然正常,借此我们需要对erase函数进行特别注意,在删除元素后返回当前删除元素的下一个元素的迭代器即可

cpp 复制代码
iterator erase(iterator pos)
		{
			assert(pos != end());

			Node* prev = pos._node->_prev;
			Node* next = pos._node->_next;

			prev->_next = next;
			next->_prev = prev;
			delete pos._node;

			--_size;

			return next;
		}

4.按需实例化

在C++模板中有一个"按需实例化"的概念,即:如果不出现语法上编译器能严格检查出来的错误,当前运行程序若并未使用该方法,依旧不会报错。例如对const迭代器指向的对象进行修改:

cpp 复制代码
template<class Container>
void print_container(const Container& con)
{
	// const iterator -> 迭代器本身不能修改
	// const_iterator -> 指向内容不能修改
	typename Container::const_iterator it = con.begin();
	//auto it = con.begin();
	while (it != con.end())
	{
		//*it += 10;

		cout << *it << " ";
		++it;
	}
	cout << endl;

	for (auto e : con)
	{
		cout << e << " ";
	}
	cout << endl;
}