【C++】STL库_list 的模拟实现

list可以看作是双向循环链表,list跟前面的vector和string使用方法大多相同,这里主要是模拟实现。

因为底层为链表的数据结构,是非线性的,所以其迭代器不能使用原生指针,要重新定义迭代器的操作,这里非常重要,因为后面大多数写法跟这里差不多。


一、list 部分函数

转移数据,把后一个链表数据转移到前另一个链表

删除指定数据

数据去重

用list操作去重操作,效率并不高,后面又更高效的去重类型。

排序

list 底层使用的是归并排序,不用消耗额外的空间。默认是升序,可以改。

二、模拟实现 list

list 包含了类和对象的很多基本要点,要是类和对象不熟悉的话,学和写都会比较困难,难以理解。

首先这里把节点、链表、迭代器,分成了三个部分(三个类)来写。

节点:

cpp 复制代码
//节点
template<class T>
class list_node
{
public:
	T _data;
	list_node<T>* _next;
	list_node<T>* _pre;

	//拷贝构造
	list_node(const T& x = T())
		:_data(x)                //自定义类型去调用自己的拷贝构造
		, _next(nullptr)
		, _pre(nullptr)
	{
	}
};

这里链表节点的写法没什么要点,跟 前面C语言链表节点的写法大同小异。因为不知道节点具体存储的什么数据,所以这里使用了模板,使其变的更为灵活。

迭代器:

cpp 复制代码
//迭代器
template<class T>
struct __list_iterator     //!注意这因为这里要经常使用迭代器的成员,所以定义成了结构体,当然也可以使用class
{
	typedef list_node<T> Node;
	//缩减一下长度
	typedef __list_iterator<T> iterator;

	Node* _node;    
}

PS: Node、iterator 共用同一个模板参数 T 。

跟vector不同,vector的迭代器是原生指针,其迭代器的操作可以直接当指针用。

这里不能使用原生指针,list 是一个非线性的容器。且 node* 解引用得到的是节点,但是这里迭代器解引用要得到是节点中的数据(_data),所以这里迭代器的操作要重新写。

这里拷贝构造函数可写可不写,直接用默认的浅拷贝就行,把一个迭代器赋值给另一个迭代器,这里希望的就是另一个迭代器也指向同一个节点。使用深拷贝反而不符合其本意。

迭代器类不用写析构函数,因为这里节点是属于链表的,这里只是封装起来进行操作。

构造函数:

cpp 复制代码
//构造函数 使用节点的指针
__list_iterator(Node* node)
	:_node(node)           
{ 

}

迭代器的精华,用一个节点的指针就能定义迭代器。

迭代器操作:

cpp 复制代码
iterator begin()
{
	return iterator(_head->_next);
}

iterator end()
{
	return iterator(_head);
}

PS:这里迭代器是开区间(左闭右开),end() 是链表尾部的下一个位置,所以end()指向哨兵头节点的位置,begin()指向哨兵头节点的下一个位置。

操作符重载:

cpp 复制代码
bool operator!=(const iterator& it)const
{
	return _node != it._node;
}

bool operator==(const iterator& it)const
{
	return _node == it._node;
}

PS:因为 list 是一个非线性的表,其节点指针位置是不确定的,有可能不再同一段内存空间,所以并不能使用 大于(>)、小于(<)等比较符号,所以也不用重载它们。

cpp 复制代码
//++it    前置
iterator& operator++()
{
	_node = _node->_next;
	return *this;
}

//it++    后置
iterator operator++(int)
{
	iterator temp(*this);
	_node = _node->_next;
	return temp;
}

iterator& operator--()
{
	_node = _node->_pre;
	return *this;
}

iterator operator--(int)
{
	iterator temp(*this);
	_node = _node->_pre;
	return temp;
}

解引用、箭头重载:

cpp 复制代码
//重载解引用
//*it 转换成 it.operator*(),然后再返回节点里面数据的引用,可以读也可以写
T& operator*()
{
	return _node->_data;
}

迭代器是像指针一样的对象。一般结构体指针才会使用箭头,用箭头来访问结构体成员。

这里迭代器 it 解引用,返回节点中 T (Pos)类型数据(_data)的引用(可读可写)。

cpp 复制代码
//重载箭头
T* operator->()
{
	return &(operator*());
}

这种写法比较难以理解,其实 operator*() 可以直接替换为 _node->_data。

cpp 复制代码
T* operator->()
{
	return &(_node->_data);
}

这里会发现有两个箭头,但是下面用一个箭头就能调用到节点的数据。

这里相当于隐藏了一个->。这里重载->,返回了一个结构体的指针,结构体的指针再使用->寻找成员。

cpp 复制代码
it->_node->data。

PS:这里隐藏的是后面那个箭头,第一个箭头是迭代器的运算符重载。

这里返回的是 T 的指针,这里T是Pos。Pos* 结构体的指针就可以使用箭头访问成员。

cpp 复制代码
it->Pos->_a1。

const迭代器:

这里无法遍历,要用 const 迭代器接收。普通迭代器可读可写,但是 const 迭代器是只读的。

const list<int>中的const修饰的是整个list<int>对象本身,而不是其元素类型int。具体来说:

  1. 列表的常量性 ‌:const表示列表对象本身不可修改。例如,不能调用push_back()pop_back()等修改列表结构的成员函数。
  2. 元素的访问权限 ‌:通过const list<int>访问元素时,迭代器会变为const_iterator,解引用得到的元素是const int&(视为只读)。但这不改变模板参数int的本质,而是由列表的常量性隐式限制了对元素的修改权限。

总结 ‌:const修饰的是list本身,而非int。列表的结构和元素都不可通过该常量对象修改,但元素类型仍是int(非const int)。

cpp 复制代码
const T& operator*()
{
	return _node->_data;
}

核心问题在于‌如何通过返回值的 const 属性来控制数据的读写权限 ‌。operator*() 的返回类型决定了通过迭代器解引用后能否修改数据。

通过控制 operator*() 的返回类型(T&const T&),可以决定迭代器的读写权限。这种设计是 C++ ‌const 正确性的核心思想,也是 STL 迭代器实现的基石。

这里怎么改很关键,需要为迭代器类提供两种版本的 operator*(),但是没参数所以这里没法写重载函数。

cpp 复制代码
//list 迭代器
template<class T,class Ref,class Ptr>
struct __list_iterator
{
	typedef list_node<T> Node;
	typedef __list_iterator<T, Ref, Ptr> iterator;

	Node* _node;
}

迭代器类这里多加两个模板参数 Ref 和 Ptr 用于‌统一管理迭代器的引用和指针类型 ‌,使得同一个迭代器模板可以同时支持普通迭代器和常量迭代器

cpp 复制代码
//链表
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; //const 迭代器

private:
    Node* _head ;

}

避免代码重复‌:通过模板参数控制引用和指针类型,可以复用同一份迭代器代码,无需为普通迭代器和常量迭代器分别编写类。

这里普通迭代器和 const 迭代器是两个类型,根据模板参数的不同,实例化了两个对象。

cpp 复制代码
//const T& operator*()
Ref operator*()
{
	return _node->_data;
}
//const T* operator->()
Ptr operator->()
{
	return &(operator*());
}
  • Ref ‌:控制解引用操作符(operator*)的返回类型,决定数据是否可修改。
    • 若为普通迭代器:Ref = T&,允许通过迭代器修改数据。
    • 若为常量迭代器:Ref = const T&,禁止通过迭代器修改数据。
  • ‌**Ptr** ‌:控制箭头操作符(operator->)的返回类型,决定指针的读写权限。
    • 若为普通迭代器:Ptr = T*,允许通过指针修改数据。
    • 若为常量迭代器:Ptr = const T*,禁止通过指针修改数据。·
cpp 复制代码
const_iterator begin()const
{
	return const_iterator(_head->_next);
}

const_iterator end()const
{
	return const_iterator(_head);
}

反向迭代器:

链表:

cpp 复制代码
链表
template<class T>
class list
{
	typedef list_node<T> Node; 
public:
	//typedef __list_iterator<T> iterator; //迭代器
    typedef __list_iterator<T,T&,T*> iterator;
    typedef __list_iterator<T,const T&,const T*> const_iterator;

private:
    Node* _head ; //哨兵位的头节点
}

这里要注意的是 typeddef 在 private 下是只能在类的内部使用,所以这里iterator这里要定义在 public 下方便在外部使用。

构造函数:

cpp 复制代码
list()
{
	_head = new Node;
	_head->_next = _head;
	_head->_pre = _head;
}

插入节点、删除节点:

cpp 复制代码
//尾部插入数据
void push_back(const T& x)
{
	Node* tail = _head->_pre;
	Node* newnode = new Node(x);

	tail->_next = newnode;
	newnode->_pre = tail;
	newnode->_next = _head;
	_head->_pre = newnode;
}

操作跟前来链表的操作一样,注意一下节点连接的代码顺序就行。

只有一个节点(又当头又当尾 )时插入数据也没有问题。

cpp 复制代码
//删除 这里使用了迭代器
iterator erase(iterator pos)
{
	assert(pos != end());     //断言 不能删除哨兵位的头节点
	Node* cur = pos._node;    //!注意pos不是指针,使用的是'.',访问结构体成员
	Node* pre = cur->_pre;
	Node* next = cur->_next;

	pre->_next = next;
	next->_pre = pre;
	
	delete cur;
	return iterator(next);
}

注意这里pos节点被删除了,pos如果再使用就是野指针了,发生了迭代器失效,如果要再继续使用pos可以使其接受erase()函数的返回值,其返回的是pos节点下一个节点位置,这里的话就是 4 的位置。

cpp 复制代码
pos = lt_1.erase(pos); 
cpp 复制代码
//任意位置插入节点
iterator insert(iterator pos,const T& x)
{
	Node* cur = pos._node;
	Node* pre = cur->_pre;

	Node* newnode = new Node(x);

	newnode->_next = cur;
	newnode->_pre = pre;
	pre->_next = newnode;
	cur->_pre = newnode;

	return iterator(newnode);
}

insert()这里插入数据后,pos并没有失效,还可以继续使用,但是指向的位置发生了改变。原来指向容器的第二个位置,插入数据过后指向了第三个位置。如果想让其继续指向第二个位置,让其接收insert()函数的返回值。

cpp 复制代码
pos = lt_1.insert(pos, 30);

函数复用:

cpp 复制代码
//尾插
void push_back(const T& x)
{
    insert(end(),x);
}
//头插
void push_front(const T& x)
{
    insert(begin(),x);
}
//尾删
void pop_back()
{
	erase(--end());
}
//头删
void pop_front()
{
	erase(begin());
}

析构函数:

cpp 复制代码
void clear()
{
	iterator it = begin();
	while (it!=end())
	{
		it = erase(it);
	}
}

~list()
{
	clear();
	delete _head;
	_head = nullptr;
}

clear()清除数据,不清除头节点。

拷贝构造函数:

cpp 复制代码
void empty_init()
{
    //创建并初始化哨兵位的头节点
	_head = new Node;
	_head->_next = _head;
	_head->_pre = _head;
}
//构造函数模板
//通过输入迭代器范围[first, last)来构造一个包含相同元素的新链表
template<class InputIterator>
list(InputIterator first, InputIterator last)
{
    empty_init();
    while (first!=last)
    {
        push_back(*first);
        ++first;
    }
}

//前面构造函数可以改写成这样    
list()
{
	empty_init();
}


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

//拷贝构造
list(const list<T>& lt)
{
	empty_init();
	list<T> tmp(lt.begin(), lt.end());
	swap(tmp);

}

现代写法,使用迭代器区间构造 list 。

适配器:

相关推荐
啊阿狸不会拉杆11 分钟前
第十五章:Python的Pandas库详解及常见用法
开发语言·python·数据分析·pandas
rqtz2 小时前
【C++指针】搭建起程序与内存深度交互的桥梁(下)
开发语言·c++·指针
小陈的进阶之路2 小时前
数据结构(并查集,图)
数据结构·c++·算法
AI让世界更懂你2 小时前
Python 包管理器 UV 全面介绍
开发语言·python·uv
IT猿手3 小时前
基于烟花算法(Fireworks Algorithm,FWA)及三次样条的机器人路径规划,50个场景任意选择,完整MATLAB代码
开发语言·算法·机器学习·matlab·机器人·无人机
SNAKEpc121383 小时前
在MFC中使用Qt(三):通过编辑项目文件(.vcxproj)实现Qt的自动化编译流程
c++·qt·mfc
厌世小晨宇yu.3 小时前
对Gpt关于泛型、Stream的提问
java·开发语言·gpt·ai
了一li3 小时前
2025年春招-Linux面经
开发语言·php
lllsure3 小时前
SpringMVC 拦截器(Interceptor)
java·开发语言·mysql
lsx2024064 小时前
MVC 文件夹:架构之美,开发之魂
开发语言