【C++初阶】--- list容器功能模拟实现
1.什么是list容器
在 C++ 标准模板库(STL)中,std::list 是一个非常有用的容器,它是双向链表的实现std::list 是一种序列式容器,它允许在序列内的任意位置进行高效的插入和删除操作。与数组和 std::vector 不同,std::list 中的元素在内存中并不是连续存储的,而是通过指针相互连接,形成一个双向链表。
2.需要创建的三个必要类模板
// 1.节点模板
template<class T>
struct list_node
{//成员变量T _data;list_node<T>* _next;list_node<T>* _prev;
};// 2.迭代器模板
template<class T, class Ref, class Ptr>
struct list_iterator
{typedef list_node<T> Node;typedef list_iterator<T, Ref ,Ptr> Self;//成员变量Node* _node;
};// 3.链表模板
template<class T>
class list
{typedef list_node<T> Node;
public:typedef list_iterator<T, T&, T*> iterator;typedef list_iterator<T, const T&, const T*> const_iterator;private://成员变量包括哨兵位和链表长度Node* _head;size_t _size;
};
3.节点类模板
list容器底层是一个双向带头循环链表,所以节点类模板中有三个成员变量:data用于存储数据,next是指向下一个节点的指针,prev是指向前一个节点的指针。
节点类模板我们只需要实现一个默认构造用于构造出一个哨兵位即可,后续插入删除等操作在链表类模板中实现
template<class T>
struct list_node
{//成员变量T _data;list_node<T>* _next;list_node<T>* _prev;
};
3.1默认构造
//默认构造
list_node(const T& data = T())//T()匿名对象,内置类型一样有:_data(data), _next(nullptr), _prev(nullptr)
{}
4.迭代器类模板
我们模拟实现的迭代器底层是一个指向节点指针,所以有一个成员变量Node* _node
template<class T, class Ref, class Ptr>
struct list_iterator
{typedef list_node<T> Node;typedef list_iterator<T, Ref ,Ptr> Self;//成员变量Node* _node;
};
我们参考一下以下代码,我们要实现迭代器模板的原因是因为我们要对迭代器变量it进行++、–、*(解引用)、==、!=等操作,我们对it进行++操作的时候我们希望的是it能够指向下一个节点,对it进行解引用操作时我们希望能够取到节点中的数据,但因为链表中的数据不是连续存储的,所以这些操作符达到的实际效果与我们预期效果不符,所以我们要对这些运算符进行重载,使其能够达到我们实际的预期效果。
auto it = con.begin();
while (it != con.end())
{cout << *it << " ";it++;
}
cout << endl;
4.1operator*
这里的Ref我们之后传T&,即返回引用
Ref operator*()
{return _node->_data;
}
4.2operator->
Ptr后续传的是T*
//返回的是指针
Ptr operator->()
{return &(_node->_data);
}
重载这个运算符的原因是我们可以进行以下操作:
struct AA
{int _a1;int _a2;
};
list<AA> lta;
lta.push_back(AA());
lta.push_back(AA());
lta.push_back(AA());
lta.push_back(AA());list<AA>::iterator ita = lta.begin();
while (ita != lta.end())
{//特殊处理,本来应该是两个->才合理,为了可读性,省略了一个->//cout << ita.operator->()->_a1 << ":" << ita.operator->()->_a2 << endl;cout << ita->_a1 << ":" << ita->_a2 << endl;ita++;
}
4.3前置++、–
//前置
Self& operator++()
{_node = _node->_next;return *this;//即返回节点
}Self& operator--()
{_node = _node->_prev;return *this;
}
4.4后置++、–
后置++、–返回的是临时变量,但实际值会改变
//后置
Self& operator++(int)
{Node* tmp(_node);_node = _node->_next;return *this;
}Self& operator--(int)
{Node* tmp(_node);_node = _node->_prev;return *this;
}
4.5operator==/operator!=
是否相同比较的是地址
bool operator!=(const Self& s) const
{return _node != s._node;
}bool operator==(const Self& s) const
{return _node == s._node;
}
5.链表类模板
链表类模板有两个成员变量:_head是指向链表哨兵位的指针,_size用于记录链表的长度。
// 3.链表模板
template<class T>
class list
{typedef list_node<T> Node;
public:typedef list_iterator<T, T&, T*> iterator;typedef list_iterator<T, const T&, const T*> const_iterator;private://成员变量包括哨兵位和链表长度Node* _head;size_t _size;
};
5.1迭代器
begin()返回的是指向链表哨兵位后一个节点的迭代器,end()返回的是指向哨兵位的迭代器。
iterator begin()
{//有名对象/*iterator it(_head->_next);return it;*///匿名对象//return iterator(_head->_next);//单参数构造函数支持隐式类型转换return _head->_next;
}iterator end()
{return _head;
}
5.2const迭代器
const_iterator begin() const
{return _head->_next;
}const_iterator end() const
{return _head;
}
const迭代器给const对象使用,如以下模板可以使用
//打印数据通用模板
template<class Container>
void print_Container(const Container& con)
{//去未实例化的类里面取东西,不知道是类型还是变量,加上typename说明是类型//typename Container::const_iterator it = con.begin();auto it = con.begin();while (it != con.end()){//*it += 10;cout << *it << " ";it++;}cout << endl;
}
5.3空初始化empty_init()
空初始化即初始化一个空链表,空链表只包含一个哨兵位。
//空初始化
void empty_init()
{_head = new Node;_head->_next = _head;_head->_prev = _head;_size = 0;
}
5.4在pos位置插入节点insert()
在pos尾插插入数据需要传pos位置的迭代器,然后我们构造一个值位x的节点,改变相关节点前后指针的指向实现pos位置的插入
//在pos位置之前插入数据
void insert(iterator pos, const T& x)
{//申请新节点Node* newnode = new Node(x);Node* cur = pos._node;Node* prev = cur->_prev;//cur->prev、newnode、curnewnode->_next = cur;newnode->_prev = prev;cur->_prev = newnode;prev->_next = newnode;++_size;
}
5.5删除pos维持节点erase()
删除pos位置节点需要传pos位置得迭代器,注意不能删除哨兵位,然后我们改变相关节点前后指针的指向实现删除。
//删除pos位置数据
iterator erase(iterator pos)
{//不能删除哨兵位assert(pos != end());//pos->prev、pos->nextNode* next = pos._node->_next;Node* prev = pos._node->_prev;prev->_next = next;next->_prev = prev;delete pos._node;--_size;return next;
}
5.6尾插push_back()
往链表中尾插一个节点我们可以选择先构造一个新节点,然后改变相关节点前后指针的指向实现尾插,也可以选择复用insert传尾节点的迭代器实现尾插。
//尾插
//方法1
void push_back(const T& x)
{//申请新节点,用x构造一个节点Node* newnode = new Node(data);Node* tail = _head->_prev;//tail、newnode、tail->next(_head)tail->_next = newnode;newnode->_prev = tail;newnode->_next = _head;_head->_prev = newnode;++_size;
}//方法2
void push_back(const T& x)
{insert(end(), x);
}
5.7头插push_front()
头插是插入至哨兵位后面。头插可以选择直接复用insert传头节点的迭代器实现,也可以想上面的尾插一样改变相关节点前后指针的指向实现头插
//头插
void push_front(const T& x)
{//_head、newnode、_head->nextinsert(begin(), x);
}
5.8尾删pop_back()
尾删我们直接复用erase,传尾节点的迭代器实现删除
//尾删
void pop_back()
{erase(--end());
}
5.9头删pop_front()
头删我们直接复用erase,传头节点的迭代器实现删除
//头删
void pop_front()
{erase(begin());
}
5.10size()和empty()
size()用于返回链表的长度,empty()用于判断链表是否为空
size_t size() const
{return _size;
}bool empty() const
{return _size == 0;
}
5.11构造函数
5.11.1默认构造
默认构造即构造一个哨兵位,我们可以复用空初始化。
//默认构造(哨兵位)
list()
{empty_init();
}
5.11.2拷贝构造
拷贝构造我们可以复用空初始化先给个哨兵位,然后使用范围for将节点一个一个尾插至链表中
//拷贝构造
list(const list<T>& lt)
{//先给个哨兵位empty_init();for (auto e : lt){push_back(e);}
}
6.打印数据的通用模板
通用模板我们传的是const对象,会调用const迭代器。
这里有一点需要注意,就是模板有个特性:按需实例化,即如果我们没有将这个模板实例化的话,如果这个模板中没有明显的语义错误,编译器是不会报错的,比如下面的打印模板,我们打印的是const对象,我们对const对象只有读的权限,我们不能对其进行修改,比如*it += 10,正常这样是会报错的,但因为这是在模板中写的,也就是说只有当模板被实例化后,编译器才会报错。
//按需实例化
template<class Container>
void print_Container(const Container& con)
{//去未实例化的类里面取东西,不知道是类型还是变量,加上typename说明是类型//typename Container::const_iterator it = con.begin();auto it = con.begin();while (it != con.end()){//*it += 10;cout << *it << " ";it++;}cout << endl;
}