C++标准库中的链表(list双向链表)

C++标准库中的链表使用了allocator和iterator实现。因为默认的allocator就是调用new和delete,所以我这里直接使用了new和delete。且仅实现了一些常用的操作,这里只是仿照标准库实现,和标准库有差别。(const的iterator没写,list拷贝构造,拷贝赋值,移动构造,移动赋值都没写,以后有时间再来吧)

在C++中list是一个双向的、环状的链表,清楚了这个概念我们就不难理解标准库中的操作。


一、list_iterator

"list_iterator.h"

#pragma once
#include <iostream>
#include "list_node.h"

template<typename T>
struct list_iterator
{
	typedef T value_type;
	typedef T* pointer;
	typedef T& reference;
	typedef std::bidirectional_iterator_tag iterator_category;
	typedef std::ptrdiff_t difference_type;      
    //两个迭代器之间的距离,这里实际上用的是longlongint
    //每个迭代器都需要定义这5个类型,方便算法获取
    //为了代码的可读性,我没有使用这里定义的5个类型

	list_iterator(list_node<T>* node) noexcept   
		: node(node) {}
    //因为链表需要通过迭代器来操作节点,进行插入删除等操作,
    //所以这里的参数不能是const

	list_iterator() noexcept
		: node(nullptr) {}

	list_iterator(const list_iterator<T>& it) noexcept
		: node(it.node) {}
    //使用const确保拷贝过程中不修改迭代器

	inline T& operator*() const noexcept { return (*node).data; }
	inline T* operator->() const noexcept { return &(operator*()); }
	
	inline list_iterator<T>& operator++() noexcept {
		node = node->next;
		return *this;
	}

	inline list_iterator<T> operator++(int) noexcept {
		list_iterator<T> tmp = *this;
		++*this;
		return tmp;
	}

	inline list_iterator<T>& operator--() noexcept {
		node = node->prev;
		return *this;
	}

	inline list_iterator<T> operator--(int) noexcept {
		list_iterator<T> tmp = *this;
		--*this;							
		return tmp;
	}

	inline bool operator==(const list_iterator<T>& it) const noexcept {
		return it.node == node;
	}

	inline bool operator!=(const list_iterator<T>& it) const noexcept {
		return it.node != node;
	}

	list_node<T>* node;
};

这里的迭代器是一个pointer-like-class,就是将一个类作为指针使用。(将一个类作为指针使用,必须重载所有指针具有的操作)

为什么不使用原生指针呢,是因为链表在内存中的分布空间不是连续的,使用原生指针++无法获取到指针下一个节点的位置,这时候就可以使用迭代器这样一个智能的指针来管理链表,重载++等操作符以满足我们操作链表的需求。(而像array,vector这样内存空间连续的容器就不需要,使用原生指针即可)

而且使用迭代器可以帮助我们很轻松地访问链表中的元素。在C++标准库中,强调将数据与操作数据的方法(算法)分离,这时候就需要通过迭代器来访问容器中的数据。以往我们在链表中特定位置插入一个数据时,需要从链表的头指针开始向后遍历直到对应位置才能进行插入操作,而使用迭代器可以配合std::advance直接进行插入操作。

list_iterator<T> insert(const list_iterator<T>& pos, const T& value) { 
	list_node<T>* new_node = new list_node<T>;
	new_node->data = value;
	list_node<T>* pos_node = pos.node;

	new_node->prev = pos_node->prev;
	new_node->next = pos_node;

	pos_node->prev->next = new_node;
	pos_node->prev = new_node;

	++size;

	return list_iterator<T>(new_node);
}

int main(){
    list<int> a{1,2,3};
	list_iterator<int> it = a.begin();
	a.show();
	//1 2 3

    advance(it, 3);
	a.insert(it,10);
	a.show();
	//11 1 2 3 10 4
}

1.关于noexcept

C++标准规定,迭代器的基本操作不能抛出异常。这里的所有函数都使用了noexcept,这样可以节省开销(如果涉及异常处理,就需要生成栈展开代码,会造成额外性能开销),若涉及动态内存操作则有可能会抛出异常,此时就不能使用noexcept。


 2.构造函数
__list_iterator(__list_node<T>* node)
		: node(node) {}

不使用explicit,允许从节点指针隐式构造迭代器。

__list_node<int>* node = ...;
__list_iterator<int> it = node;  // 若使用 explicit 则禁止隐式转换

这里的参数不使用const, 因为链表需要通过迭代器来操作节点,进行删除插入等操作,所以不能是const。

__list_iterator(const __list_iterator<T>& it)
	: node(it.node) {}

不使用explicit,允许迭代器隐式复制。

__list_iterator<int> it1 = ...;
__list_iterator<int> it2 = it1;  // 若使用 explicit 则禁止隐式转换

 使用const,确保拷贝过程中不修改迭代器。


 3.析构函数

iterator不需要析构函数,因为迭代器不具有节点的所有权,也不进行内存的分配,所以不需要特殊清理,应该交由list实现。在这里实现析构函数反而是一个危险的操作。


 4.operator*与operator->的重载
inline T& operator*() const noexcept { return (*node).data; }
inline T* operator->() const noexcept { return &(operator*()); }

如果要获取iterator指向的节点直接使用it.node(结构体中的成员默认为public),如果要获取iterator指向节点所存储的数据,则使用*it

使用迭代器访问容器中元素的成员时有两种方法:(*it).member或it->member
C++规定->运算符会递归调用,直到返回一个原始指针,然后通过该指针访问成员。
这里的operator->调用了operator*,在外部调用it->value
等价于(it.operator->())->value
等价于&(*it)->value(对(*node).data取地址,获得的是node中data(T)类型的指针)
如果data(T)是Foo类型,那么就相当于*Foo->value

为什么operator->返回的是一个指针而不是引用?
因为只有指针或重载了operator->的类能使用->,如果返回的是引用类型的话会报错。


5.operator++与operator--的重载
//++iterator	
inline __list_iterator<T>& operator++() noexcept{ 
    node = node->next;
    return *this;						
}

//iterator++
inline __list_iterator<T> operator++(int) noexcept{ 
    __list_iterator<T> tmp = *this;    
    //这里调用拷贝构造函数
    ++*this;						   
    //调用了operator++重载,也就是node = node -> next
    //后面的operator--同理,node = node -> prev
    return tmp;
}

这里的return *this解引用得到迭代器本身,而不会调用operator*重载。因为this是一个指针,*this是对指针解引用,而不是对对象使用operator*,只有当*it时才会调用我们重载的operator*。

为什么前置++返回的是引用,后置++返回的是值?
C++不允许对后置++进行链式调用

++++i; 相当于 ++(++i); 合法
i++++; 相当于 (i++)++; 非法

因为后置++返回的是一个临时的副本,即右值,我们无法修改一个右值。


6.operator==与operator!=的重载
inline bool operator==(const __list_iterator<T>& it) const noexcept {
	return it.node == this.node;
}

inline bool operator!=(const __list_iterator<T>& it) const noexcept {
	return it.node != this.node;
}

注意这里使用it.node来获取迭代器的节点进行比较,对于普通指针,p1 == p2 比较的是它们是否指向同一内存地址,链表迭代器的 == 运算符同样比较底层指针,正确模拟了指针的行为。不使用it->node,因为iterator是一个结构体,可以通过it.node直接获取(struct成员默认public)。如果这里使用it->node就变成了irerator.node->node。


二、list_node

“list_node.h”

#pragma once

template<typename T>
struct list_node {
	list_node* prev;
	list_node* next; 
    //在模板类的定义内部,类名本身(如 list_node)会被视为 list_node<T> 的同义词
	T data;

    template<typename... Args>
    _List_node(Args&&... args) 
        : data(std::forward<Args>(args)...), prev(nullptr), next(nullptr) {}
};

typename... Args:定义了一个模板参数包,表示零个或多个模板类型参数。允许构造函数接收任意数量、任意类型的参数,用于初始化节点的数据成员 data。
Args&&万能折叠,可以根据参数类型自动绑定到左值或右值。
使用forward完美转发将参数 args 以原始值类别(左值或右值)转发给 data 的构造函数。若参数是右值(如临时对象),转发为右值,可触发移动构造。若参数是左值,转发为左值,触发拷贝构造。


三、list 

"list.h"

#pragma once
#include "list_iterator.h"

template<typename T>
class list
{
public:
	explicit list() {
		init();
	}

	list(std::initializer_list<T> list) {
		init();
		for (auto i : list) {
			push_back(i);			//????????????????
		}
	}
    
    ~list() {
		clear();
		delete ptr;
	}

	list_iterator<T> insert(const list_iterator<T>& pos, const T& value) {  /该位置后面擦插入,符合后面push_back操作
		list_node<T>* new_node = new list_node<T>;
		new_node->data = value;
		list_node<T>* pos_node = pos.node;

		new_node->prev = pos_node->prev;
		new_node->next = pos_node;

		pos_node->prev->next = new_node;
		pos_node->prev = new_node;

		++size;

		return list_iterator<T>(new_node);
	}

	list_iterator<T> begin() {
		return list_iterator<T>(ptr->next);
	}

	list_iterator<T> end() {
		return list_iterator<T>(ptr);
	}

	void clear() {
		while (!empty()) {
			pop_front();        为什么不删哨兵ptr??????????????????????
		}
	}

	bool empty() {
		return ptr->next == ptr;
	}

	list_iterator<T> erase(const list_iterator<T>& pos) {   为什么要加const????????
		list_node<T>* pos_node = pos.node;
		list_iterator<T> next_node = list_iterator<T>(pos_node->next);

		pos_node->prev->next = pos_node->next;
		pos_node->next->prev = pos_node->prev;

		delete pos_node;

		--size;

		return next_node;
	}

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

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

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

	void push_back(const T& value) {
		insert(end(), value);  //这里的链表是环状链表,insert插入的位置是给定位置的后一位,这样子刚好就是在尾部插入
	}

	void show() {
		for (list_iterator<T> it = begin(); it != end();++it) {
			std::cout << *it << " ";
		}
		std::cout << '\n';
	}

	void show_size() {
		std::cout << size << '\n';
	}
private:
	void init() {
		ptr = new list_node<T>;   //标准库在这里使用allocator实现
		ptr->next = ptr->prev = ptr;
		size = 0;
	}
	list_node<T>* ptr; //环形链表的哨兵
	unsigned long long size;
};

 1.构造函数
explicit list() {
	init();
}

void init() {
	ptr = new list_node<T>;   //标准库在这里使用allocator实现
	ptr->next = ptr->prev = ptr;
	size = 0;
}

这里的ptr->next = ptr->prev = ptr实际上就是ptr->next = ptr,ptr->prev = ptr。将ptr的前驱指针和后继指针全部指向自己,形成了一个环状的空链表。统一了链表的插入,删除,遍历等操作,不需要再对空链表进行额外的处理。注意,该ptr哨兵并不属于容器中的一部分(size=0),只是作为连接头尾指针的一个桥梁,在后续对链表进行操作的时候大有用处。

explicit list(std::initializer_list<T> list) {
	init();
	for (auto i : list) {
		push_back(i);			
	}
}

为什么使用explicit?
禁止链表进行隐式转换。但对于上面的默认构造函数是多余的,因为默认构造函数不会触发隐式转换。

list<int> a = {1, 2, 3}; // 拷贝初始化,需要隐式转换
list<int> a{1, 2, 3}; // 直接初始化,无论是否有 explicit 都合法

 使用后禁止第一种初始化方式,但是在标准库中是允许的,一般在标准库容器的初始化列构造函数中不使用explicit。

push_back(i);

 使用push_back而不是push_front,确保元素存入容器的顺序与初始化列表一致。


2.析构函数
~list() {
	clear();
	delete ptr;
}

void clear() {
	while (!empty()) {
		pop_front();
	}
}

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

一直弹出(删除)第一个元素直至链表为空(这里使用push_back也是一样的效果),最后删除哨兵节点。


3.insert()
list_iterator<T> insert(const list_iterator<T>& pos, const T& value) {  
	list_node<T>* new_node = new list_node<T>;
	new_node->data = value;
	list_node<T>* pos_node = pos.node;

	new_node->prev = pos_node->prev; //设置新节点的前驱
	new_node->next = pos_node;       //设置后驱

	pos_node->prev->next = new_node; //更新前驱节点的后继
	pos_node->prev = new_node;       //更新位置节点的前驱

	++size;

	return list_iterator<T>(new_node);
}

这里对链表节点操作的顺序不可以变更,顺序大概是先对新节点进行前驱和后继的更新,然后从远端开始更新。
如果害怕写错可以使用临时变量来储存:

list_node<T>* prev_node = pos_node->prev;  // 保存前驱节点
new_node->prev = prev_node;
new_node->next = pos_node;
prev_node->next = new_node;    // 使用临时变量,避免依赖修改后的 pos_node->prev
pos_node->prev = new_node;

这里返回值是list_iterator<T>,因为返回的list_iterator<T>(new_node)是一个临时变量,无法使用引用传值。

为什么在这里的参数要加const?
因为后面在push_front()调用了insert()

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

传入的是一个临时变量begin()(右值),无法对其进行引用,加上const代表对其不进行修改(实际也没有进行修改),这样insert的参数就可以任意传入左值或右值,后面的erase同理。

这里的insert插入的位置是指定位置的后一位,这样写的作用会在后面push_back体现。


4.erase()
list_iterator<T> erase(const list_iterator<T>& pos) {  
	list_node<T>* pos_node = pos.node;
	list_iterator<T> next_node = list_iterator<T>(pos_node->next);

	pos_node->prev->next = pos_node->next;
	pos_node->next->prev = pos_node->prev;

	delete pos_node;

	--size;

	return next_node;
}

erase对节点的操作顺序可以交换,不会产生影响。
delete pos_node和delete pos.node是一样的,因为它们都指向了同一个节点。


5.begin()与end()
list_iterator<T> begin() {
	return list_iterator<T>(ptr->next);
}
list_iterator<T> end() {
	return list_iterator<T>(ptr);
}

之前有提到过,ptr是连接头尾指针的桥梁,使用begin()就能获取头指针,使用--end()就能获取尾指针。

为什么end()要返回一个不属于容器中的元素ptr?
因为标准库中的容器都是左闭右开区间,使用for(iterator it = begin();it != end();++it)就可以正确遍历一个容器。


6.push_back()
void push_back(const T& value) {
	insert(end(), value); 
}

这里的链表是环状链表,insert插入的位置是给定位置的后一位,这样子刚好就是在尾部插入。


四、测试与测试结果

#include "list.h"
using namespace std;

int main() {
	list<int> a{1,2,3};
	list_iterator<int> it = a.begin();
	a.show();
	//1 2 3

	a.push_back(4);
	a.show();
	//1 2 3 4

	a.push_front(11);
	a.show();
	//11 1 2 3 4

	advance(it, 3);
	a.insert(it,10);
	a.show();
	//11 1 2 3 10 4

	a.pop_front();
	a.show();
	//1 2 3 10 4

	a.pop_back();
	a.show();
	//1 2 3 10
	a.show_size();
	//4

	return  0;
}

{1,2,3}会自动生成为一个初始化列表,传入list(std::initializer_list<T> list)进行初始化。在调用到insert()中的new list_node<T>时会调用list_node的构造函数,prev和next初始化为nullptr,而类型T的data根据左值或右值进行对应的构造。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值