详细介绍:STL——list

介绍

list 双向链表,擅长中间位置 O(1) 插入/删除,但牺牲了随机访问性能与内存紧凑性;当需要频繁在非尾端增删元素时,list 是首选容器。

模拟实现

构造函数

成员初始化列表只能 初始化成员本身,不能在列表里访问成员的成员

template
struct myList_node
{
myList_node(const T& val = T())
:_val(val)
,_prev(nullptr)
,_next(nullptr)
{}
T _val;
myList_node* _prev;
myList_node* _next;
};
template
class myList {
typedef myList_node Node;
public:
myList()	// 初始化头结点
:_head(new Node)
{
_head->_next = _head;
_head->_prev = _head;
}
private:
Node* _head;
};

拷贝构造

遍历旧链表,逐个节点拷贝到新链表

如果 T 是申请了内存资源的自定义类型,那么这里会触发 T 的拷贝构造,只要 T 的 拷贝构造 实现了深拷贝的逻辑,这里构造新节点时就不需要考虑深拷贝问题

myList(const myList& ml)	// 拷贝构造
:_head(new Node)
{
_head->_next = _head;
_head->_prev = _head;
Node* cur = ml._head->_next;	// 旧
Node* tail = _head;	// 新
while (cur != ml._head)
{
Node* newNode = new Node(cur->_val);	// 构造新节点
tail->_next = newNode;	// 链接新节点
newNode->_prev = tail;
tail = tail->_next;
cur = cur->_next;
}
tail->_next = _head;	// 最后链接头节点
_head->_prev = tail;
}

迭代器

list 将迭代器封装为单独的类,并在类中实现运算符重载

构造

目前使用指针来实现迭代器

template
struct _myList_iterator
{
typedef myList_node Node;
Node* _node;
_myList_iterator(Node* node)	// 使用节点指针构造迭代器对象
:_node(node)
{}
};

运算符重载

重载迭代器可能用到的运算符,使迭代器的运算符的功能符合我们的预期

_myList_iterator& operator++()	// 前置++
{
_node = _node->_next;
return *this;
}
_myList_iterator operator++(int)	// 后置++
{
_myList_iterator tmp(_node);	// 构造临时对象
_node = _node->_next;
return tmp;
}
_myList_iterator& operator--()	// 前置--
{
_node = _node->_prev;
return *this;
}
_myList_iterator operator--(int)	// 后置--
{
_myList_iterator tmp(_node);	// 构造临时对象
_node = _node->_prev;
return tmp;
}
T& operator*()
{
return _node->_val;
}
// 加上 const 让 const 的迭代器也可以调用
bool operator==(const _myList_iterator& ml_it) const	// 判断迭代器的指向 是否相等
{
return _node == ml_it._node;
}
bool operator!=(const _myList_iterator& ml_it) const
{
return !(_node == ml_it._node);
}

另外,为迭代器类重载指针解引用运算符 -> 需要注意:

如果选择第一种,那么返回的是 T 的指针类型,随后可以继续接 -> 运算符来访问 T 中的成员;

如果选择第二种,那么返回的是 T 的引用,随后可以接 . 运算符来访问 T 中的成员;

T* operator->()
{
return &_node->_val;
}
T& operator->()
{
return _node->_val;
}

标准库实现的是第一种,那么实际上调用时,展开函数调用是这个样子:

_myList_iterator it(_node);
it.operator->()->“T的成员”

这里就连续地使用了两个 -> 解引用运算符,编译器为了优化可读性,直接两个变一个:

_myList_iterator it(_node);
it->“T的成员”

const 迭代器

const 迭代器无非就是不想让你修改 该迭代器 所指向的 对象 的 成员

那么在迭代器类中,涉及到对 迭代器 指向 对象 的操作的 方法,就是两个 解引用运算符的重载

将其 返回值类型 加上 const 属性 修饰即可

但是标准库中的实现:是为迭代器类添加了额外的 2 个模板参数,让迭代器类在实例化的时候,就确定该迭代器 是否为 const 迭代器

template
struct _myList_iterator
{
typedef myList_node Node;
Node* _node;
......
Ref operator*()
{
return _node->_val;
}
......
Ptr operator->()
{
return &_node->_val;
}
};

该迭代器实例化时传参示例如下:

_myList_iterator;
_myList_iterator;

在 list 中实例化

实例化迭代器

template
class myList {
typedef myList_node Node;
typedef _myList_iterator iterator;
typedef _myList_iterator const_iterator;
public:
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);
}

析构函数

需要逐个释放节点,使用指针即可

~myList() {
Node* cur = _head->_next;
while (cur != _head) {
Node* next = cur->_next; // 先保存下一个
delete cur;	// 再释放当前
cur = next;	// 继续
}
delete _head;
_head = nullptr;
}

operator=

 (copy-and-swap 的小技巧)

赋值运算符的重载,直接复用 拷贝构造 的逻辑:使用传值传参,这时形参是实参的拷贝,然后交换形参与被赋值对象的头节点指针;出了函数作用域, ml 会被自动调用析构函数释放掉

myList& operator=(myList ml)
{
std::swap(_head, ml._head);
return *this;
}

insert

在迭代器 position 前,插入一个值为 val 的元素

iterator insert(iterator position, const T& val)
{
Node* cur = position->_node;
Node* curPrev = cur->_prev;
Node* newNode = new Node(val);	// new 一个新节点
// 链接新节点
curPrev->_next = newNode;
newNode->_prev = curPrev;
newNode->_next = cur;
cur->_prev = newNode;
return iterator(newNode);
}

push_back

复用 insert

void push_back(const T& val)
{
insert(end(), val);
}

erase

释放当前迭代器处的节点,并返回下个新节点的迭代器

但是注意不能删除头节点,破坏双链表结构

iterator erase(iterator position)
{
Node* cur = position._node;
if (cur == _head)	// 不能删除头节点
{
return iterator(_head);
}
Node* curPrev = cur->_prev;	// 链接
Node* curNext = cur->_next;
curPrev->_next = curNext;
curNext->_prev = curPrev;
delete cur;
return iterator(curNext);
}

pop_back

复用 erase,也要注意不能删除头节点

void pop_back()
{
if (_head->_prev != _head)
{
erase(iterator(_head->_prev));
}
}

resize

void resize (size_t n, T val = T());

resize 依然要分段考虑,如果 n <= size( ),将链表从后向前缩减至 size() == n;(复用pop_back)

如果 n > size(),就 push_back n 个值为 val 的节点

那么首先实现 size(),难免要遍历一遍链表;当然,也可以提前在 myList 类中定义成员变量 _size,来记录当前链表的size

size_t size()
{
size_t cnt = 0;
iterator it = begin();
while (it != end())
{
cnt++;
it++;
}
return cnt;
}
void resize(size_t n, T val = T())
{
size_t cur_size = size();    // 提前缓存 size()
while (cur_size > n)
{
pop_back();
cur_size--;
}
while (cur_size < n)
{
push_back(val);
cur_size++;
}
}

这里为避免每次循环都调用 size() ,提前缓存好即可(进一步提升性能还得是像标准库中一样,维护一个 _size 成员来记录)

小结

有了一些手撕容器的思路,要注意深拷贝问题,从 拷贝构造 到 赋值运算符重载;

元素 的 方法中,要注意构造对象时的操作; 方法中,要注意对资源的深度清理

迭代器的封装:主要是为了实现让迭代器的 运算符重载 支持预期的功能;以及迭代器需要实现两套(const

且在使用过程中,要注意迭代器的失效问题,在 vector 容器中扩容之后尤其注意;因为 list 不涉及扩容,所以迭代器失效问题不明显;但只要迭代器失效就不能再使用,否则行为未定义

注意被封装的迭代器中,对 -> 解引用运算符的重载,其实是为 list<string<T>> 这样的类型准备的;这样迭代器就可以通过 -> 解引用实现对 string<T> 成员的访问,实际上有两层解引用

posted @ 2025-08-01 21:02  wzzkaifa  阅读(10)  评论(0)    收藏  举报