【C++杂货铺】探索list的底层实现

这篇具有很好参考价值的文章主要介绍了【C++杂货铺】探索list的底层实现。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

【C++杂货铺】探索list的底层实现,C++杂货铺,c++,list,windows,热门

一、list的介绍及使用

1.1 list的介绍

  • list 是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。

  • list 的底层是双向链表结构,双向链表中的每个元素存储在互不相关的独立节点中,在节点中通过指针指向的前一个元素和后一个元素。

  • list 和 forward_list 非常相似:最主要的不同在于 forward_list 是单链表,只能朝前迭代,已让其更简单高效。

  • 与其它的序列式容器相比(arry、vector、deque),list 通常在任意位置进行插入,移除元素的执行效率更好。

  • 与其它序列式容器相比,list 和 forward_list 最大的缺陷是不支持任意位置的随机访问,比如:要访问 list 的第 5 个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list 还需要一些额外的空间,已保存每个节点的相关联信息。

1.2 list的使用

list 学习时一定要学会查看文档:list的文档介绍,list 在实际中非常重要,在实际中我们熟悉常用的接口就可以,下面列出了需要我们重点掌握的接口。

1.2.1 list的构造

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

小Tips:size_type 表示一个无符号整数类型,value_type 是 list 的第一个模板参数,也就是要存储的数据类型。使用迭代器区间的构造函数是函数模板,只要是满足 Input 类型的迭代器都可以使用该构造函数。

void TestList1()
{
    list<int> l1;                         // 构造空的l1
    list<int> l2(4, 100);                 // l2中放4个值为100的元素
    list<int> l3(l2.begin(), l2.end());  // 用l2的[begin(), end())左闭右开的区间构造l3
    list<int> l4(l3);                    // 用l3拷贝构造l4

    // 以数组为迭代器区间构造l5
    int array[] = { 16,2,77,29 };
    list<int> l5(array, array + sizeof(array) / sizeof(int));

    // 列表格式初始化C++11
    list<int> l6{ 1,2,3,4,5 };

    // 用迭代器方式打印l5中的元素
    list<int>::iterator it = l5.begin();
    while (it != l5.end())
    {
        cout << *it << " ";
        ++it;
    }
    cout << endl;

    // C++11范围for的方式遍历
    for (auto& e : l5)
        cout << e << " ";

    cout << endl;
}

1.2.2 list iterator的使用

此处,大家可暂时将迭代器理解成一个像指针一样的东西,该指针指向 list 中的某个节点。

函数声明 接口说明
begin() + end() 返回第一个元素的迭代器 + 返回最后一个元素下一个位置的迭代器
rebegin() + ren() 返回第一个元素的 reverse_iterator,即 end 位置,返回最后一个一个元素下一个位置的 reverse_iterator,即 begin 位置

注意:begin 与 end 为正向迭代器,对迭代器执行 ++ 操作,迭代器向后移动。rbegin 与 rend 为反向迭代器,对迭代器执行 ++ 操作,迭代器向前移动。由于 list 的底层物理空间并不连续,所以 list 的迭代器不再是原生指针,并且 list 的迭代器没有对 + 和 - 进行重载,只重载了 ++ 和 – ,因为空间不连续,重载 + 会比较复杂。即 l.begin() + 5 是不被允许的。

void PrintList(const list<int>& l)
{
    // 注意这里调用的是list的 begin() const,返回list的const_iterator对象
    for (list<int>::const_iterator it = l.begin(); it != l.end(); ++it)
    {
        cout << *it << " ";
        // *it = 10; 编译不通过
    }

    cout << endl;
}

void TestList2()
{
    int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
    list<int> l(array, array + sizeof(array) / sizeof(array[0]));
    // 使用正向迭代器正向list中的元素
    // list<int>::iterator it = l.begin();   // C++98中语法
    auto it = l.begin();                     // C++11之后推荐写法
    while (it != l.end())
    {
        cout << *it << " ";
        ++it;
    }
    cout << endl;

    // 使用反向迭代器逆向打印list中的元素
    // list<int>::reverse_iterator rit = l.rbegin();
    auto rit = l.rbegin();
    while (rit != l.rend())
    {
        cout << *rit << " ";
        ++rit;
    }
    cout << endl;
}

注意:遍历链表只能使用迭代器和范围 for。

1.2.3 list capacity(容量相关)

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

1.2.4 list element access(元素访问)

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

1.2.5 list modifiers(链表修改)

函数声明 接口说明
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 中的有效元素

小Tips:insert 插入元素并不会导致迭代器失效,例如:相较于 vector 中的 insert,list 中的 insert 并不会去扩容挪动数据,而 vector 中的 insert 可能会进行扩容挪动数据,最终导致迭代器失效。list 中的删除元素接口会导致迭代器失效,失效的只有指向被删除节点的迭代器,其他迭代器不会受到影响。

1.2.6 list operation(对链表的一些操作)

函数声明 接口说明
reverse 对链表进行逆置
sort 对链表中的元素进行排序(稳定排序)
merge 对两个有序的链表进行归并,得到一个有序的链表
unique 对链表中的元素去重
remove 删除具有特定值的节点
splice 将 A 链表中的节点转移到 B 链表

小Tips:链表逆置可以使用 list 自身的接口,也可以使用算法库中的 reverse,二者没有什么区别。链表排序只能使用 list 自身的 sort() 接口(底层是利用归并排序),不能使用算法库的 sort,因为算法库中的 sort 底层是通过快排来实现的,而快排中会涉及到三数取中需要迭代器 - 迭代器,链表不能很好的支持。虽然链表提供了排序接口,但是用链表对数据排序意义不大,效率太低了,更希望用 vector 来对数据进行排序。

void TestSort()
{
    srand(time(0));
    const int N = 5000000;
    vector<int> v;
    list<int> l;

    v.reserve(N);//提前开好空间

    for (int i = 0; i < N; i++)
    {
        auto e = rand();
        v.push_back(e);
        l.push_back(e);
    }

    //开始比较vector 和 list 的排序
    int begin1 = clock();
    sort(v.begin(), v.end());
    int end1 = clock();

    int begin2 = clock();
    l.sort();
    int end2 = clock();

    printf("vector sort:%d\n", end1 - begin1);
    printf("list sort:%d\n", end2 - begin2);
}

【C++杂货铺】探索list的底层实现,C++杂货铺,c++,list,windows,热门

扩展:可以从功能角度对迭代器分为以下 3 类:

迭代器类型 功能
单向(InputIterator) 支持 ++
双向(BidirectionalItreator) 支持 ++/- -
随机(RandomAccessIterator) 支持 ++ / - - / + / -

其中 forward_listunordered_xxx 都是单向迭代器;listmapset 都是双向迭代器;vectorstringdeque 都是随机迭代器。对迭代器的这种分类方式,是由容器的底层结构来决定的。

二、list的模拟实现

2.1 list的节点

template<class T>
struct ListNode
{
	ListNode<T>* _next;
	ListNode<T>* _prev;
	T _val;

	ListNode(const T& val = T())
	{
		_next = nullptr;
		_prev = nullptr;
		_val = val;
	}
};

2.2 list的成员变量

class list
{
	typedef ListNode<T> Node;
public:
	//一些成员函数
private:
	Node* _head;
}

小Tips:typedef 会受到访问限定符的限制,这里没写默认是 private,意味着 Node 这个类型只能在 list 这个类里面使用。链表本质上是一种数据结构,我们只需要维护好一个链表的头节点即可,所以 list 的成员变量就只有一个头节点的指针。

2.3 list的迭代器

list 的迭代器不能再使用原生指针,如果 list 的迭代器使用原生指针的话,那对迭代器解引用得到的是一个节点,而我们希望对迭代器解引用可以得到节点里面存储的元素,并且 list 在底层的物理空间并不连续,如果使用原生指针作为 list 的迭代器,那对迭代器执行 ++ 操作,并不会让迭代器指向下一个节点。因此我们需要对 list 的迭代器进行封装,然后将一些运算符进行重载,以实现迭代器本该有的效果。

2.3.1 普通迭代器

template<class T>
struct _list_iterator
{
	typedef ListNode<T> Node;

	Node* _node;

	_list_iterator(Node* val)
	{
		_node = val;
	}

	T& operator* ()
	{
		return _node->_val;
	}

	T* operator-> ()//迭代器通过->应该指向节点中的元素,因此返回的是一个T类型的地址
	{
		return &(_node->_val);
	}

	bool operator!= (const _list_iterator<T>& right)
	{
		return _node != right._node;
	}

	_list_iterator<T> operator++()
	{
		_node = _node->_next;

		return *this;
	}

	_list_iterator<T> operator++(int)
	{
		_list_iterator<T> tmp(this->_node);

		_node = _node->_next;

		return tmp;
	}
};

小Tips:这里的类名不能直接叫 iterator,因为每种容器的迭代器底层实现可能都有所不同,即可能会为每一种容器都单独实现一个迭代器类,如果都直接使用 iterator,会导致命名冲突。其次,迭代器类不需要我们自己写析构函数、拷贝构造函数、赋值运算符重载函数,直接使用默认生成的就可以,言外之意就是这里使用浅拷贝即可,因为迭代器只是一种工具,它不需要对资源进行释放清理,资源释放清理工作是在容器类中实现的,浅拷贝的问题就出在会对同一块空间释放两次,而迭代器无需对空间进行释放,所以浅拷贝是满足我们需求的。

2.3.2 const 迭代器

上面我们实现了普通迭代器,那 const 迭代器该如何实现呢?直接在容器类里面写上一句 typedef const _list_iterator<T> const_iterator 可以嘛?答案是不可以,const 迭代器本质是限制迭代器指向的内容不能修改,而 const 迭代器自身可以修改,它可以指向其他节点。前面这种写法,const 限制的就是迭代器本身,会让迭代器无法实现 ++ 等操作。那如何控制迭代指向的内容不能修改呢?可以通过控制 operator* 的返回值来实现。但是仅仅只有返回值类型不同,是无法构成函数重载的。那要怎样才能在一个类里面实现两个 operator* 让他俩一个返回普通的 T&,一个返回 const T& 呢?一般人可能想着那就再单独写一个 _list_const_iterator 的类,这样也行,就是会比较冗余,我们可以通过在普通迭代器的基础上,再传递一个模板参数,让编译器来帮们生成呀。除此之外, operator->也需要实现 const 版本,因此还需要第三个模板参数。

template<class T,class Ref, class Ptr>
struct _list_iterator
{
	typedef ListNode<T> Node;
	typedef _list_iterator<T, Ref, Ptr> self;

	Node* _node;

	_list_iterator(Node* val)
	{
		_node = val;
	}

	Ref operator* ()
	{
		return _node->_val;
	}

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

	bool operator!= (const self& right) const
	{
		return _node != right._node;
	}
	bool operator== (const self& right) const
	{
		return _node == right._node;
	}


	self operator++()
	{
		_node = _node->_next;

		return *this;
	}

	self operator++(int)
	{
		self tmp(this->_node);

		_node = _node->_next;

		return tmp;
	}

	self operator--()
	{
		_node = _node->_prev;

		return *this;
	}

	self operator--(int)
	{
		self tmp(*this);
		_node = _node->_prev;

		return tmp;
	}
};
//operator->的使用场景
struct A
{
	A(int a = 0, int b = 0)
	{
		_a = a;
		_b = b;
	}

	int _a;
	int _b;
};

void Textlist3()
{
	wcy::list<A> l;
	l.push_back(A(1, 2));
	l.push_back(A(3, 4));
	l.push_back(A(5, 6));
	l.push_back(A(7, 8));

	wcy::list<A>::iterator it = l.begin();
	while (it != l.end())
	{
		cout << it->_a << ',' << it->_b << " ";
		cout << endl;
		it++;
	}
}

小Tips:上面代码中的 it->_a 会去调用 operator->,返回一个 A 类型的指针,所以这里应该是两个 ->,即 it->->_a ,但是编译器进行了优化,只需要一个 -> 即可。

2.4 list的成员函数

2.4.1 构造函数

list()
{
	_head = new Node;
	_head->_prev = _head;
	_head->_next = _next;
}

小Tips:list 本质上是一个带头双向循环链表。

2.4.2 拷贝构造函数

list(const list& ll)
//list(const list<T>& ll)
{
	_head = new Node;
	_head->_prev = _head;
	_head->_next = _head;

	for (auto& e : ll)
	{
		push_back(e);
	}
}

2.4.3 赋值运算符重载

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

list& operator=(const list ll)
//list<T>& operator=(const list<T> ll)
{
	//现代写法
	swap(ll);

	return *this;
}

小Tips:构造函数和赋值运算符重载函数的形参和返回值类型可以只写类名 list,无需写完整的类型 list<T>,但是不推荐这样写,容易造成混淆,其次现代写法和常规写法在效率上没有任何区别,只是将本来需要我们做的事情交给了编译器去做。

2.4.4 push_back

void push_back(const T& val)
{
	//先找尾
	Node* tail = _head;
	while (tail->_next != _head)
	{
		tail = tail->_next;
	}

	//插入元素
	Node* newnode = new Node(val);
	tail->_next = newnode;
	newnode->_prev = tail;

	newnode->_next = _head;
	_head->_prev = newnode;
}

2.4.5 迭代器相关

iterator begin()
{
	return _head->_next;//单参数的构造函数支持隐式类型转换
}

iterator end()
{
	return _head;
}

const_iterator begin() const
{
	return _head->_next;//单参数的构造函数支持隐式类型转换
}

const_iterator end() const
{
	return _head;
}

2.4.6 insert

iterator insert(iterator pos, const T& val)
{
	//找到 pos 位置的前一个位置
	Node* cur = pos._node;
	Node* prev = cur->_prev;

	//插入元素
	Node* newnode = new Node(val);
	prev->_next = newnode;
	newnode->_prev = prev;

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

	return newnode;
}

2.4.7 erase

iterator erase(iterator pos)
{
	assert(pos != end());
	Node* cur = pos._node;//保存当前节点
	Node* prev = cur->_prev;//保存前一个节点
	Node* next = cur->_next;//保存后一个节点
	
	prev->_next = next;
	next->_prev = prev;

	delete cur;
	cur = nullptr;

	return next;
}

2.4.8 push_front

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

2.4.9 pop_back

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

2.4.10 pop_front

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

2.4.11 size

size_t size()
{
	size_t sz = 0;
	iterator it = begin();

	while (it != end())
	{
		it++;
		sz++;
	}

	return sz;
}

2.4.12 clear

void clear()
{
	iterator it = begin();

	while (it != end())
	{
		it = erase(it);
	}
}

2.4.13 析构函数

~list()
{
	clear();

	delete _head;
	_head = nullptr;
}

小Tips:clear 和 析构函数的主要区别在于是否释放头节点。

三、结语

今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,春人的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是春人前进的动力!

【C++杂货铺】探索list的底层实现,C++杂货铺,c++,list,windows,热门文章来源地址https://www.toymoban.com/news/detail-696646.html

到了这里,关于【C++杂货铺】探索list的底层实现的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【C++杂货铺】优先级队列的使用指南与模拟实现

    优先级队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的。 此上下文类似于堆,在堆中可以随时插入元素,并且只能检索最大堆元素(优先级队列中位于顶部的元素)。 优先级队列被实现为容器适配器,容器适配器即将特定容器

    2024年02月09日
    浏览(46)
  • 【C++杂货铺】模板

    📖 实现一个通用的交换函数 想要实现一个通用的交换函数不难,借助函数重载就可以。函数重载小伙伴们还记得嘛👀,忘了的小伙伴可以走传送门回去复习一下。如上面代码所示,我们借助函数重载实现了三份 Swap 函数,分别用来交换两个整型变量、两个双精度浮点型变量

    2024年02月09日
    浏览(50)
  • 【C++杂货铺】引用

    前言:  相信大家在学习C语言的时候,最头疼的就是指针,经常会碰到一级指针、二级指针,这些指针使用起来,稍有不慎就会等导致程序崩溃,为了让广大程序员少掉点头发,C++中提出了 引用 这一概念。当然,在C++的代码中,仍然可以兼容C语言的指针。  在语法上 引用

    2024年02月16日
    浏览(48)
  • 【C++杂货铺】内存管理

    从用途和存储的角度来看,在C/C++程序中有 局部数据、静态数据、全局数据、常量数据、动态申请的数据 五种主要的数据,各种数据的特点如下: 局部数据 :随用随创建,存储在栈区,作用域只在局部,生命周期在局部,出了作用域就销毁。 静态数据 :存储在数据段,作

    2024年02月16日
    浏览(42)
  • 【C++杂货铺】内管管理

    目录 🌈前言🌈 📁 C/C++中内存分布 📁 new 和 delete的使用 📁 new 和 delete的优点 📁 new 和 delete的原理  📂 operator new 和 operator delete函数  📂 内置类型  📂 自定义类型 📁 内存泄漏 📁 总结         欢迎收看本期【C++杂货铺】,本期内容讲解C++内存管理。包含了C++中内存

    2024年04月14日
    浏览(48)
  • 【C++杂货铺】详解string

    目录  🌈前言🌈 📁 为什么学习string 📁 认识string(了解) 📁 string的常用接口  📂 构造函数  📂 string类对象的容量操作  📂 string类对象的访问以及遍历操作​编辑  📂 string类对象的修改操作 📁 模拟实现string 📁 总结         欢迎观看本期【C++杂货铺】,本期内容

    2024年03月20日
    浏览(45)
  • 【C++杂货铺】拷贝构造函数

    📖 定义 拷贝构造函数 是构造函数的一个重载 ,它的本质还是 构造函数 ,那就意味着,只有在创建对象的时候,编译器才会自动调用它,那他和普通的构造函数有什么区别呢? 拷贝构造函数,是创建对象的时候,用一个已存在的对象,去初始化待创建的对象 。简单来说,

    2024年02月16日
    浏览(52)
  • 【C++杂货铺】运算符重载

    本文将以日期类为基础,去探寻运算符重载的特性与使用方法,下面先给出日期类的基础定义: 备注 :拷贝构造函数和析构函数,均可以不写,因为当前日期类的三个成员变量都是内置类型,没有动态申请空间,使用浅拷贝就可以。 📖 如何比较两个日期的大小? 现如今,

    2024年02月16日
    浏览(74)
  • 【C++杂货铺】缺省参数、函数重载

     缺省参数是 声明或定义函数时为函数的参数指定一个缺省值 。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。  上面代码在 fun 函数的形参部分给了缺省值10,这意味着在调用 fun 函数的时候可以传参,也可以不传参,如果传参了那形参

    2024年02月16日
    浏览(38)
  • 【C++杂货铺】C++介绍、命名空间、输入输出

     C语言是 结构化 和 模块化 的语言,适合处理 较小规模 的程序。对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适。为了解决软件危机,20世纪80年代,计算机界提出了 OOP (object oriented programming: 面向对象 )思想,支持面向对象的程序设计语言应

    2024年02月16日
    浏览(42)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包