STL 목록의 시뮬레이션 구현

목차

기본 사상

시뮬레이션 구현

1. list_node 구현

2. 목록의 기본 틀

<1> 멤버 변수

<2> 매개변수 생성자 없음

3.__list_iterator (목록 반복자 구현) 이 기사의 가장 중요한 내용

<1>__list_iterator의 생성자 및 복사 생성자

<2>* 과부하

<3> ++와 --의 오버로딩

<4> != 및 ==의 오버로딩

4. 목록을 계속 구현합니다.

<1> 푸시백 인터페이스

<2>삽입

<3> 목록의 반복자 인터페이스 시작 및 종료 키 콘텐츠

<*>비어 있음

<4>삭제

<5> push_back, push_front 및 pop_back, pop_front 기능을 구현하기 위해 지우기와 삽입을 적용합니다.

<6>.반복자 생성자  

<*>교환

<7> 현대 작문의 복사 생성자와 대입 복사

<*>지우다

 <8> 소멸자

<*>중요 콘텐츠 확장

요약하다


기본 사상

C++에서 리스트의 구현은 연결 리스트(linked list) 형태로 구현되는데, 연결 리스트의 특징은 메모리의 연속적인 공간에 저장되지 않고 계속해서 새로운 랜덤 메모리를 열어 데이터를 저장한다는 점이다. 방법은 데이터를 삽입하는 입니다 . 문자열 및 벡터와 같은 데이터를 이동할 필요가 없으므로 많은 시간 비용이 듭니다.

그러나 그 단점도 분명합니다

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: 클래스 대신 구조체를 사용하는 이유는 무엇입니까?

Class는 encapsulation에 상대적으로 타이트한데, class를 사용하면 list에 해당 노드에 접근하는 데 도움이 되지 않으므로 struct를 사용하면 나중에 확실히 체감할 수 있습니다.

질문 2: 여기에 소멸자를 작성해야 합니까?

아니요, 클래스와 개체 과학이 견고하다면 이 세 가지 멤버 변수는 모두 내장 유형이며 자체 소멸자를 호출합니다.

2. 목록의 기본 틀

<1> 멤버 변수

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

이것에 대해 할 말이 없습니다. 목록은 _head sentinel 노드만 가리킵니다.

<2> 매개변수 생성자 없음

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

센티널 비트를 위한 공간만 열면 되고 연결 리스트가 데이터를 저장하지 않으면 _head의 _next와 _prev가 모두 자신을 가리킵니다.

3.__list_iterator (목록 반복자 구현) 이 기사의 가장 중요한 내용

우리는 전에 문자열과 벡터 단어를 배웠습니다. 우리는 그들의 반복자가 네이티브 포인터라는 것을 분명히 알 수 있습니다. 왜 그렇습니까?

우선 iterator는 포인터처럼 존재한다는 것을 이해해야 합니다.문자열과 벡터의 경우 연속 저장 메모리 공간이므로 iterator는 네이티브 포인터를 직접 사용할 수 있습니다 .

연결된 목록의 경우 반복자는 어떻게 구현됩니까? 반복자가 문자열 및 벡터 반복자만큼 쉽게 저장 공간을 통과하는 방법은 무엇입니까?

template<class T, class Ref, class Ptr>
struct __list_iterator{
	typedef list_node<T> 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> 푸시백 인터페이스

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> 목록 반복자 인터페이스 시작 및 종료의    주요 내용

이전에 __list_iterator에서 그런 질문이 있었습니다. 왜 여기에 세 개의 템플릿 매개변수가 있습니까?

Ref는 참조(레퍼런스)를 가리키고, Ptr은 포인터를 가리킵니다.

여기서 목록이 const인지 여부를 일치시켜야 합니다.

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인 경우 < T, const T&를 반환합니다. , const T*>는 매개변수 템플릿의 반복자, Ref는 const T&, Ptr은 const T*, 매개변수 템플릿이 <T, const T&, const T*>인 것으로 인식되면 *Overloaded일 때 호출한다. , const T&를 반환하고, 그렇지 않으면 T&를 반환합니다.

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

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

시작과 끝의 범위는 왼쪽이 닫히고 오른쪽이 열리기 때문에 시작은 _head의 _next(즉, 첫 번째 유효한 스토리지 노드)를 가리킵니다.

끝점은 _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;
}

clear와 달리 소멸자는 센티넬 비트를 해제해야 합니다. 그렇지 않으면 메모리 누수가 발생합니다!

<*>  중요 콘텐츠 확장

그런 경우가 있다면 리스트에 저장되는 것은 구조체나 클래스이고, 이러한 구조체의 멤버 변수와 함수에 접근하고 싶다면 __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로 돌아가 보겠습니다. 왜냐하면 오버로드된 ->에는 매개 변수가 없기 때문 입니다. 그런 다음 매우 이상한 현상을 발견하게 됩니다. 누락된 ->이 있는지, it->- >_date여야 하지 않습니까?

사실, 잘못된 것은 없고 ->가 두 개 있어야 하는데, 컴파일러가 코드 가독성을 위해 ->-> here to ->를 단순화하기 때문입니다.

그리고 여기에 ->->라고 쓰면 대신 오류를 보고합니다!

여기서 반환 값은 *의 오버로드와 다소 유사합니다. const 목록인 경우 const T*를 반환합니다.

요약하다

리스트와 문자열, 벡터의 가장 큰 차이점은 이터레이터의 차이이므로 우리는 주로 이터레이터 구현을 배우기 위해 리스트를 배웁니다.

STL도 비슷한데 하나를 배우면 사실 사용의 관점에서 다른 것들은 데이터 구조가 완전히 달라도 사용하게 됩니다.

추천

출처blog.csdn.net/fengjunziya/article/details/130574156