Имитационная реализация STL-списка

Оглавление

основная концепция

Реализация моделирования

1. Реализация list_node

2. Базовая структура списка

<1> переменная-член

<2> Нет конструктора параметров

3.__list_iterator (реализация итератора списка) Самое важное содержание этой статьи

<1>Конструктор и конструктор копирования __list_iterator

<2>* перегрузка

<3> Перегрузка ++ и --

<4> Перегрузка != и ==

4. Продолжайте выполнять список

<1> интерфейс push_back

<2>вставить

<3> интерфейс итератора списка начало и конец содержимого ключа

<*>пусто

<4>стереть

<5> применить стирание и вставку для реализации функций push_back, push_front и pop_back, pop_front

<6>.Конструктор итератора  

<*>обменять

<7> конструктор копирования и копия присваивания в современном письме

<*>очистить

 <8> Деструктор

<*>Развернуть важный контент

Подведем итог


основная концепция

В C++ реализация списка реализована в виде связанного списка.Характеристикой связанного списка является то, что он не хранится в непрерывном пространстве в памяти, а постоянно открывает новую случайную память для хранения данных.Самое большое преимущество этого хранилища метод заключается в том , чтобы вставить данные, не нужно будет перемещать данные, такие как строки и вектора, что приводит к большим временным затратам.

Но его недостатки также очевидны

1. Большой объем прерывистой памяти может привести к большому количеству фрагментов памяти в середине, но не может быть эффективно использован.

2. Не может поддерживать произвольный доступ, например, sting и vactor могут использовать [num] для произвольного доступа.

3. Каждый раз, когда вы вставляете данные, вам нужно запрашивать новое пространство, а коэффициент использования кеша невелик.

Но даже при этом связанный список по-прежнему является важной частью STL, и его недостатки не затмевают его достоинств. Давайте просто смоделируем и реализуем часто используемые интерфейсные функции в STL.

Реализация моделирования

1. Реализация list_node

Из приведенного выше рисунка видно, что связанный список сохраняется одним узлом за другим.Узел имеет сохраненные данные и указатели на предыдущий узел и следующий узел внутри, поэтому мы можем реализовать list_node через структуру.

template<typename T>
	struct list_node {
		typedef list_node<T> node;

		T _data;
		node* _prev;
		node* _next;

		list_node(const T& val = T()) //缺省值为T的默认构造
			:_data(val)
			,_prev(nullptr)
			,_next(nullptr)
		{}
	};

Для конструктора list_node нам нужно только передать значение и установить _prev и _next в nullptr (нулевой указатель)

Вопрос 1: Зачем использовать структуру вместо класса?

Класс относительно тугой для инкапсуляции, если мы используем класс, то список не способствует доступу к его узлам, поэтому используйте структуру, и вы это сразу почувствуете.

Вопрос 2: Нужно ли писать здесь его деструктор?

Нет, если ваш класс и объектная наука надежны, все эти три членские переменные являются встроенными типами и будут вызывать свои собственные деструкторы.

2. Базовая структура списка

<1> переменная-член

class list { 
public:
	typedef list_node<T> node;
private:
	node* _head;
};

Говорить об этом нечего, список лишь указывает на дозорный узел _head.

<2> Нет конструктора параметров

void ini_empty()
{
	//初始化哨兵位节点
	_head = new node;
	_head->_next = _head;
	_head->_prev = _head;
}
//无参构造函数
list()
{
	ini_empty();
}

Нам нужно только освободить место для дозорного бита, и когда связанный список не хранит никаких данных, и _next, и _prev в _head указывают на себя.

3.__list_iterator (реализация итератора списка) самое важное содержание этой статьи

Мы выучили строковые и векторные слова раньше, мы можем ясно знать, что их итераторы являются нативными указателями, почему?

Прежде всего, нам нужно понять, что итераторы существуют точно так же, как указатели.Для строк и векторов, поскольку они представляют собой непрерывное пространство памяти для хранения, их итераторы могут напрямую использовать собственные указатели .

Итак, для связанного списка, как реализован его итератор? Как его итераторы могут перемещаться по нашему пространству хранения так же легко, как строковые и векторные итераторы?

template<class T, class Ref, class Ptr>
struct __list_iterator{
	typedef list_node<T> node;

    node* _node;
};

У вас может возникнуть много вопросов, итератор списка на самом деле является структурой, его переменные-члены также являются node*, и в этой структуре есть три типа параметров шаблона.

Потихоньку ответим на эти вопросы

Прежде всего, почему итератор списка является структурой?

Потому что, если он хочет иметь возможность перемещаться по сохраненным данным, как по необработанному указателю, невозможно напрямую использовать строку и вектор в качестве необработанного указателя, потому что это прерывистое пространство для хранения. Мы можем получить доступ к следующему узлу только через _next узла, и самая важная причина в том, что структура, как и класс, поддерживает перегрузку операторов! 

<1>Конструктор и конструктор копирования __list_iterator

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

__list_iterator(const iterator& lt)
	:_node(lt._node){}

Думаю, это нетрудно понять каждому, и в этом нет ничего нового.

<2>* перегрузка

Ref operator*()
{
	return _node->_data;
}

После перезагрузки * мы можем напрямую обращаться к данным узла через (*iterator).

<3> Перегрузка ++ и --

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;
}

Видя это, ясно ли, что после перегрузки ++ и -- мы можем получить доступ к данным в следующей (предыдущей) позиции, например ++(--), как векторный итератор.

Следует отметить, что сообщения ++ и -- должны возвращать исходный итератор, а возвращаемое значение не может быть возвращено по ссылке, поскольку tmp является временной переменной.

<4> Перегрузка != и ==

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

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

4. Продолжайте выполнять список

<1> интерфейс push_back

void push_back(const T& val)      //尾插
{
	node* newnode = new node(val);//创建新节点

	node* tail = _head->_prev;    //提前记录尾部节点
    //节点的指向重新设定
	tail->_next = newnode;
	newnode->_prev = tail;
	_head->_prev = newnode;
	newnode->_next = _head;
}

Очень простая реализация push_back, никаких сложностей

<2>вставить

iterator insert(iterator pos, const T& val)  //在迭代器pos位置插入一个节点
{  
	node* newnode = new node(val);    //创建新节点
	node* cur = pos._node;            //提前记录pos所指结点
	node* prev = cur->_prev;  
	prev->_next = newnode;
	newnode->_prev = prev;
	newnode->_next = cur;
	cur->_prev = newnode;
	return pos;
}

Здесь return возвращает позицию итератора pos.

<3> Ключевое содержимое интерфейса итератора списка begin и end   

У нас раньше был такой вопрос в __list_iterator, почему у нас тут три шаблонных параметра?

Ref ссылается на ссылку (reference), Ptr ссылается на указатель

Здесь вам нужно сопоставить, является ли список константным

class list { 
public:
	typedef list_node<T> node;
	typedef __list_iterator<T, T&, T*> iterator;
	typedef __list_iterator<T, const T&, const T*> 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);
}

};

Основываясь на нашем прошлом опыте написания строк и векторов, наиболее важными интерфейсами между их итераторами и ними являются функции начала и конца, и список, который мы пишем сейчас, не является исключением.

Если мы определяем список const, то он должен построить итератор const, и как мы можем определить, является ли он const, мы можем судить, добавив больше параметров шаблона Ref и Ptr, если он является const, то вернуть a < T, const T& , const T*> — итератор шаблона параметра, Ref — const T&, Ptr — const T*, если распознано, что шаблон параметра <T, const T&, const T*>, вызывается *При перегрузке , он вернет const T&, иначе вернет T&

Ref operator*()
{
	return _node->_data;
}

-------------------------------------------------- -------------------------------------------------- ----------------------------

Поскольку диапазон начала и конца закрыт слева и открыт справа, начало указывает на _next of _head (то есть на первый допустимый узел хранения).

end указывает на _head (сам часовой).

<*>пусто

Функция: определить, является ли связанный список пустым

bool empty()
{
	return begin()._node == _head;  //begin是否是指向哨兵位
}

Если begin указывает на дозорный бит, это означает, что в этом узле нет данных, то есть пустой связанный список.

<4>стереть

bool empty()
{
	return begin()._node == _head;  //begin是否是指向哨兵位
}

iterator erase(iterator pos)
{
	assert(pos._node != end()._node);    //不能把哨兵位给删了
	assert(!empty());                    //不能是空链表
	node* cur = pos._node;
	node* prev = cur->_prev;
	node* next = cur->_next;
	prev->_next = next;
	next->_prev = prev;
	delete cur;
	return iterator(next);
}

Обратите внимание, что удаляемый узел освобождается для удаления, и возвращается следующий узел удаленного узла.

<5> применить стирание и вставку для реализации функций push_back, push_front и pop_back, pop_front

void push_back(const T& val)     //尾插
{
	/*node* newnode = new node(val);
	node* tail = _head->_prev;
	tail->_next = newnode;
	newnode->_prev = tail;
	_head->_prev = newnode;
	newnode->_next = _head;*/

	//套用insert
	insert(end(), val);
}

void push_front(const T& val)    //头插
{
	//套用insert
	insert(begin(), val);
}


void pop_back()                 //尾删
{
	erase(--end());
}

void pop_front()                //头删
{
	erase(begin());
}

<6>.Конструктор итератора  

//迭代器构造
template<typename InputIterator>
list(InputIterator first, InputIterator last)
{
	ini_empty();    //初始化哨兵位
	while (first != last)
	{
		push_back(*first);
		++first;
	}
}

Почему его конструктор итератора добавляет параметры шаблона?    

Например, если вы хотите вставить ряд данных в вектор, вы можете вставить данные в список, передав диапазон итератора вектора, не правда ли, это очень удобно!

<*>обменять

Функция: обмен данными двух списков

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

Если мы хотим обменяться данными двух связанных списков, нам не нужно перемещать их один за другим, нам просто нужно поменять местами их сигнальные биты.

<7> конструктор копирования и копия присваивания в современном письме

list(const list& lt)      //在类里可以不加模版参数(但是推荐加上)
{
	ini_empty();
	list tmp(lt.begin(), lt.end());
	swap(*this, tmp);
}

list<T>& operator=(list<T> lt)
{
	swap(*this, lt);
	return *this;
}

Мы выступаем за современный способ написания текстов и копий заданий, потому что они действительно лаконичны! ! !

<*>очистить

Функция: Очистить все действительные узлы хранения в связанном списке (за исключением битов сторожевого устройства).

void clear()
{
	auto it = begin();
	while (it != end())
	{
		it = erase(it);
	}
}

 <8> Деструктор

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

В отличие от очистки, деструктор должен сбрасывать сторожевой бит, иначе произойдет утечка памяти!

<*>Развернуть  важный контент

Если есть такой случай, то то, что хранится в списке, является структурой или классом, и если мы хотим получить доступ к переменным-членам и функциям в этих структурах, мы можем перегрузить его оператор -> в __list_iterator, чтобы этот упрощенный доступ к данные.

Возьмите следующий код в качестве примера

struct Pos
{
    int _a1;
    int _a2;

    Pos(int a1 = 0, int a2 = 0)
	    :_a1(a1)
	    , _a2(a2)
        {}
};
void list_test3()
{
	list<Pos> lt;
	lt.push_back(Pos(10, 20));
	lt.push_back(Pos(10, 21));

	list<Pos>::iterator it = lt.begin();
	while (it != lt.end())
	{
		//cout << (*it)._a1 << ":" << (*it)._a2 << endl;
		cout << it->_a1 << ":" << it->_a2 << endl;

		++it;
	}
	cout << endl;
}
Ref operator*()
{
	return _node->_data;
}

Ptr operator->()        //注意,这里竟然没有参数!!
{
	return &(operator*());         //这种写法是什么意思呢?
}

Присмотритесь к методу перегрузки -> здесь, что означает &(operator*());?

Просто возьмите адрес _node->_data.

Вернёмся к приведенному выше примеру it->_a1, т.к. наш перегруженный -> не имеет параметров , то мы обнаружим очень странное явление, отсутствует ли ->, не должно ли быть оно->->_date?

На самом деле ничего плохого нет, их должно быть два ->, а потому компилятор упрощает ->-> здесь до -> для читабельности кода.

А если мы напишем здесь ->->, вместо этого он сообщит об ошибке!

Возвращаемое значение здесь чем-то похоже на перегрузку *, если это список const, он вернет const T*.

Подведем итог

Самая большая разница между списком, строкой и вектором — это разница между итераторами, поэтому мы изучаем список главным образом для изучения реализации итератора.

STL похож: после изучения одного, фактически, с точки зрения использования, будут использоваться и другие, даже если их структуры данных совершенно разные.

рекомендация

отblog.csdn.net/fengjunziya/article/details/130574156
рекомендация