Diseño de listas enlazadas de diseño STL, análisis de bloques y componentes, ideas de diseño de iteradores

contenido

Prefacio.

1. Piensa en el diseño del iterador de la lista.

2. Análisis y comprensión de prototipos de funciones importantes, con estos primeros escritos para ver el efecto.

Análisis de las funciones necesarias del framework iterator

Análisis de las funciones necesarias del marco List

3. Construir un marco general

4. Análisis de bloques de los detalles de la función (similar al vector)

construcción de gama

intercambio: intercambio muy puro y simple, intercambia tus miembros conmigo, me convertiré en ti, te convertirás en mí

Copie la construcción (debido al intercambio, me inicializo en nullptr para evitar punteros salvajes, y luego intercambio el objeto tmp construido multiplexando el código de construcción de rango para construirlo)

sobrecarga de tareas

Mover la construcción (arrebatar los recursos del montón del objeto moribundo para la construcción)

Después de escribir la interfaz insert() + erase(), es equivalente a escribir seis interfaces

insertar()

push_back() reutiliza la función de inserción

push_front() reutiliza la función de inserción

borrar();

pop_back() reutilizar borrar

pop_front() reutilizar borrar

5. Código general + prueba

6. Resumen


Prefacio.

  • Bienvenido al capítulo STL escrito a mano de Xiaojie. Xiaojie hará todo lo posible para guiarlo a analizar el diseño de componentes como los iteradores de interfaz de función de varios contenedores importantes en STL en un lenguaje hablado relativamente simple basado en lo que ha aprendido y comprensión simple durante mucho tiempo. tiempo. , Espero que puedas apoyar a Xiaojie, muchas gracias.  
  • El maravilloso enlace anterior se adjunta de la siguiente manera

Análisis de bloques desde el prototipo de función hasta la implementación de bloques de C++ STL (vector)

1. Piensa en el diseño del iterador de la lista.

  • En primer lugar, en cuanto al diseño del iterador de la lista, ya no es tan simple como el vector, porque la Lista no es un espacio de almacenamiento continuo para almacenar elementos, y no hay forma de acceder a los elementos tan directamente como el ++ operación del puntero nativo en el vector Para acceder a elementos posteriores al pedido,   pero el iterador es una clase que puede admitir operaciones ++ -- *, necesitamos poder atravesar y acceder a elementos en todo el contenedor a través del operación de iteradores ++
  • Debido a los requisitos anteriores, el nodo LIstNode* pnode en nuestra Lista debe ser administrado por una clase de iterador especialmente diseñada para él, de modo que el   pnode pueda acceder al siguiente elemento a través de la operación ++, la operación puede ir a acceder el elemento anterior
  • Entonces surgió la idea general, una clase de iterador de iterador de estructura encapsula y administra un puntero ListNode*, la función es una clase de puntero inteligente, administra un puntero ListNode*, alinea y realiza varias sobrecargas de operadores: para que nuestro puntero ListNode* pueda pasarse through ++ -- La operación implementa el recorrido del contenedor
  • clase de nodo

  •  Clase de iterador: aquí solo se pegan los componentes importantes del iterador. Para aclarar la idea, es bastante fácil escribir las funciones sobrecargadas del operador de otros iteradores.

  •  En primer lugar: establecemos el iterador en tres parámetros de plantilla, a saber, T Ref Ptr, ¿es necesario hacerlo? La respuesta es definitivamente Sí. Es imposible diseñar tres parámetros de plantilla en el código fuente STL sin ningún motivo.
  • Debido a que la secuencia posterior necesita usar ListIterator<T, Ref, Ptr> y ListNode<T>, pero es un poco problemático escribir con plantillas, por lo que se crean alias y el alias propio representa el tipo de cuerpo del iterador.

  •  La solución al problema anterior está realmente aquí. La clave principal es la generación de dos iteradores, uno es const_iterator y el otro es iterador. Una vez que lo establezcamos como tres parámetros de plantilla, cuando pasemos const, seguirá la plantilla. de la plantilla genera automáticamente una clase const para nosotros, por lo que ya no necesitamos diseñar una nueva clase para const, no hay necesidad de hacer este gasto manual, y no hay necesidad de escribir una nueva gestión de iterador const si hay un plantilla clase

2. Análisis y comprensión de prototipos de funciones importantes, con estos primeros escritos para ver el efecto.

Análisis de las funciones necesarias del framework iterator

  • operador * la sobrecarga es sacar los datos que contiene
  • opeartor-> sobrecargar es sacar la dirección de los datos
  • != y == Todo el mundo sabe, para determinar si es un iterador

  •  ++ -- operaciones necesarias para operar iteradores, representando hacia adelante y hacia atrás un nodo

Análisis de las funciones necesarias del marco List

  • Escribir una construcción predeterminada es simple, un nodo virtual sin datos actúa como encabezado y los punteros delantero y trasero apuntan al encabezado mismo.

  • Insertar un nodo al final, tres procesos
  1. Obtenga el puntero del nodo de cola pTail y cree un nuevo nodo nuevo
  2. pTail está conectado a newnode, newnode se convierte en la nueva cola
  3. Para formar un bucle, el nodo de cola de newnode debe estar conectado a la cabeza

  •  Lo anterior simplemente escribe que for_each usa iteradores como parámetros de plantilla, no para nada más, solo para que todos sientan por qué los iteradores    se pueden usar como el pegamento entre varios contenedores y algoritmos, y no hay manera sin iteradores Se admite el cruce de contenedores . . .             
  • Debido a que no hay un iterador para administrar el puntero del objeto, la clase de puntero inteligente le permite admitir la operación ++ -- * para atravesar todo el contenedor de forma genérica y realizar el procesamiento de funciones de algoritmo en los elementos del contenedor      (que todos los algoritmos transversales de STL no tienen La ley se cumple, y están todos paralizados)

3. Construir un marco general

#include <iostream>
#include <list>
#include <assert.h>
using namespace std;

namespace tyj {
	//链表节点的设计, 双向循环链表
	template<class T>
	struct ListNode {
		ListNode(const T& _val = T())
			: pre(nullptr)
			, next(nullptr)
			, val(_val) {
		}
		ListNode<T>* pre;
		ListNode<T>* next;
		T val;
	};

	//分析一下三个模板??? 为啥要三个,其实这个是STL源码里面这样设计的
	template<class T, class Ref, class Ptr>
	struct ListIterator {
		typedef ListNode<T> Node;
		typedef ListIterator<T, Ref, Ptr > self;
		Node* pnode;	//Iterator管理的指针

		ListIterator(Node* _pnode = nullptr)
			: pnode(_pnode) { 
		}
		Ref operator*() {
			return pnode->val;	//*重载访问val
		}

		Ptr operator->() {		//支持->访问
			return &pnode->val;
		}

		bool operator!=(const self& obj) const {
			return pnode != obj.pnode;
		}
		bool operator==(const self& obj) const {
			return pnode == obj.pnode;
		}

		self& operator++() {	//前置++ -- 操作返回本体
			pnode = pnode->next;
			return *this;
		}
		self operator++(int) {
			self before(*this);	//返回的是之前的
			pnode = pnode->next;
			return before;
		}


		self& operator--() {
			pnode = pnode->pre;
			return *this;
		}

		self operator--(int) {
			self before(*this);
			pnode = pnode->pre;
			return before;
		}

	};

	template<class T>
	class List {
		typedef ListNode<T> Node;//
	public:
		typedef ListIterator<T, T&, T*> iterator;
		typedef ListIterator<T, const T&, const T*> const_iterator;
		//此处体现出来了  Ref  Ptr模板化的好处了	
		List() : head(new Node) {
			head->pre = head->next = head;//双向链表的最初状态
		}

		iterator begin() {//第一个结点
			return iterator(head->next);
		}

		const_iterator begin() const {
			return const_iterator(head->next);
		}

		iterator end() {
			return iterator(head);//返回虚拟头部结点
		}

		const_iterator end() const {
			return const_iterator(head);//虚拟头部结点
		}

		void push_back(const T& val) {
			Node* pTail = head->pre;
			Node* newnode = new Node(val);
			//连接到尾部
			pTail->next = newnode;
			newnode->pre = pTail;
			//成环连接到head上
			newnode->next = head;
			head->pre = newnode;
		}

	private:
		Node* head;//头部指针指向头部结点

	};

	template<class InputIterator, class Function>
	void for_each(InputIterator first, InputIterator last, Function f) {
		while (first != last) {
			f(*first++);
		}
	}
	template<class T>
	struct Print {
		void operator()(const T& val) const {
			cout << val << " ";
		}
	};
}

template<class T>
void PrintList(tyj::List<T>& lt) {
	tyj::for_each(lt.begin(), lt.end(), tyj::Print<T>());
    cout << endl;
}

int main() {

	tyj::List<int> lt;
	for (int i = 0; i < 5; ++i) {
		lt.push_back(i);
	}
	PrintList(lt);

	return 0;
}

4. Análisis de bloques de los detalles de la función (similar al vector)

  • construcción de gama

  •  Tanto el vector como la lista se implementan de esta manera, atravesando el rango entrante y luego llamando al código de multiplexación push_back para implementar la rutina común entrante.
  • swap prepara el código para su reutilización
void swap(List& lt) {
	::swap(head, lt.head);
}
  • intercambio: intercambio muy puro y simple, intercambia tus miembros conmigo, me convertiré en ti, te convertirás en mí

  • Copie la construcción (debido al intercambio, me inicializo en nullptr para evitar punteros salvajes, y luego intercambio el objeto tmp construido multiplexando el código de construcción de rango para construirlo)

List(const List& lt) 
	: head(nullptr) {
	List tmp(lt.begin(), lt.end());
	swap(tmp);	//换, 复用范围构造
}
  • sobrecarga de tareas

//直接复用拷贝构造出来的lt
List& operator=(List lt) {
	head = nullptr;
	swap(lt);
	return *this;
}
  • El objeto lt construido por copia de multiplexación pura, usted es un objeto temporal de todos modos, y debe destruirse cuando sale del alcance. Reemplazaré directamente su estructura de recursos de almacenamiento dinámico subyacente y reutilizaré la estructura de copia ( esencialmente, el la estructura de rango se reutiliza ). ) 
  • Mover la construcción (arrebatar los recursos del montón del objeto moribundo para la construcción)

List(List&& lt) {
    head = nullptr;
	swap(lt);
}
  • Mover la construcción, cambiar directamente, no hacer nada más, ¿por qué puede ser así? Copiar la construcción necesita reutilizar la construcción del rango para construir un tmp para cambiar, pero mover la construcción es un cambio directo,   
  • Razón principal: el parámetro pasado por la construcción de movimiento es un valor real, qué es un valor real, un objeto moribundo, un objeto temporal, dado que el parámetro es un objeto moribundo, puedo usar directamente sus recursos de montón subyacentes para construirse a sí mismo

  • Después de escribir la interfaz insert() + erase(), es equivalente a escribir seis interfaces

  • insertar()

  • insert(pos, val); inserta un nodo newnode cuyo valor es val antes de la posición del iterador pos
  • push_back() reutiliza la función de inserción

void push_back(const T& val) {
	insert(end(), val);
    //在end()前插入一个newnode结点
}
  • push_front() reutiliza la función de inserción

void push_front(const T& val) {
	insert(begin(), val);
    //在begin前插入一个newnode结点    
}
  • borrar();

El resumen de erase(pos) son tres oraciones:

  1. Tome el siguiente nodo antes y después de pos
  2. eliminar posición
  3. Los nodos anterior y siguiente están conectados 
  • pop_back() reutilizar borrar

void pop_back() {
	erase(--end());
    //删除end()前一个迭代器
    //end() == head 
    //--end() == head->pre == pTail
}
  • pop_front() reutilizar borrar

void pop_front() {
	erase(begin());
    //begin() head->next;
    //head->next == firstnode
    //head是一个空头结点, firstnode才是真实的第一个元素
}

5. Código general + prueba

#include <iostream>
#include <list>
#include <algorithm>
#include <assert.h>
using namespace std;

namespace tyj {
	//链表节点的设计, 双向循环链表
	template<class T>
	struct ListNode {
		ListNode(const T& _val = T())
			: pre(nullptr)
			, next(nullptr)
			, val(_val) {
		}
		ListNode<T>* pre;
		ListNode<T>* next;
		T val;
	};

	//分析一下三个模板??? 为啥要三个,其实这个是STL源码里面这样设计的
	template<class T, class Ref, class Ptr>
	struct ListIterator {
		typedef ListNode<T> Node;
		typedef ListIterator<T, Ref, Ptr > self;
		Node* pnode;	//Iterator管理的指针

		ListIterator(Node* _pnode = nullptr)
			: pnode(_pnode) { 
		}
		Ref operator*() {
			return pnode->val;	//*重载访问val
		}

		Ptr operator->() {		//支持->访问
			return &pnode->val;
		}

		bool operator!=(const self& obj) const {
			return pnode != obj.pnode;
		}
		bool operator==(const self& obj) const {
			return pnode == obj.pnode;
		}

		self& operator++() {	//前置++ -- 操作返回本体
			pnode = pnode->next;
			return *this;
		}
		self operator++(int) {
			self before(*this);	//返回的是之前的
			pnode = pnode->next;
			return before;
		}


		self& operator--() {
			pnode = pnode->pre;
			return *this;
		}

		self operator--(int) {
			self before(*this);
			pnode = pnode->pre;
			return before;
		}

	};

	template<class T>
	class List {
		typedef ListNode<T> Node;
	public:
		typedef ListIterator<T, T&, T*> iterator;
		typedef ListIterator<T, const T&, const T*> const_iterator;
		//此处体现出来了  Ref  Ptr模板化的好处了	
		List() : head(new Node) {
			head->pre = head->next = head;//双向链表的最初状态
		}

		template<class InputIterator>
		List(InputIterator first, InputIterator last)
			: head(new Node) {
			head->pre = head->next = head;
			while (first != last) {
				push_back(*first++);
			}
		}

		void swap(List& lt) {
			::swap(head, lt.head);
		}

		List(const List& lt)
			: head(nullptr) {
			List tmp(lt.begin(), lt.end());
			swap(tmp);	//换, 复用范围构造
		}

		//直接复用拷贝构造出来的lt
		List& operator=(List lt) {
			head = nullptr;
			swap(lt);
			return *this;
		}

		List(List&& lt) {
			swap(lt);
		}

		iterator begin() {//第一个结点
			return iterator(head->next);
		}

		const_iterator begin() const {
			return const_iterator(head->next);
		}

		iterator end() {
			return iterator(head);//返回虚拟头部结点
		}

		const_iterator end() const {
			return const_iterator(head);//虚拟头部结点
		}

		//在pos位置插入一个val 
		void insert(iterator pos, const T& val) {
			assert(pos.pnode);//先断言结点位置存在, 不存在就无法插入
			Node* cur = pos.pnode;//先拿取到结点指针
			Node* pre = cur->pre;
			Node* newnode = new Node(val);//创建新的结点

			pre->next = newnode;
			newnode->pre = pre;
			//新的结点连接pre
			newnode->next = cur;
			cur->pre = newnode;
			//新的结点连接cur
		}

		void push_back(const T& val) {
			//Node* pTail = head->pre;
			//Node* newnode = new Node(val);
			连接到尾部
			//pTail->next = newnode;
			//newnode->pre = pTail;
			成环连接到head上
			//newnode->next = head;
			//head->pre = newnode;
			insert(end(), val);
		}
		 
		void push_front(const T& val) {
			insert(begin(), val);
		}


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

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

		//删除pos迭代器位置元素返回下一个元素
		iterator erase(iterator pos) {
			assert(pos.pnode);//存在才可以做删除操作
			assert(pos != end());
			//拿取到前面一个结点和后一个结点
			Node* pre = pos.pnode->pre;
			Node* next = pos.pnode->next;

			//删除现在iterator
			delete pos.pnode;

			pre->next = next;
			next->pre = pre;
			return iterator(next);
		}
		void clear() {
			Node* p = head->next, *q;
			while (p != head) {
				q = p->next;
				delete p;
				p = q;
			}
			delete head;
		}

		size_t size() {
			size_t ans = 0;
			Node* p = head->next;
			while (p != head) {
				ans += 1;
				p = p->next;
			}
			return ans;
		}

		~List() {
			if (head != nullptr)
				clear();
			head = nullptr;
		}
	private:
		Node* head;//头部指针指向头部结点
	};

	template<class InputIterator, class Function>
	void for_each(InputIterator first, InputIterator last, Function f) {
		while (first != last) {
			f(*first++);
		}
	}
	template<class T>
	struct Print {
		void operator()(const T& val) const {
			cout << val << " ";
		}
	};
}

template<class T>
void PrintList(tyj::List<T>& lt) {
	tyj::for_each(lt.begin(), lt.end(), tyj::Print<T>());
	cout << endl;
}


// 测试List的构造
void TestList1()
{
	tyj::List<int> l1;
	int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
	tyj::List<int> l3(array, array + sizeof(array) / sizeof(array[0]));
	PrintList(l3);
	tyj::List<int> l4(l3);
	PrintList(l4);
	l1 = l4;
	PrintList(l1);
}

void TestList2()
{
	// 测试PushBack与PopBack
	tyj::List<int> l;
	l.push_back(1);
	l.push_back(2);
	l.push_back(3);
	PrintList(l);
	l.pop_back();
	l.pop_back();
	PrintList(l);
	l.pop_back();
	cout << l.size() << endl;
	// 测试PushFront与PopFront
	l.push_front(1);
	l.push_front(2);
	l.push_front(3);
	PrintList(l);
	l.pop_front();
	l.pop_front();
	PrintList(l);
	l.pop_front();
	cout << l.size() << endl;
}

void TestList3()
{
	int array[] = { 1, 2, 3, 4, 5 };
	tyj::List<int> l(array, array + sizeof(array) / sizeof(array[0]));
	auto pos = l.begin();
	l.insert(l.begin(), 0);
	PrintList(l);

	++pos;
	l.insert(pos, 2);
	PrintList(l);
	l.erase(l.begin());
	l.erase(pos);
	PrintList(l);
	// pos指向的节点已经被删除,pos迭代器失效
	cout << *pos << endl;
	auto it = l.begin();
	while (it != l.end())
	{
		it = l.erase(it);
	}
	cout << l.size() << endl;
}

int main() {

	//tyj::List<int> lt;
	//for (int i = 0; i < 5; ++i) {
	//	lt.push_back(i);
	//}

	//PrintList(lt);
	//int arr[5] = { 1, 2, 3, 4, 5 };
	//tyj::List<int> lt2(arr, arr + 5);

	//PrintList(lt2);
	/*TestList1();*/

	/*TestList2();*/

	TestList3();
	return 0;
}

6. Resumen

  • En la medida en que la lista se compara con el vector, la lista puede reflejar mejor la importancia del diseño del iterador
  • En esencia, la capa inferior de la lista de STL es una lista enlazada circular bidireccional (estructura de datos), cuya parte más valiosa es el diseño del iterador. La forma en que el iterador une el contenedor + el algoritmo es particularmente importante.
  • Primero, necesitamos encapsular una estructura ListNode para escribir una lista enlazada:    
  • Necesitamos administrar los punteros de ListNode*, por lo que debemos diseñar la clase ListIterator para admitir la sobrecarga de operadores de varios punteros, para admitir métodos específicos de recorrido de contenedores y algoritmos de unión.
  •  Lecciones de escribir STL o escribir algo pequeño
  • Primero, vea si hay un determinado marco o documentos oficiales. Si los hay, divida el marco general en componentes, diseñe e implemente los componentes importantes por separado, lea los documentos para las interfaces importantes y analícelos e impleméntelos.
  • Escriba una prueba simple del marco. Después de que el marco no tenga problemas, podemos ver pequeños efectos y alegría, y luego mordisquear lentamente las interfaces clave. . Después de escribir la interfaz, debe probarse a tiempo.
  • Finalmente, el código general se prueba en varios escenarios especiales. . encontrar depuración

Gracias por leer el artículo de Xiaojie. Xiaojie continuará introduciendo algunas ideas de diseño y escritura de STL de acuerdo con su propio nivel de dominio. Si cree que la escritura de Xiaojie no es mala, preste atención y apoyo. Muchas gracias. Les deseo todo el mejor en sus estudios y carreras, y todos los que trabajan serán promovidos y elevados.

Supongo que te gusta

Origin blog.csdn.net/weixin_53695360/article/details/123647344
Recomendado
Clasificación