《 C++ 修炼全景指南:四 》揭秘 C++ List 容器背后的实现原理,带你构建自己的双向链表

本篇博客,我们将详细讲解如何从头实现一个功能齐全且强大的 C++ List 容器,并深入到各个细节。这篇博客将包括每一步的代码实现、解释以及扩展功能的探讨,目标是让初学者也能轻松理解。


一、简介

1.1、背景介绍

在 C++ 中,std::list 是一个基于双向链表的容器,允许高效的插入和删除操作,适用于频繁插入和删除操作的场景。与动态数组不同,list 允许常数时间内的插入和删除操作,支持双向遍历。这篇文章将详细讲解如何实现一个自定义的 List 容器,涵盖模板、迭代器、双向链表的核心技术,并逐步扩展其功能,达到媲美标准库的效果。


1.2、学习目标

通过阅读本文,您将掌握以下技能:

  • 如何从零开始实现一个功能齐全的 C++ List 容器。
  • 理解 C++ 中动态内存分配的机制,并学习如何防止内存泄漏。
  • 学会运算符重载的正确使用方式。
  • 实现 C++ 类的拷贝构造函数、赋值运算符。
  • 深入理解迭代器设计,实现迭代器功能
  • 简化代码逻辑,复用代码

1.3、代码仓库

这篇博客所涉及的所有代码可以从我的代码仓库获得:https://git.lenyiin.com/Lenyiin/List


二、双向链表的基本结构

要实现一个 List 容器,首先要理解双向链表的数据结构。在链表中,每个节点都包含数据和指向前后节点的指针。双向链表的优点是可以在常数时间内从任意位置插入和删除元素,缺点是不能像数组一样高效地随机访问元素。


2.1、基本节点结构

我们首先定义一个表示链表节点的结构体。该结构体包含三个重要成员:存储数据的 data,指向前一个节点的 prev 指针,以及指向下一个节点的 next 指针。我们可以使用一个模板类来实现这一结构,使得 List 能够存储任何类型的元素。

template 
struct __list_node
{
	__list_node* _next;
	__list_node* _prev;
	T _data;

	__list_node(const T& data = T())
		: _data(data), _next(nullptr), _prev(nullptr)
	{}
};

template 
class List
{
	typedef __list_node Node;

public:
	// 带头双向循环链表
	// 默认构造
    List()
    {
        _head = new Node;
        _head->_next = _head;
        _head->_prev = _head;
    }
    
    // 析构函数
    ~List()
    {
        clear();
        delete _head;
        _head = nullptr;
    }
    
private:
	Node* _head;
};

详细解释:

  1. 节点结构:每个 __list_node 包含一个数据元素 _data,以及两个指针 _prev_next,分别指向前一个节点和后一个节点。
  2. 构造函数与析构函数:我们初始化链表为空,并在析构函数中清空链表,确保不产生内存泄漏。

2.2、拷贝构造和赋值运算

拷贝构造函数用于创建一个新对象,该对象是通过复制另一个现有对象生成的。对于 List 类,我们需要确保在拷贝时,新对象有自己独立的内存副本。

// 拷贝构造
List(const List& lt)
{
	_head = new Node;
	_head->_next = _head;
	_head->_prev = _head;

	for (const auto& e : lt)
	{
		push_back(e);
	}
}

详细解释:

  • 深拷贝:通过分配新内存来创建新对象的独立副本,而不是简单地复制指针。这样,两个 List 对象可以独立管理各自的内存,避免潜在的内存管理冲突。
// 赋值运算符
List& operator=(const List& lt)
{
	if (this != <)
	{
		clear();
		for (const auto& e : lt)
		{
			push_back(e);
		}
	}
	return *this;
}

赋值运算符用于将一个对象的内容复制到另一个已经存在的对象中。为了避免自赋值和内存泄漏,我们需要在实现赋值运算符时特别小心。

List& operator=(const List& lt)
{
	if (this != <)
	{
		clear();
		for (const auto& e : lt)
		{
			push_back(e);
		}
	}
	return *this;
}

详细解释:

  • 自赋值检查:在赋值运算符实现中,首先检查是否为自赋值,即 this 指针是否与 lt 相同。如果是自赋值,则无需进行任何操作,直接返回当前对象。
  • 内存管理:在分配新内存之前,记得释放当前对象所持有的旧内存,防止内存泄漏。
  • 深拷贝:与拷贝构造函数类似,通过分配新内存来存储字符串的副本,确保两个对象独立管理各自的内存。

进阶:

  • 也可以复用拷贝构造函数,直接交换拷贝构造的数据
// 进阶写法
List& operator=(List lt)
{
	std::swap(_head, lt._head);
	return *this;
}

2.3、基础功能:添加和删除元素

双向链表的主要优势在于可以在常数时间内插入和删除元素。我们将实现 push_backpush_front 函数用于分别在链表尾部和头部插入元素,同时实现 pop_backpop_front 用于删除元素。

2.3.1、头插、尾插

push_back 函数用于在链表尾部插入新元素。

// 结构设计的优势, 有没有数据, 插入的逻辑都是一样的
void push_back(const T& data)
{
	Node* tail = _head->_prev;
	Node* newnode = new Node(data);
	tail->_next = newnode;
	newnode->_prev = tail;
	newnode->_next = _head;
	_head->_prev = newnode;
}

push_front 函数用于在链表头部插入新元素。

void push_front(const T& data)
{
	Node* cur = _head->_next;
	Node* newnode = new Node(data);

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

详细解释:

  1. push_back:因为是带头的双向循环链表,所以不管是空链表还是非空链表,插入逻辑都是一样的。
  2. push_front:类似 push_back,相同的插入逻辑。

2.3.2、头删、尾删

删除操作包括从链表的头部和尾部移除节点,这里分别通过 pop_backpop_front 函数实现。

void pop_back()
{
	Node* tail = _head->_prev;
	Node* prev = tail->_prev;

	delete tail;
	prev->_next = _head;
	_head->_prev = prev;
}
void pop_front()
{
	Node* head = _head->_next;
	Node* next = head->_next;

	delete head;
	_head->_next = next;
	next->_prev = _head;
}

详细解释:

  1. pop_front:从头部删除节点。
  2. pop_back:从尾部删除节点。

三、进阶功能

3.1、双向迭代器的实现

为了让 List 支持 for-each 这样的遍历操作,我们需要实现迭代器。链表的迭代器允许我们像操作数组一样顺序访问链表中的每一个节点。

3.1.1、基本正向迭代器结构

迭代器类封装了对节点的操作,支持解引用和递增操作。

template 
struct __list_iterator
{
	typedef __list_node Node;
	typedef __list_iterator Self;

	Node* _node;

	// 默认构造
	__list_iterator(Node* node)
		: _node(node)
	{
	}

	// 运算符重载
	// *it
	Ref operator*()
	{
		return _node->_data;
	}

	// it ->
	Ptr operator->()
	{
		return &_node->_data;
	}

	// ++it
	Self& operator++()
	{
		_node = _node->_next;
		return *this;
	}

	// it++
	Self operator++(int)
	{
		Self tmp(*this);
		++(*this);
		return tmp;
	}

	// --it
	Self& operator--()
	{
		_node = _node->_prev;
		return *this;
	}

	// it--
	Self operator--(int)
	{
		Self tmp(*this);
		--(*this);
		return tmp;
	}

	// it != end()
	bool operator!=(const Self& it)
	{
		return _node != it._node;
	}

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

详细解释:

  1. __list_iterator类:封装了指向节点的指针,并实现了解引用操作符 * 和递增操作符 ++,使得可以通过 for 循环遍历链表。

3.1.2、iterator && const_iterator

typedef __list_iterator iterator;
typedef __list_iterator const_iterator;

iterator begin()	// 返回头节点的下一个节点
{
	return iterator(_head->_next);
}

iterator end()	// 返回头节点
{
	return iterator(_head);
}

const_iterator begin() const
{
	return const_iterator(_head->_next);
}

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

详细解释:

  1. begin 和 endbegin 返回指向头节点下一个节点的迭代器,end 返回指向头的迭代器,表示链表的结束。

3.1.3、反向迭代器结构

有时候,我们需要从后向前遍历链表。为此,我们可以实现反向迭代器,允许从尾部向头部遍历。反向迭代器与正向迭代器迭代方向相反,但逻辑不变。

我们继续完善反向迭代器,使其支持与普通迭代器相同的操作,包括前置和后置递减(--)、比较操作符等。这样,链表可以从尾部向头部遍历,增强了容器的灵活性。

template 
struct __list_reverse_iterator
{
	typedef __list_node Node;
	typedef __list_reverse_iterator Self;

	Node* _node;

	// 默认构造
	__list_reverse_iterator(Node* node)
		: _node(node)
	{
	}

	// 运算符重载
	// *it
	Ref operator*()
	{
		return _node->_data;
	}

	// it ->
	Ptr operator->()
	{
		return &_node->_data;
	}

	// ++it
	Self& operator++()
	{
		_node = _node->_prev;
		return *this;
	}

	// it++
	Self operator++(int)
	{
		Self tmp(*this);
		++(*this);
		return tmp;
	}

	// --it
	Self& operator--()
	{
		_node = _node->_next;
		return *this;
	}

	// it--
	Self operator--(int)
	{
		Self tmp(*this);
		--(*this);
		return tmp;
	}

	// it != end()
	bool operator!=(const Self& it)
	{
		return _node != it._node;
	}

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

详细解释:

  1. __list_reverse_iterator类:封装了指向节点的指针,并实现了解引用操作符 * 和递增操作符 ++,使得可以通过 for 循环遍历链表。

3.1.4、reverse_iterator && const_reverse_iterator

typedef __list_reverse_iterator reverse_iterator;
typedef __list_reverse_iterator const_reverse_iterator;

reverse_iterator rbegin()	// 返回头节点的下一个节点
{
	return reverse_iterator(_head->_prev);
}

reverse_iterator rend()	// 返回头节点
{
	return reverse_iterator(_head);
}

const_reverse_iterator rbegin() const
{
	return const_reverse_iterator(_head->_prev);
}

const_reverse_iterator rend() const
{
	return const_reverse_iterator(_head);
}

详细解释:

  1. rbegin 和 rendrbegin 返回指向尾节点的迭代器,rend 返回指向头的迭代器,表示链表的结束。

3.2、模板支持与代码复用

为了使 List 容器能够支持任意类型的元素存储,我们使用 C++ 模板来实现这一功能。通过模板,List 可以适应不同类型的数据,而不需要为每种类型单独实现一个类。

template 
class List {
    // 内部结构与之前相同
};

模板化后的 List 容器可以在实例化时指定存储的数据类型,如 ListList


3.3、插入和删除任意位置的元素

为了使 List 更加灵活,我们还需要支持在链表中任意位置插入和删除元素。我们需要实现 inserterase 函数。这些操作的实现与头部和尾部的插入删除操作类似,但更加通用。通过迭代器,我们可以确定插入或删除的具体位置。

3.3.1、在任意位置插入元素

我们通过传入迭代器的位置,在链表的任意位置插入一个新元素。

void insert(iterator pos, const T& data)
{
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* newnode = new Node(data);

	// prev newnode cur
	prev->_next = newnode;
	newnode->_prev = prev;
	newnode->_next = cur;
	cur->_prev = newnode;
}

详细解释:

  1. insert:在给定的迭代器位置 pos 插入一个新节点。先调整新节点与前后节点的指针关系,再插入节点。

3.3.2、在任意位置删除元素

iterator erase(iterator pos)
{
	assert(pos != end());

	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* next = cur->_next;
	delete cur;

	prev->_next = next;
	next->_prev = prev;

	return next;
}

详细解释:

  1. erase:删除 pos 位置上的节点,更新前后节点的指针,使其指向彼此,完成删除。

3.3.3、复用任意位置的插入和删除

我们还可以直接复用插入和删除,直接重写头插、尾插、头删、尾删,以简化代码逻辑。

void push_back(const T& data)
{
	insert(end(), data);
}

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

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

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

3.3.4、clear

clear 函数能够清除所有节点

// 清除
void clear()
{
    iterator it = begin();
    while (it != end())
    {
    	erase(it++);
    }
}

四、迭代器兼容性与异常安全

为了让 List 更加健壮,我们需要确保在插入或删除元素时迭代器不会失效。我们还需要考虑异常安全性,确保在操作失败或异常抛出时,链表的状态能够保持一致。

4.1、迭代器失效问题

当链表在操作过程中插入或删除元素时,特别是在 inserterase 操作后,指向旧节点的迭代器可能会失效。为了解决这个问题,我们需要更新受影响的迭代器,或者确保不会对现有的迭代器产生副作用。

4.2、异常安全性

在插入或删除操作时,若发生异常(例如内存分配失败),我们要确保链表不会处于不一致的状态。我们可以通过在执行插入和删除操作时,分阶段更新链表的指针关系,确保每一步都能保持链表结构的完整性。


五、完整实现代码和测试

5.1、List.hpp

新建头文件 List.hpp

#pragma once


#include 
#include 

using namespace std;

namespace Lenyiin
{
	template 
	struct __list_node
	{
		__list_node* _next;	// 指向后一个节点的指针
		__list_node* _prev;	// 指向前一个节点的指针
		T _data;	// 节点存储的数据

		__list_node(const T& data = T())
			: _data(data), _next(nullptr), _prev(nullptr)
		{}
	};

	template 
	struct __list_iterator
	{
		typedef __list_node Node;
		typedef __list_iterator Self;

		Node* _node;

		// 默认构造
		__list_iterator(Node* node)
			: _node(node)
		{
		}

		// 运算符重载
		// *it
		Ref operator*()
		{
			return _node->_data;
		}

		// it ->
		Ptr operator->()
		{
			return &_node->_data;
		}

		// ++it
		Self& operator++()
		{
			_node = _node->_next;
			return *this;
		}

		// it++
		Self operator++(int)
		{
			Self tmp(*this);
			++(*this);
			return tmp;
		}

		// --it
		Self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}

		// it--
		Self operator--(int)
		{
			Self tmp(*this);
			--(*this);
			return tmp;
		}

		// it != end()
		bool operator!=(const Self& it)
		{
			return _node != it._node;
		}

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

	template 
	struct __list_reverse_iterator
	{
		typedef __list_node Node;
		typedef __list_reverse_iterator Self;

		Node* _node;

		// 默认构造
		__list_reverse_iterator(Node* node)
			: _node(node)
		{
		}

		// 运算符重载
		// *it
		Ref operator*()
		{
			return _node->_data;
		}

		// it ->
		Ptr operator->()
		{
			return &_node->_data;
		}

		// ++it
		Self& operator++()
		{
			_node = _node->_prev;
			return *this;
		}

		// it++
		Self operator++(int)
		{
			Self tmp(*this);
			++(*this);
			return tmp;
		}

		// --it
		Self& operator--()
		{
			_node = _node->_next;
			return *this;
		}

		// it--
		Self operator--(int)
		{
			Self tmp(*this);
			--(*this);
			return tmp;
		}

		// it != end()
		bool operator!=(const Self& it)
		{
			return _node != it._node;
		}

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

	template 
	class List
	{
		typedef __list_node Node;

	public:
		typedef __list_iterator iterator;
		typedef __list_iterator const_iterator;
		typedef __list_reverse_iterator reverse_iterator;
		typedef __list_reverse_iterator const_reverse_iterator;

		iterator begin()	// 返回头节点的下一个节点
		{
			return iterator(_head->_next);
		}

		iterator end()	// 返回头节点
		{
			return iterator(_head);
		}

		const_iterator begin() const
		{
			return const_iterator(_head->_next);
		}

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

		reverse_iterator rbegin()	// 返回头节点的下一个节点
		{
			return reverse_iterator(_head->_prev);
		}

		reverse_iterator rend()	// 返回头节点
		{
			return reverse_iterator(_head);
		}

		const_reverse_iterator rbegin()	const
		{
			return const_reverse_iterator(_head->_prev);
		}

		const_reverse_iterator rend() const
		{
			return const_reverse_iterator(_head);
		}

	public:
		// 带头双向循环链表
		// 默认构造
		List()
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
		}

		// 拷贝构造
		List(const List& lt)
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;

			for (const auto& e : lt)
			{
				push_back(e);
			}
		}

		// 赋值运算符
		//List& operator=(const List& lt)
		//{
		//	if (this != <)
		//	{
		//		clear();
		//		for (const auto& e : lt)
		//		{
		//			push_back(e);
		//		}
		//	}
		//	return *this;
		//}

		// 进阶写法
		List& operator=(List lt)
		{
			std::swap(_head, lt._head);
			return *this;
		}

		// 清除
		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				erase(it++);
			}
		}

		// 析构函数
		~List()
		{
			clear();
			delete _head;
			_head = nullptr;
		}

		// 结构设计的优势, 有没有数据, 插入的逻辑都是一样的
		//void push_back(const T& data)
		//{
		//	Node* tail = _head->_prev;
		//	Node* newnode = new Node(data);
		//	tail->_next = newnode;
		//	newnode->_prev = tail;
		//	newnode->_next = _head;
		//	_head->_prev = newnode;
		//}

		//void push_front(const T& data)
		//{
		//	Node* cur = _head->_next;
		//	Node* newnode = new Node(data);

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

		void push_back(const T& data)
		{
			insert(end(), data);
		}

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

		//void pop_back()
		//{
		//	Node* tail = _head->_prev;
		//	Node* prev = tail->_prev;

		//	delete tail;
		//	prev->_next = _head;
		//	_head->_prev = prev;
		//}

		//void pop_front()
		//{
		//	Node* head = _head->_next;
		//	Node* next = head->_next;

		//	delete head;
		//	_head->_next = next;
		//	next->_prev = _head;
		//}

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

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

		void insert(iterator pos, const T& data)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(data);

			// prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
		}

		iterator erase(iterator pos)
		{
			assert(pos != end());

			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* next = cur->_next;
			delete cur;

			prev->_next = next;
			next->_prev = prev;

			return next;
		}

	private:
		Node* _head;
	};
}

5.2、List.cpp

新建源文件 List.cpp

#include "List.hpp"

using namespace Lenyiin;

void print(const List& lt)
{
    List::const_iterator it = lt.begin();
    while (it != lt.end())
    {
        cout << *it << " ";
        ++it;
    }
    cout << endl;
}

void print_reverse(const List& lt)
{
    List::const_reverse_iterator it = lt.rbegin();
    while (it != lt.rend())
    {
        cout << *it << " ";
        ++it;
    }
    cout << endl;
}

// 测试遍历
void test_1()
{
    List lt;
    lt.push_back(1);
    lt.push_back(2);
    lt.push_back(3);
    lt.push_back(4);
    lt.push_back(5);

    // iterator
    cout << "iterator \t\t遍历: ";
    List::iterator it = lt.begin();
    while (it != lt.end())
    {
        cout << *it << " ";
        it++;
    }
    cout << endl;

    // const_iterator
    cout << "const_iterator \t\t遍历: ";
    print(lt);

    // for
    cout << "for \t\t\t遍历: ";
    for (auto& it : lt)
    {
        cout << it << " ";
    }
    cout << endl;

    cout << "for const \t\t遍历: ";
    for (const auto& it : lt)
    {
        cout << it << " ";
    }
    cout << endl;

    // reverse iterator
    cout << "reverse iterator \t遍历: ";
    List::reverse_iterator rit = lt.rbegin();
    while (rit != lt.rend())
    {
        cout << *rit << " ";
        rit++;
    }
    cout << endl;

    // const reverse iterator
    cout << "const reverse iterator \t遍历: ";
    print_reverse(lt);
}

// 测试插入删除
void test_2()
{
    List lt;
    lt.push_back(1);
    lt.push_back(2);
    lt.push_back(3);
    lt.push_back(4);
    lt.push_back(5);

    print(lt);

    lt.erase(lt.begin());
    print(lt);

    lt.pop_back();
    print(lt);

    lt.pop_front();
    print(lt);

    lt.push_front(100);
    print(lt);
}

void test_3()
{
    // 默认构造
    List lt1;
    lt1.push_back(1);
    lt1.push_back(2);
    lt1.push_back(3);
    lt1.push_back(4);
    lt1.push_back(5);
    print(lt1);

    // 拷贝构造
    List lt2(lt1);
    print(lt2);
    lt2.push_back(6);
    lt2.push_back(7);
    lt2.push_back(8);
    lt2.push_back(9);
    lt2.push_back(10);
    print(lt2);

    // 赋值运算
    lt1 = lt2;
    print(lt1);
}

// 模板
struct Date
{
    int _year;
    int _month;
    int _day;

    Date(int year = 0, int month = 1, int day = 1)
        : _year(year), _month(month), _day(day)
    {}
};

void test_4()
{
    List lt;
    lt.push_back(Date());
    lt.push_back(Date(2022, 2, 22));
    lt.push_back(Date(2024, 9, 8));

    List::iterator it = lt.begin();
    while (it != lt.end())
    {
        // cout << *it << " ";
        // operator->   operator*
        cout << it->_year << "-" << it->_month << "-" << it->_day << endl; // 更喜欢这么去用
        cout << (*it)._year << "-" << (*it)._month << "-" << (*it)._day << endl;
        it++;
    }
    cout << endl;
}

int main()
{
    test_1();
    //test_2();
    //test_3();
    //test_4();

    return 0;
}

六、总结与优化

通过这篇文章,我们实现了一个功能完备的双向链表 List 容器。我们的 List 容器具备如下特点:

  1. 模板支持:允许存储任意类型的数据。
  2. 动态扩容:无需预定义容器大小,链表能够根据需求动态增长。
  3. 迭代器与反向迭代器:支持双向遍历,并与标准库中的 std::list 类似。
  4. 插入与删除:支持在任意位置插入和删除元素,适用于频繁的动态修改操作。
  5. 异常安全与迭代器兼容性:通过合理的内存管理,确保操作的健壮性。

本文通过逐步构建实现了一个强大的 List 容器,这一 List 实现适用于需要高效插入和删除操作的场景,并且能够与 STL 的标准容器互相借鉴。这篇文章通过详细的代码解释和逻辑推导,带领读者逐步实现了一个媲美 C++ 标准库 std::list 的双向链表。学习与实现这样的数据结构,能够帮助我们更加深入理解 C++ 容器的设计与实现思路,也为日后实现更多复杂的数据结构打下坚实的基础。



希望这篇博客对您有所帮助,也欢迎您在此基础上进行更多的探索和改进。如果您有任何问题或建议,欢迎在评论区留言,我们可以共同探讨和学习。更多知识分享可以访问我的个人博客网站 : https://blog.lenyiin.com/ 。本博客所设计的代码也可以访问我的 git 仓库获取 :https://git.lenyiin.com/Lenyiin/List

你可能感兴趣的:(技术指南,C++,修炼全景指南,c++,list,链表,stl)