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