目录
节点篇
链表中的节点包含储存的数据、前一个节点的指针、后一个节点的指针。在实现节点时还要将其初始化。那么这里我们不知道模板参数T具体是什么类型,就给了一个缺省参数,使用T的构造函数初始化生成一个匿名对象作为val的默认值,使用const T&延长了匿名对象的生命周期。
特别注意一点,初始化列表的写法,第一行的:别写成了;。
//T是节点中存储的数据类型
template <class T>
//节点结构
struct List_Node
{
//节点中包含的元素
List_Node* _next;
List_Node* _prev;
T _data;
//构造函数初始化节点
//初始化列表:别写成;了!!!!!!!!!!!!!!!
List_Node(const T& val = T())
: _next(nullptr)
, _prev(nullptr)
, _data(val)
{}
};
迭代器篇
链表中的迭代器与vector中的有所不同。
后者的迭代器只是一个单纯的指向数组内容的指针,由于数组的空间是连续的,所以可以满足迭代器的++,--,*等需求,但是这里链表的节点空间不是连续的,无法满足迭代器的基本需求,所以不能将迭代器定义为单纯的指向节点内容的指针。
于是聪明的人们用一个结构体模板来封装迭代器,并在结构体里面重载运算符,来实现迭代器的基本操作。
首先我们需要一个成员来表示此时指向的节点的指针,并用这个节点的指针来初始化该成员,这个成员的类型肯定也是节点的指针。
接下来我们就要完成一些符号的重载,来满足迭代器的操作需求。
还有一个问题就是不仅仅有普通迭代器iterator还要有const_iterator,为了简化代码,在实现时不实现两个模板结构体,就多使用了两个模板参数Ref和Ptr。
比如说在重载*时,普通情况下我们拿到的是T&就是节点储存的数据的引用(为什么不直接返回T?原因是我们要修改内容,而返回T拿到的只是一个临时副本,无法修改),但是有时我们不希望他被修改就需要返回const T&,所以为了方便就使用Ref来替代,使用Ptr来替代也是同理。
template <class T,class Ref,class Ptr>
struct list_iterator
{
typedef List_Node<T> node;
typedef list_iterator<T, Ref, Ptr> Self;
node* _node;
list_iterator(node* Node)
:_node(Node)
{}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
Self& operator++()
{
_node = _node->_next;
return *this;
}
Self operator++(int)
{
Self tmp(*this);
_node = _node->_next;
return tmp;
}
Self& operator--()
{
_node = _node->_prev;
return *(this);
}
Self operator--(int)
{
Self tmp(*this);
_node = _node->prev;
return tmp;
}
bool operator!=(const Self& s)
{
return _node != s._node;
}
bool operator==(const Self& s)
{
return _node == s._node;
}
};
链表篇
链表的成员变量主要包含头节点的指针和节点个数。
构造函数
构造函数的主要目的就是创建一个头节点出来,为了简化实现不同构造函数时的代码,将构造头节点的主要过程封装在函数empty_init()中。
默认构造函数里面会new一个node大小的空间,并会调用node的默认构造函数把这个节点初始化,同时返回这个空间的地址给_head,然后再改变_next和_prev的指向,并将_size置为0。
使用花括号{}构造的构造函数List(std::initializer_list<T> il),内部同样会使用empty_init(),然后再将花括号{}中的内容尾插过来。
void empty_init()
{
//初始化头节点
_head = new node;
_head->_next = _head;
_head->_prev = _head;
_size = 0;
}
List()
{
empty_init();
}
List(std::initializer_list<T> il)
{
empty_init();
for (auto& e : il)
{
push_back(e);
}
}
拷贝构造函数
1.调用empty_init()初始化头节点。
2.尾插链表内容。
List(List<T>& lt)
{
empty_init();
for (auto& t : lt)
{
push_back(t);
}
}
赋值=重载
和vector中的赋值重载类似,需要实现一个swap函数来交换链表中的内容。
当lt2给lt1赋值,首先会调用拷贝构造将lt2的内容拷贝给lt,再将lt1的内容和lt交换,出了作用域lt就会销毁机lt1之前的空间就被销毁,而此时的lt1指向的就是拷贝构造出来的lt指向的空间。
void swap(List<T>& lt)
{
std::swap(_head, lt._head);
std::swap(_size, lt._size);
}
//lt1 = lt2
List<T>& operator=(List<T> lt)
{
swap(lt);
return (*this);
}
迭代器
在链表中需要提供迭代器接口供外部使用。
那么如何在类中使用我们定义的迭代器结构体?如何将迭代器结构体和链表类联系起来呢?
解决方法是使用typedef将模板实例化的复杂类型嵌套定义为类的成员类型,在类中有了成员类型就可以使用这个成员类型来定义变量了。
typedef list_iterator<T, T&, T*> iterator;
typedef list_iterator<T, const T&, const T*> const_iterator;
为了满足使用需求,还需要提供供外部使用的迭代器,分为普通迭代器和const迭代器。这里返回的是一个调用迭代器类中的构造函数的匿名对象。
本来的过程是,先在函数内构造匿名对象,然后通过拷贝构造函数将这个匿名对象拷贝到返回值位置,最后匿名对象被销毁。
编译器优化后变为,直接在返回值的内存位置构造这个匿名对象,完全跳过了临时对象的创建和拷贝步骤。
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);
}
析构函数
链表的各个节点不是存在于一块连续的空间, 空间没法一次性释放,必须将节点逐个释放。所以要先实现一个函数clear()来释放链表中各个节点(头节点除外,因为clear()不止用在析构函数中)的空间。再将头节点空间释放。
~List()
{
clear();
delete _head;
_head = nullptr;
_size = 0;
}
void clear()
{
auto it = begin();
while (it != end())
{
it = erase(it);
}
}
插入和删除
插入和删除的函数之间都有很强的复用性。实现节点的插入和删除,只需改变目标节点的_prev和_next即可,插入要创建节点空间,删除要释放节点空间。
void push_back(const T& val)
{
/*node* newnode = new node(val);
node* tail = _head->_prev;
tail->_next = newnode;
newnode->_next = _head;
newnode->_prev = tail;
_head->_prev = newnode;
_size++;*/
insert(end(), val);
_size++;
}
void push_front(const T& val)
{
insert(begin(), val);
}
void pop_back()
{
erase(--end());
}
void pop_front()
{
erase(begin());
}
void insert(iterator pos, const T& val)
{
node* pcur = pos._node;
node* prev = pcur->_prev;
node* newnode = new node(val);
newnode->_prev = prev;
newnode->_next = pcur;
prev->_next = newnode;
pcur->_prev = newnode;
_size++;
}
iterator erase(iterator pos)
{
assert(pos != end());
node* pcur = pos._node;
node* prev = pcur->_prev;
node* next = pcur->_next;
prev->_next = next;
next->_prev = prev;
delete pcur;
--_size;
//erase删完返回下一个节点的地址
return next;
}
补充篇
迭代器失效
list的erase在使用过后会将pos指向的那块空间释放并置为nullptr,那删除后就无法执行迭代器的相关操作,所以要在erase函数中返回下一个节点的地址,在循环中连续使用erase时就要重新给迭代器赋值。
while (it != lt.end())
{
//删除偶数
if (*it % 2 == 0)
{
//迭代器失效了,重新赋值
it = lt.erase(it);
}
else
{
it++;
}
}
节点中的数据自定义类型的情况
struct AA
{
int a1;
int a2;
AA(int a = 0, int b = 3)
:a1(a)
, a2(b)
{}
};
void test02()
{
//当链表节点存储的数据是自定义类型时
List<AA> lt;
AA aa = { 1,2 };
lt.push_back(aa);
List<AA>::iterator it = lt.begin();
//for (auto& e : lt)
//{
// //e开始拿到的是lt的begin()迭代器,而这个迭代器呢指向的内容是一个自定义类型
// //而库中的<<只能打印内置类型,如果要打印就要在AA中重载<<
// cout << e << " ";
//}
while(it != lt.end())
{
//(*it)是一个自定义类型AA,自定义类型访问成员变量用.操作符
cout << (*it).a1 << " " << (*it).a2 << " ";
//自定义类型的指针可以通过->来访问成员函数和成员变量
//it是一个迭代器,底层是一个指针,其初始化成lt的第一个节点的指针
//那么it的内容就是一个指针,通过->可以访问节点的内容,这个内容就包含了T _data(自定义类型的对象的指针)
//这个_data是一个指针变量,即AA的对象aa的地址
//所以要访问aa中的a1,a2就要再次使用->
//为了易读性,这里省略一个->
cout << it->a1 << " " << it->a2 << " ";
//本质是
cout << it.operator->()->a1 << " " << it.operator->()->a2 << " ";
it++;
}
}
}
"希望这篇内容能帮你少走弯路。保持好奇,持续编码!🚀"