[C++] 深度剖析list

在这里插入图片描述

文章目录

  • 前言
  • list的介绍
    • list的主要接口函数
      • 构造函数
      • 迭代器
        • 迭代器失效
      • 修改操作
  • list的模拟实现
    • 类的声明
    • 迭代器的实现
    • 构造函数的实现
    • 修改操作
  • 总结


前言

list是C++标准库中STL的一部分,list基于链表结构的类,相信你在阅读了本文后一定能熟练掌握使用list。

list的介绍

list是基于双向链表结构的一种容器,能够在任意位置进行插入与删除,以及双向迭代,因为其内存分布不是连续的,所以并不支持像vector的随机访问。

其特征如下:
  • 支持双向迭代,头插与尾插只需常量级的时间复杂度。
  • 内存空间不连续,无法做到随机访问。
  • 无需移动空间便能在任意结点插入数据。
  • 与forward_list非常相似,但forward_list是单向链表。
  • 是类模板,能够存储任意类型数据
    [C++] 深度剖析list_第1张图片

list的主要接口函数

因为list的接口较多,所以只介绍常见的重要接口

构造函数

list的构造函数接口都是STL的通用接口。

构造函数 接口说明
list(size_type n, const value_type& val = value_type() 构造n个值为val的元素
list() 构造空的list
list(const list& x) 拷贝构造函数
list(InputIterator first, InputIterator last)) 利用迭代器[first,last)的元素构造list
使用演示:
list<int> lt1();	//默认构造函数
list<int> lt2(10, 1);	//创建了10个值为1的元素
list<int> lt3(lt2.begin(), lt2.end());	//利用迭代器进行遍历

迭代器

迭代器是STL的重要功能,提供了一种通用的遍历方式,本文将重点介绍迭代器的实现。

成员函数 功能说明
begin+end 返回第一个元素的迭代器和最后一个元素下一个位置的迭代器
rbegin+rend 返回第一个元素的反向迭代器(end+1)+返回最后一个元素下一个位置的反向迭代器(begin-1)

[C++] 深度剖析list_第2张图片

使用演示:
vector<int> nums {1,2,3,4,5,6};		
list<int> lt(nums.rbegin(),nums.rend());		//用反向迭代器构造获得逆序
list<int>::reverse_iterator it = lt.rbegin();
while(it != lt.rend()) 
{
	cout << *it << " "l		
	++lt;
}
迭代器失效

在这里我们可以把迭代器当成指针,迭代器失效就是指针指向的空间是未知或者已经释放掉的。在之前讨论vector的文章有讲解过,因为vector是连续的内存空间,每次扩容都会导致迭代器失效,而list却有些许不同。当我们使用list进行插入时,并不会使迭代器失效,只有在结点时才会导致迭代器失效,并且只有指向被删除结点的迭代器才会失效。

可以通过以下代码观察到该现象:
	int arr[] = {1,2,3,4,5,6,7,8,9};
	list<int> lt(arr, arr+sizeof(arr)/sizeof(arr[0]));
    
    list<int>::iterator it = lt.begin();
    while(it != lt.end())
    {
        if(*it % 2 == 0)
            lt.erase(it);
        ++it;
    }

报错信息: [1] 2734 segmentation fault (core dumped)

修改操作

成员函数 功能说明
push_front 头插
pop_front 头删
push_back 尾插
pop_back 尾删
insert 在任意结点位置插入
erase 在任意结点位置删除
swap **交换两个list的元素
clear 清理list的有效元素
使用演示:
int main() {
	int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
	//这里也能把指针当作迭代器
    list<int> lt(array, array+sizeof(array)/sizeof(array[0]));	
    lt.pop_back();
    lt.push_front(0);
    lt.insert(lt.end(),10);
    lt.erase(lt.begin());
    for(auto it : lt)
        cout << it << " ";
    cout << endl;
    
    return 0;
}

list的模拟实现

类的声明

因为list是模板类,所以建议将类的声明与定义都写在头文件中,否则编译将会报错,或者你也可以在.cpp文件中使用export关键字来定义成员函数。

template <class T>
struct list_node	//链表结构
{
    T _data;					//数据
    list_node<T>* _next;		//下一个结点
    list_node<T>* _perv;		//上一个结点
    list_node(const T& data = T())		//默认构造函数
        :_data(data)
        ,_next(nullptr)
        ,_perv(nullptr) {}
};

template <class T, class Ref, class Ptr>	//三个参数分别为插入的数据类型,它的引用,与指针
class ListIterator	//自定义迭代器
{
	typedef list_node<T> Node;
	typedef ListIterator<T, Ref, Ptr> self;
	Node* _node;
// ------ 成员数据 --------
};

template<class T>
class list
{
public:
	typedef ListIterator<T, T&, T*> iterator;	//迭代器
	typedef ListIterator<T, const T&, const T*> const_iterator;	//const 迭代器
	typedef list_node<T> Node;			// 链表结构
//	----------- 成员函数 -------------
private:
	Node* _node;		
};

迭代器的实现

你看到上面代码中的迭代器声明可能会有些不解,为什么不是用普通的指针而是用自定义类型,而且还是有这么多参数的自定义类型。我们已经知道,list的内存空间是不是连续的,普通的++操作已经不能够访问到下一个结点,必须要求我们重载这些操作符,至于为什么这么多参数,那都是为了做出const的迭代器的同时复用代码。

试着想一想,如果只用一个模板参数T做迭代器的模板类,那么const迭代器要怎么来呢?const iterator吗?

template <class T>
class ListIterator
{
	typedef list_node<T> Node;
	Node* _node;
	T& operator*() { return *_it->_data; }
	T* operator->() { return &_it->_data; }
	// ------- 成员函数 -------
};

当然不是,const iterator意味着我们不能修改类的成员变量,也就是不能做到++和–等操作。
多定义一个专门的const iterator类也能解决问题,也就是把* ->这两个操作符重载的返回指改为const
但这样代码的复用度太高,于是这也多参数的写法便诞生了,只要把后面两个参数改为引用和指针便
完美解决了问题。

	// T对应数据类型,Ref对应数据引用,Ptr对应数据的指针
    template <class T, class Ref, class Ptr>	
    class ListIterator
    {
        typedef list_node<T> Node;
        typedef ListIterator<T, Ref, Ptr> self;
        Node* _node;
   		Ref operator*() { return _node->_data;}
   		Ptr operator->() { return &_node->}
 	}
 	
	template<class T>
	class list				//
	{
	public:							
		typedef ListIterator<T, T&, T*> iterator;	//迭代器
		typedef ListIterator<T, const T&, const T*> const_iterator;	//const 迭代器
		typedef list_node<T> Node;			// 链表结构
		iterator begin() { return _node->_next;}	//这里隐式转化成了iterator类型
		iterator end() { return _node;}
		const_iterator begin() const { return _node->_next;}
		const_iterator end() const { return _node->_prev; }
	private:
		Node* _node
	}

构造函数的实现

因为我们的list是靠另一个结构体list_node()实现的,不太方便使用初始化列表来进行初始化,所以这里我是用了一个私有函数init()来进行辅助初始化。

template <class T>
void list<T>::init()
{
    _node = new Node;
    _node->_perv = _node;	//这里使用了头结点来方便后续的增删查改操作
    _node->_next = _node;
}
首先是默认构造函数,直接调用init函数便可。
list() { init(); }
拷贝构造函数等
list(const list<T>& l)
{
     init();
     for(auto it : l)	//依次推入新结点
         push_back(it);	
}

list(int n, const T& value)
{
    init();
    for(int i = 0; i < n; ++i)
        push_back(value);
}

修改操作

list的增删查改操作和普通的双向带头链表的增删查改没有多少区别。

void push_back(const T& val)	
{
    Node* cur = new Node(val);
    cur->_next = _node;
    cur->_perv = _node->_perv;
    _node->_perv->_next = cur;
    _node->_perv = cur;
  //  insert(end(), val);
}

void pop_back()
{
    assert(size() > 0);

    Node* pev = _node->_perv->_perv;
    delete _node->_perv;
    _node->_perv = pev;
    pev->_next = _node;
}

template <class T>
typename list<T>::iterator list<T>::erase(iterator pos)
{
	Node* cur = pos._node;
	Node* perv = pos._perv;
	cur->_next->_perv = perv;
	perv->_next = cur->_next;
	
	delete cur;
	return perv->_next;
}

总结

list是双向链表的容器,在插入数据方面比顺序表的速度要快不少,因为它不用挪动数据,但在排序方面的效率就比顺序表要低不少,而且在迭代器的处理上要比vector麻烦上不少。如果需要完整代码,可以到我的github上去寻找。

博客主页:主页
我的专栏:C++
我的github:github

你可能感兴趣的:(c++,list,windows)