C++list详解

C++list详解


递归何不归:个人主页
个人专栏 : 《C++庖丁解牛》《数据结构详解》

在广袤的空间和无限的时间中,能与你共享同一颗行星和同一段时光,是我莫大的荣幸


一,list是什么

list即是带头双向链表,是顺序表的一种,以链式结构实现。

二,list的迭代器

迭代器是一种可以遍历容器中元素的对象,提供了统一的访问接口。

但是不同的迭代器在操作上是存在差异的,

例如list 的迭代器受限于list的链式结构,无法实现vector 迭代器可以实现的+=、- =操作

2.1迭代器类型

我们这里详细说一下常见的迭代器类型

迭代器类型 方向 读写权限 支持的操作 典型容器 标签
输入迭代器 (Input Iterator) 单向 (→) 只读 (一次) ++ *(读) == != istream_iterator input_iterator_tag
输出迭代器 (Output Iterator) 单向 (→) 只写 (一次) ++ *(写) ostream_iterator output_iterator_tag
前向迭代器 (Forward Iterator) 单向 (→) 读写 (多次) ++ * -> == != forward_list unordered_set forward_iterator_tag
双向迭代器 (Bidirectional Iterator) 双向 (↔) 读写 ++ -- * -> == != list set map bidirectional_iterator_tag
随机访问迭代器 (Random Access Iterator) 随机 (跳跃) 读写 + - += -= [] < > <= >= vector deque array random_access_iterator_tag

我们可以一一对照,发现:

  • vector 的迭代器是随机访问迭代器
  • list 的迭代器是双向迭代器

三,简述emplace_back接口

emplace_back和push_back都是在尾部插入一个元素,但是emplace_back是可变参数模版,这意味着他可以支持隐式类型转换

四,杂项接口细节

4.1简单概述

接口 作用
empty 检测list是否为空,是返回true,否则返回false
size 返回list中有效节点的个数
front 返回list的第一个节点中值的引用
back 返回list的最后一个节点中值的引用
接口 作用
push_front 在list首元素前插入值为val的元素
pop_front 删除list中第一个元素
push_back 在list尾部插入值为val的元素
pop_back 删除list中最后一个元素
insert 在list position 位置中插入值为val的元素
erase 删除list position位置的元素
swap 交换两个list中的元素,最好使用这个,使用std::swap会因为拷贝而产生较大的开销
clear 清空list中的有效元素
merge 合并链表,需要两个链表有序,底层有点类似于归并排序的逻辑,双指针遍历两个链表找小插入在返回拷贝。被合并的list会变空。
unique 去重,同样要求链表有序,原因是底层是默认相同的数据被放在一起。
remove 通过元素的值来移除元素。

此处还有一个splice接口,我们待会再讲

4.2细讲splice接口


这是splice函数的函数原型

splice的作用简单来说就是剪切,即将一个list中的一些节点转移到另一个list中去

cpp 复制代码
void test_list1()
{
	list<int> l1 = { 1,2,3,4,5 };
	list<int> l2;
	l2.splice(l2.begin(),l1, l1.begin(), l1.end());
}

可以看到,l1中的数据被转移到了l2中

五,list的排序效率问题

list并没有sort的函数重载 ,我们需要调list::sort来对list进行排序

但是

先将list拷贝到vector中进行排序再拷贝回来

六,底层探究:手写一个list

6.1结构介绍

list主要实现的是三个部分:

  • 1、单个的节点
  • 2、迭代器
  • 3、list本体

6.2结点类的实现

cpp 复制代码
template<class T>
struct ListNode
{
    ListNode(const T& val = T())
    {
        _val = val;
        _pPre = nullptr;
        _pNext = nullptr;
    }
    ListNode<T>* _pPre;
    ListNode<T>* _pNext;
    T _val;
};

6.3迭代器的实现逻辑(重点)

迭代器的底层其实就是原生指针,但是迭代器的实现是一个封装过的类

为什么要将指针封装起来?答:方便++、--等函数的重载

6.3.0函数的声明和成员变量

此处存在一个问题:我们需要实现iterator和const_itreator两种迭代器,应该如何将这两种迭代器整合在一个class中呢

这里先埋下一个伏笔,后面再讲

cpp 复制代码
template<class T, class Ref, class Ptr>
struct ListIterator
{
    typedef ListNode<T>* PNode;
    typedef ListIterator<T, Ref, Ptr> Self;
}

6.3.1operator*

cpp 复制代码
//Ref == T& or const T&
 Ref operator*()
 {
     return _pNode->_val;
 }

6.3.2operator++和--

cpp 复制代码
Self& operator++()
{
    _pNode = _pNode->_pNext;
    return *this;
}
Self operator++(int)
{
    Self ret = *this;
    _pNode = _pNode->_pNext;
    return ret;
}
Self& operator--()
{
    _pNode = _pNode->_pPre;
    return *this;
}
Self operator--(int)
{
    Self ret = *this;
    _pNode = _pNode->_pPre;
    return *this;
}

6.3.3operator==与!=

cpp 复制代码
 bool operator!=(const Self& l)
 {
     return _pNode != l._pNode;
 }
 bool operator==(const Self& l)
 {
     return _pNode == l._pNode;
 }

6.3.4operator->

这里还是有一点讲究的

cpp 复制代码
//Ptr==const T* orT*
 Ptr operator->()
 {
     return &_pNode->_val;
 }

这里调用的时候其实是跳过了一个步骤:其实本来应该是两个解引用箭头才合理
此处是经过特殊处理

6.4list类的实现逻辑

list基于带头双向链表实现

list的成员变量包括哨兵结点_head 和 元素个数_size

6.4.1先前的迭代器问题

我们之前写迭代器的类的时候在类的模版中加入了更多的参数

cpp 复制代码
template<class T, class Ref, class Ptr>

此时我们在list类这样写:

cpp 复制代码
public:
    typedef ListIterator<T, T&, T*> iterator;
    typedef ListIterator<T, const T&, const T*> const_iterator;

这实际上就是通过后两个参数来区分cosnt_iterator和iterator

就是将本来是我们做的工作交给了编译器 ,我们这里通过模版来实现大部分相似的功能,在通过参数区分这两种迭代器

6.4.2构造&拷贝构造&赋值重载&析构

cpp 复制代码
list()
{
    CreateHead();
}
list(int n, const T& value = T())
{
    CreateHead();
    for (int i = 0; i < n; i++)
    {
        push_back(value);
    }
}
template <class Iterator>
list(Iterator first, Iterator last)
{
    CreateHead();
    while (first != last)
    {
        push_back(*first);
        //此处也不会破坏封装,还是不要直接访问来的好
        ++first;
    }
}
list(const list<T>& l)
{
    CreateHead();
    //这里是还在构造过程中,所以如果直接使用this就会产生无限递归的问题
    /*auto it = l.begin();
    while (it != l.end())
    {
        push_back(it._pNode->_val);
        ++it;
    }*/

    for (auto it : l)
    {
        push_back(it);
    }
}
list<T>& operator=( list<T> l)
{
    swap(l);
    return *this;
}
~list()
{
    clear();
    delete _head;
    _head = nullptr;
}

void clear()
{
    auto it = begin();
    while (it != end())
    {
        it = erase(it);
    }
    _size = 0;
    _head->_pNext = _head;
    _head->_pPre = _head;
}
void swap(list<T>& l)
{
    std::swap(_head, l._head);
    std::swap(_size, l._size);
}

private:
void CreateHead()
{
    _head = new Node;
    _head->_pNext = _head;
    _head->_pPre = _head;
    _size = 0;
}

6.4.3insert和erase的实现

cpp 复制代码
		iterator insert(iterator pos, const T& val)
        {
            PNode new_node = new Node(val);
            
            Node* _pre = pos._pNode->_pPre;
            Node* _next = pos._pNode;
            
            _pre->_pNext = new_node;
            new_node->_pPre = _pre;

            _next->_pPre = new_node;
            new_node->_pNext = _next;

            _size++;
            return iterator(new_node);
        }
        // 删除pos位置的节点,返回该节点的下一个位置
        iterator erase(iterator pos)
        {
            Node* _pre = pos._pNode->_pPre;
            Node* _next = pos._pNode->_pNext;

            _pre->_pNext = _next;
            _next->_pPre = _pre;

            delete pos._pNode;

            _size--;
            return iterator(_next);
        }

6.5迭代器失效

由于list的链式结构,插入操作不会使迭代器失效,但是删除操作会导致迭代器失效

  • insert操作后,迭代器仍然指向原来的位置
  • erase操作后,当前位置的迭代器指向的空间被释放,迭代器失效,但是当前位置前后的迭代器没有失效

6.6特殊的构造方式(隐式类型转换)

我们常常可以看到这样的构造方法:

cpp 复制代码
list<int> lt1 = { 1,2,3,4,5,6,7,8 };

这种方法实际上是先将{}里的数据写入到可以使用迭代器的initializer_list类型中 ,然后再使用initializer_list类型区间构造

cpp 复制代码
void test_list4()
	{
		// 直接构造
		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 };

		func(lt0);
		func({ 1,2,3,4,5,6 });

		print_container(lt1);
		
		//auto il = { 10, 20, 30 };
	/*	initializer_list<int> il = { 10, 20, 30 };
		cout << typeid(il).name() << endl;
		cout << sizeof(il) << endl;*/
	}
}
cpp 复制代码
list(initializer_list<T> il)
	{
		empty_init();
		for (auto& e : il)
		{
			push_back(e);
		}
	}

6.7按需实例化的体现

我们会发现,在我们没有调用模版的时候 ,模版中的一些非语法错误是不会被检查出来的

这是因为在这个模版没有被调用的时候 ,编译器是不会实例化这个模版的,这样的话就检查不出来一些**"不是很明显"**的错误

cpp 复制代码
template<class Contianer>
void printf_contianer(const Contianer& con)
{

	auto it = con.begin();
	while (it != con.end())
	{
		*it += 10;
		//如果没有使用这个模版,这里就不会报错
		++it;
	}
	cout << endl;

	for (auto e : con)
	{
		cout << e << " ";
	}
	cout << endl;
}
相关推荐
仰泳的熊猫2 小时前
题目2268:蓝桥杯2016年第七届真题-密码脱落
数据结构·c++·算法·蓝桥杯
longson.2 小时前
本地部署 MuseTalk 数字人(windows)
windows
山栀shanzhi2 小时前
C++ 核心机制解析:#pragma once 与 extern 的具体职责与区别
开发语言·c++·面试
Yusei_05232 小时前
C++14入门
c++·算法
行稳方能走远2 小时前
从轮询到回调再到观察者——嵌入式应用层感知底层变化的三种姿势
c++
知无不研2 小时前
中介者模式
c++·设计模式·中介者模式
机器小乙2 小时前
【开源】2 分钟在 Windows 上搭建 AI Agent 运行环境:MachineY Engine 使用指南
人工智能·windows·ai·开源·openclaw
crescent_悦2 小时前
PTA C++:正整数A+B
数据结构·c++·算法
YYYing.3 小时前
【Linux/C++多线程篇(一) 】多线程编程入门:从核心概念到常用函数详解
linux·开发语言·c++·笔记·ubuntu