目录
[二、push_back 函数](#二、push_back 函数)
[三、迭代器 iterator](#三、迭代器 iterator)
[3.1 iterator 结构体](#3.1 iterator 结构体)
[3.2 begin() 与 end() 函数](#3.2 begin() 与 end() 函数)
[3.3 iterator 运算符重载](#3.3 iterator 运算符重载)
[3.4 -> 的重载](#3.4 -> 的重载)
[3.5 const_iterator](#3.5 const_iterator)
[5.1 insert 函数](#5.1 insert 函数)
[5.2 erase 函数](#5.2 erase 函数)
[5.3 push 函数](#5.3 push 函数)
[5.4 pop 函数](#5.4 pop 函数)
[6.1 size 函数](#6.1 size 函数)
[6.2 empty 函数](#6.2 empty 函数)
一、成员变量
cpp
template<class T>
struct ListNode
{
T _data;
ListNode<T>* _prev;
ListNode<T>* _next;
ListNode(const T& x = T())
:_data(x)
,_prev(nullptr)
,_next(nullptr)
{}
};
template<class T>
class my_list
{
typedef ListNode<T> Node;
private:
Node* _head;//哨兵位头结点
};
cpp
my_list()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
二、push_back 函数
为了测试方便,我们在这里先把 push_back 写出来。
cpp
void push_back(T data)
{
Node* NewNode = new Node(data);
NewNode->_next = _head;
NewNode->_prev = _head->_prev;
_head->_prev->_next = NewNode;
_head->_prev = NewNode;
}
三、迭代器 iterator
3.1 iterator 结构体
这里的迭代器和之前的迭代器有所不同,诸如 string 、 vector ,他们的内存空间是连续的,迭代器的底层都是基于指针的,而 list 中,由于存储单元的随机性,迭代器不可以再是单纯的指针,而是像上面那样的结构体对象:
cpp
template<class T>
struct ListIterator
{
typedef ListNode<T> Node;
typedef ListIterator<T> iterator;
Node* _node;
ListIterator(Node* node)//构造函数
{
_node = node;
}
};
3.2 begin() 与 end() 函数
然后,需要在 my_list 类中新添 begin 和 end 函数,因为他们都是服务于迭代器,所以他们的返回值类型应该是迭代器,还要注意,根据 STL 通用设计模式, end() 指向的位置不用于存放有效数据元素,而是作为遍历结束的标记。所以 end() 函数可以直接返回通过 _head 构造的迭代器,以此表示链表的末尾。
cpp
template<class T>
class my_list
{
typedef ListNode<T> Node;
public:
typedef ListIterator<T> iterator;
iterator begin()
{
return iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
private:
Node* _head;
};
3.3 iterator 运算符重载
随后,为了使迭代器可以跑起来,需要再对 ++ 、 -- 、 != 等运算符进行重载,
其中,为了保持与内建类型行为一致,内建类型(如int
、float
等)的前置自增和自减操作 返回的是引用 。自定义类型(如迭代器)模拟内建类型的行为,包括操作符的使用,有助于保持语言的一致性和直观性。
后置自增和自减不返回其引用 的原因与前置自增自减的设计意图及使用方式有关。后置自增操作符的主要特点是先返回对象当前的值(在自增之前的状态),然后再对对象进行自增操作。这一行为决定了其返回类型应该是对象的值 而不是引用
顺便,我们可以把 == 和 * 运算符都重载:
cpp
typedef ListIterator<T> iterator;
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;
}
T& operator*()
{
return _node->_data;
}
bool operator!=(const iterator& it)
{
return _node != it._node;
}
bool operator==(const iterator& it)
{
return _node == it._node;
}
3.4 -> 的重载
当我们创建一个自定义类型,如 class A(此时在Flash命名空间内):
cpp
class A
{
public:
int a = 0;
int b = 0;
};
去测试 A 类型的 list 时,虽然 push_back 可以用,但是我们想遍历链表时,迭代器却不能用了:
cpp
void test()
{
my_list<A> la;
la.push_back({ 6, 12 });
la.push_back({ 7, 18 });
la.push_back({ 3, 22 });
my_list<A>::iterator it = la.begin();
while (it != la.end())
{
cout << *it << " ";
it++;
}
cout << endl;
}
错误的原因在于,系统不支持自定义类型的流插入,这里有两种解放方案:
1.重新重载一个 << 运算符
2.改变输出的格式 cout << (*it).a << (*it).b << " ";
cpp
while (it != la.end())
{
cout << (*it).a << (*it).b << " ";
it++;
}
但是究其本源,迭代器模拟的还是指针,如果是指针的话,就可以通过 -> 读取指针指向空间的数据,所以这里可以重载 -> 来达到模拟指针目的。
cpp
T* operator->()
{
return &_node->_data;//->的优先级更高
}
3.5 const_iterator
为了保证迭代器的内容不被修改,我们还要为迭代器添加 const ,但是根据所学知识,仅在函数返回值前加 const 是不能满足需求的,这只能保证迭代器本身不能被修改,此时就不可能自增和自减了,所以要新建一个 const_iterator 类,来模拟实现 const T* 指针的行为。
其中只有 operator* 与 operator-> 返回值不同。
cpp
template<class T>
struct ListConstIterator
{
typedef ListNode<T> Node;
typedef ListConstIterator<T> const_iterator;
Node* _node;
ListConstIterator(Node* node)
:_node(node)
{}
// *it
const T& operator*()
{
return _node->_data;
}
// it->
const T* operator->()
{
return &_node->_data;
}
// ++it
const_iterator& operator++()
{
_node = _node->_next;
return *this;
}
const_iterator operator++(int)
{
const_iterator tmp(*this);
_node = _node->_next;
return tmp;
}
const_iterator& operator--()
{
_node = _node->_prev;
return *this;
}
const_iterator operator--(int)
{
const_iterator tmp(*this);
_node = _node->_prev;
return tmp;
}
bool operator!=(const Self& it)
{
return _node != it._node;
}
bool operator==(const Self& it)
{
return _node == it._node;
}
};
此时在 my_list 类中在添加 cbegin 和 cend 函数:
cpp
iterator begin() const
{
return iterator(_head->_next);
}
iterator end() const
{
return iterator(_head);
}
四、测试代码
有了之前的代码,我们就可以进行统一的测试:
cpp
void test_push_back()
{
my_list<int> l1;
l1.push_back(1);
l1.push_back(2);
l1.push_back(3);
l1.push_back(4);
l1.push_back(5);
my_list<int>::iterator it = l1.begin();
while (it != l1.end())
{
cout << *it << endl;
it++;
}
}
五、修饰符成员
上面的代码就可以让我们基本理清 list 的行为,有助于写下面的函数。
5.1 insert 函数
库中的 insert 支持的是迭代器访问,此时 pos 是上面的迭代器结构体, pos._node 才是此时要访问的节点。pos 位置前的节点的下一个节点是新插入的节点,pos位置是新插入节点后面的节点。
cpp
void insert(iterator pos, T data)
{
Node* NewNode = new Node(data);
Node* prev = pos._node->_prev;
Node* cur = pos._node;
//节点位置: prev Newnode cur
prev->_next = NewNode;
NewNode->_prev = prev;
NewNode->_next = cur;
cur->_prev = NewNode;
}
5.2 erase 函数
erase 函数更加简单,其不需要创建新节点,仅需控制指针的指向即可。
cpp
void erase(iterator pos)
{
Node* prev = pos._node->_prev;
Node* next = pos._node->_next;
//prev pos next
prev->_next = next;
next->_prev = prev;
delete pos._node;
}
5.3 push 函数
有了 insert 函数, push_back 函数就可以直接复用,因为是尾插,且传入的 pos 位置是插入位置的下一个节点, end 函数指向的是哨兵位节点,所以 pos 直接取到 end 即可。
cpp
void push_back2(T data)
{
insert(end(), data);
}
同 push_back 使用 end 作为 pos 一样, begin 返回的是哨兵位的下一个节点,所以传入位置直接取到 begin 即可。
cpp
void push_front(T data)
{
insert(begin(), data);
}
5.4 pop 函数
因为没有重载运算符 - ,所以 pop_back 传值时只能使用自减操作, end 指向的是哨兵位,所以需要使用 end 前面的位置,标准库中并没有实现减操作的重载,且效率很低,所以没有必要手搓。
cpp
void pop_back()
{
erase(--end());
}
void pop_front()
{
erase(begin());
}
六、容量成员
6.1 size 函数
`std::list`的`size()`成员函数返回容器中元素的数目。在C++11及之后的标准中,`std::list::size()`的时间复杂度为常数时间O(1)。这是通过内部维护一个表示元素数量的计数器来实现的。
每当添加或删除元素时,这个计数器就会相应地更新。
因此,`size()`函数的实现通常看起来像这样:
cpp
size_t size() const
{
return _size; // _size是一个成员变量,用来追踪当前链表中元素的数量
}
这里的`_size`是容器的一个私有成员变量,其在构造函数中初始化,在向容器添加元素或从容器中移除元素时被更新。
但是要注意,在使用复用函数时,如上的 "push_front" "pop_back" 时, _size 不需要进行自增或自减操作,这样可能会自增自减两次,导致错误。
6.2 empty 函数
cpp
bool empty()
{
return _size == 0;
}
七、模板优化iterator
上面的 iterator 与 const_iterator 重复度太高,此时,就可以使用模板进行优化,让编译器根据传入的参数进行实例化:
cpp
template<class T, class Ref, class Ptr>
struct ListIterator
{
typedef ListNode<T> Node;
typedef ListIterator<T, Ref, Ptr> iterator;
Node* _node;
};
与之前学的模板不同的是,传入的参数增加了,传入的参数变成了引用和指针:
cpp
template<class T>
class my_list
{
typedef ListNode<T> Node;
public:
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;
}