STL库 —— list 的编写

目录

一、成员变量

​编辑

[二、push_back 函数](#二、push_back 函数)

[三、迭代器 iterator](#三、迭代器 iterator)

[3.1 iterator 结构体](#3.1 iterator 结构体)

[3.2 begin() 与 end() 函数](#3.2 begin() 与 end() 函数)

[3.3 iterator 运算符重载](#3.3 iterator 运算符重载)

[3.4 -> 的重载](#3.4 -> 的重载)

[3.5 const_iterator](#3.5 const_iterator)

四、测试代码

五、修饰符成员

[5.1 insert 函数](#5.1 insert 函数)

[5.2 erase 函数](#5.2 erase 函数)

[5.3 push 函数](#5.3 push 函数)

[5.4 pop 函数](#5.4 pop 函数)

六、容量成员

[6.1 size 函数](#6.1 size 函数)

[6.2 empty 函数](#6.2 empty 函数)

七、模板优化iterator


一、成员变量

cpp 复制代码
	template<class T>
	struct ListNode
	{
		T _data;
		ListNode<T>* _prev;
		ListNode<T>* _next;
		ListNode(const T& x = T())
			:_data(x)
			,_prev(nullptr)
			,_next(nullptr)
		{}
	};

	template<class T>
	class my_list
	{
		typedef ListNode<T> Node;
	private:
		Node* _head;//哨兵位头结点
	};

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

二、push_back 函数

为了测试方便,我们在这里先把 push_back 写出来。

cpp 复制代码
		void push_back(T data)
		{
			Node* NewNode = new Node(data);

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

三、迭代器 iterator

3.1 iterator 结构体

这里的迭代器和之前的迭代器有所不同,诸如 string 、 vector ,他们的内存空间是连续的,迭代器的底层都是基于指针的,而 list 中,由于存储单元的随机性,迭代器不可以再是单纯的指针,而是像上面那样的结构体对象:

cpp 复制代码
    template<class T>
	struct ListIterator
	{
		typedef ListNode<T> Node;
        typedef ListIterator<T> iterator;
		
        Node* _node;
		ListIterator(Node* node)//构造函数
		{
			_node = node;
		}
	};

3.2 begin() 与 end() 函数

然后,需要在 my_list 类中新添 begin 和 end 函数,因为他们都是服务于迭代器,所以他们的返回值类型应该是迭代器,还要注意,根据 STL 通用设计模式, end() 指向的位置不用于存放有效数据元素,而是作为遍历结束的标记。所以 end() 函数可以直接返回通过 _head 构造的迭代器,以此表示链表的末尾。

cpp 复制代码
    template<class T>
	class my_list
	{
		typedef ListNode<T> Node;
	public:
		typedef ListIterator<T> iterator;

		iterator begin()
		{
			return iterator(_head->_next);
		}

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

	private:
		Node* _head;
	};

3.3 iterator 运算符重载

随后,为了使迭代器可以跑起来,需要再对 ++ 、 -- 、 != 等运算符进行重载,

其中,为了保持与内建类型行为一致,内建类型(如intfloat等)的前置自增和自减操作 返回的是引用 。自定义类型(如迭代器)模拟内建类型的行为,包括操作符的使用,有助于保持语言的一致性和直观性。
后置自增和自减不返回其引用 的原因与前置自增自减的设计意图及使用方式有关。后置自增操作符的主要特点是先返回对象当前的值(在自增之前的状态),然后再对对象进行自增操作。这一行为决定了其返回类型应该是对象的值 而不是引用

顺便,我们可以把 == 和 * 运算符都重载:

cpp 复制代码
		typedef ListIterator<T> iterator;
        iterator& operator++()//前置++
		{
			_node = _node->_next;
			return *this;
		 }
		iterator operator++(int)//后置++
		{
			iterator tmp(*this);
			_node = _node->_next;
			return tmp;
		}
		iterator& operator--()//前置--
		{
			_node = _node->_prev;
			return *this;
		}
		iterator operator--(int)//后置--
		{
			iterator tmp(*this);
			_node = _node->_prev;
			return tmp;
		}
		T& operator*()
		{
			return _node->_data;
		}
		bool operator!=(const iterator& it)
		{
			return  _node != it._node;
		}
		bool operator==(const iterator& it)
		{
			return _node == it._node;
		}

3.4 -> 的重载

当我们创建一个自定义类型,如 class A(此时在Flash命名空间内):

cpp 复制代码
	class A
	{
	public:
		int a = 0;
		int b = 0;
	};

去测试 A 类型的 list 时,虽然 push_back 可以用,但是我们想遍历链表时,迭代器却不能用了:

cpp 复制代码
	void test()
	{
		my_list<A> la;
		la.push_back({ 6, 12 });
		la.push_back({ 7, 18 });
		la.push_back({ 3, 22 });
		my_list<A>::iterator it = la.begin();
		while (it != la.end())
		{
			cout << *it << " ";
			it++;
		}
		cout << endl;
	}

错误的原因在于,系统不支持自定义类型的流插入,这里有两种解放方案:

1.重新重载一个 << 运算符

2.改变输出的格式 cout << (*it).a << (*it).b << " ";

cpp 复制代码
		while (it != la.end())
		{
			cout << (*it).a << (*it).b << " ";
			it++;
		}

但是究其本源,迭代器模拟的还是指针,如果是指针的话,就可以通过 -> 读取指针指向空间的数据,所以这里可以重载 -> 来达到模拟指针目的。

cpp 复制代码
		T* operator->()
		{
			return &_node->_data;//->的优先级更高
		}

3.5 const_iterator

为了保证迭代器的内容不被修改,我们还要为迭代器添加 const ,但是根据所学知识,仅在函数返回值前加 const 是不能满足需求的,这只能保证迭代器本身不能被修改,此时就不可能自增和自减了,所以要新建一个 const_iterator 类,来模拟实现 const T* 指针的行为。

其中只有 operator* 与 operator-> 返回值不同。

cpp 复制代码
template<class T>
	struct ListConstIterator
	{
		typedef ListNode<T> Node;
		typedef ListConstIterator<T> const_iterator;

		Node* _node;

		ListConstIterator(Node* node)
			:_node(node)
		{}

		// *it
		const T& operator*()
		{
			return _node->_data;
		}

		// it->
		const T* operator->()
		{
			return &_node->_data;
		}

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

		const_iterator operator++(int)
		{
			const_iterator tmp(*this);
			_node = _node->_next;

			return tmp;
		}

		const_iterator& operator--()
		{
			_node = _node->_prev;
			return *this;
		}

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

			return tmp;
		}

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

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

此时在 my_list 类中在添加 cbegin 和 cend 函数:

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

四、测试代码

有了之前的代码,我们就可以进行统一的测试:

cpp 复制代码
    void test_push_back()
	{
		my_list<int> l1;
		l1.push_back(1);
		l1.push_back(2);
		l1.push_back(3);
		l1.push_back(4);
		l1.push_back(5);
		my_list<int>::iterator it = l1.begin();
		while (it != l1.end())
		{
			cout << *it << endl;
			it++;
		}
	}

五、修饰符成员

上面的代码就可以让我们基本理清 list 的行为,有助于写下面的函数。

5.1 insert 函数

库中的 insert 支持的是迭代器访问,此时 pos 是上面的迭代器结构体, pos._node 才是此时要访问的节点。pos 位置前的节点的下一个节点是新插入的节点,pos位置是新插入节点后面的节点。

cpp 复制代码
		void insert(iterator pos, T data)
		{
			Node* NewNode = new Node(data);
			Node* prev = pos._node->_prev;
			Node* cur = pos._node;
			//节点位置: prev Newnode cur
			prev->_next = NewNode;
			NewNode->_prev = prev;
			NewNode->_next = cur;
			cur->_prev = NewNode;
		}

5.2 erase 函数

erase 函数更加简单,其不需要创建新节点,仅需控制指针的指向即可。

cpp 复制代码
		void erase(iterator pos)
		{
			Node* prev = pos._node->_prev;
			Node* next = pos._node->_next;
			//prev pos next
			prev->_next = next;
			next->_prev = prev;
			delete pos._node;
		}

5.3 push 函数

有了 insert 函数, push_back 函数就可以直接复用,因为是尾插,且传入的 pos 位置是插入位置的下一个节点, end 函数指向的是哨兵位节点,所以 pos 直接取到 end 即可。

cpp 复制代码
		void push_back2(T data)
		{
			insert(end(), data);
		}

同 push_back 使用 end 作为 pos 一样, begin 返回的是哨兵位的下一个节点,所以传入位置直接取到 begin 即可。

cpp 复制代码
		void push_front(T data)
		{
			insert(begin(), data);
		}

5.4 pop 函数

因为没有重载运算符 - ,所以 pop_back 传值时只能使用自减操作, end 指向的是哨兵位,所以需要使用 end 前面的位置,标准库中并没有实现减操作的重载,且效率很低,所以没有必要手搓。

cpp 复制代码
		void pop_back()
		{
			erase(--end());
		}
		void pop_front()
		{
			erase(begin());
		}

六、容量成员

6.1 size 函数

`std::list`的`size()`成员函数返回容器中元素的数目。在C++11及之后的标准中,`std::list::size()`的时间复杂度为常数时间O(1)。这是通过内部维护一个表示元素数量的计数器来实现的。

每当添加或删除元素时,这个计数器就会相应地更新。

因此,`size()`函数的实现通常看起来像这样:

cpp 复制代码
        size_t size() const 
        {
            return _size;  // _size是一个成员变量,用来追踪当前链表中元素的数量
        }

这里的`_size`是容器的一个私有成员变量,其在构造函数中初始化,在向容器添加元素或从容器中移除元素时被更新。

但是要注意,在使用复用函数时,如上的 "push_front" "pop_back" 时, _size 不需要进行自增或自减操作,这样可能会自增自减两次,导致错误。

6.2 empty 函数

cpp 复制代码
		bool empty()
		{
			return _size == 0;
		}

七、模板优化iterator

上面的 iterator 与 const_iterator 重复度太高,此时,就可以使用模板进行优化,让编译器根据传入的参数进行实例化:

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

与之前学的模板不同的是,传入的参数增加了,传入的参数变成了引用和指针:

cpp 复制代码
    template<class T>
	class my_list
	{
		typedef ListNode<T> Node;
	public:
		typedef ListIterator<T, T&, T*> iterator;
		typedef ListIterator<T, const T&, const T*> const_iterator;
	}
相关推荐
芊寻(嵌入式)8 分钟前
C转C++学习笔记--基础知识摘录总结
开发语言·c++·笔记·学习
獨枭10 分钟前
C++ 项目中使用 .dll 和 .def 文件的操作指南
c++
霁月风13 分钟前
设计模式——观察者模式
c++·观察者模式·设计模式
橘色的喵14 分钟前
C++编程:避免因编译优化引发的多线程死锁问题
c++·多线程·memory·死锁·内存屏障·内存栅栏·memory barrier
一颗松鼠17 分钟前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript
有梦想的咸鱼_19 分钟前
go实现并发安全hashtable 拉链法
开发语言·golang·哈希算法
海阔天空_201324 分钟前
Python pyautogui库:自动化操作的强大工具
运维·开发语言·python·青少年编程·自动化
天下皆白_唯我独黑31 分钟前
php 使用qrcode制作二维码图片
开发语言·php
夜雨翦春韭35 分钟前
Java中的动态代理
java·开发语言·aop·动态代理
小远yyds37 分钟前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js