목차
3.__list_iterator (목록 반복자 구현) 이 기사의 가장 중요한 내용
<1>__list_iterator의 생성자 및 복사 생성자
<3> 목록의 반복자 인터페이스 시작 및 종료 키 콘텐츠
<5> push_back, push_front 및 pop_back, pop_front 기능을 구현하기 위해 지우기와 삽입을 적용합니다.
기본 사상
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도 비슷한데 하나를 배우면 사실 사용의 관점에서 다른 것들은 데이터 구조가 완전히 달라도 사용하게 됩니다.