【C++】STL容器--list的模拟实现
目录
- 前言
- 一、基本框架
- 二、list模拟实现
- 1. 构造函数
- 2. push_back
- 3. iterator
- 4. const_iterator
- 5. insert
- 6. erase
- 7. push_front
- 8. pop_back/pop_front
- 9. clear
- 10. 析构函数
- 11. size/empty
- 12. 拷贝构造函数
- 13. swap
- 14. operator=
- 三、源码
前言
前面介绍了【C++】STL容器–list的使用详情请点击,今天继续深入了解list的模拟实现
一、基本框架
我们使用模板来实现list,因此声明和定义不能分离,我们创建两个文件:test.cpp和list.h
- test.cpp:包含main函数,用于测试list的相关实现
- list.h:list实现的声明和定义
- list是类模板,所以我们采用类模板形式模拟实现list,list的数据不是连续的,节点是独立的,这和vector是不同的,需要注意
- list是由一个一个节点组成的,并且需要频繁访问前后数据,所以这里我们将节点定义成一个strcut结构体,并且初始化节点内容即可,这里的缺省值我们采用匿名对象的形式,如果用户没有显示传入参数,那么编译器默认采用调用对应类型的默认构造函数去构造出T类型的匿名对象去进行初始化,这里的T不仅可以是自定义类型同样也可以是内置类型,因为c++对内置类型进行了全面升级,使内置类型也可以调用默认构造函数进行构造出对象
- 链表需要一个变量记录当前链表的有效数据个数,因此list定义一个变量_size用来记录当前链表的有效数据个数;由于list和vector不同,list是单独new出一个节点再插入,因此不需要_capacity来记录容量
- list是带头双向循环链表,这里我们需要有一个头节点,我们还需要定义一个头节点的指针_head作为list对象的成员变量用于找到头节点
namespace gy
{template <class T>struct list_node{list_node<T>* prev;list_node<T>* next;T _data;list_node(const T& x = T()):prev(nullptr),next(nullptr),data(x){ } };template <class T>class list{typedef list_node<T> Node;public:private:Node* _head;size_t _size;};
}
二、list模拟实现
1. 构造函数
- 对list进行初始化,当list中没有数据时,只有头节点,让头节点的的两个用于指向下一个节点指针和指向前一个节点的指针指向它自己即可
- 当没有数据的空初始化不仅是构造函数需要使用,同时在拷贝构造的时候仍然需要进行使用,所以这里将这里的步骤封装成一个函数便于调用
void empty_init()
{_head = new node;_head->next = _head;_head->prev = _head;_size = 0;
}
list()
{empty_init();
}
2. push_back
- 在list结构中尾插数据,根据带头双向循环链表的特点,头节点的前节点指针指向的是最后一个节点
void push_back(const T& x)
{Node* newNode = new Node(x);Node* tail = _head->prev; // _head->prev 指向的节点就是尾节点tail->next = newNode;newNode->prev = tail;_head->prev = newNode;newNode->next = _head;++_size;
}
3. iterator
- 在string和vector中,我们实现iterator使用的是指针,由于其结构特点,指针直接++就可以指向下一个元素,解引用这个空间就可以直接拿到里面的数据
- 但是list的结构是一个一个的节点,指针指向这个节点(不仅仅只有数据),解引用无法直接得到数据,且节点之间不是连续的,无法++指向下一个节点
- 那么我们怎么样去获得节点数据,以及++就能指向下一个节点呢?我们可以重载运算符 * 和++,使用常规指针的原生行为 * 和++无法拿到我们想要的,同时迭代器的本质又是模拟指针的行为,所以那么我们可以考虑使用一个struct类封装一个节点的指针,通过运算符重载*和++,进而达到我们的目的,同时由于我们需要频繁访问类的所有成员,所以我们这里使用struct而不是class
template<class T>
struct list_iterator
{typedef list_node<T> Node;Node* _node;list_iterator(Node* node)//使用节点指针构造一个迭代器:_node(node){ }T& operator*(){return _node->_data;}//前置++list_iterator<T>& operator++(){_node = _node->next;return *this;}//后置++list_iterator<T>& operator++(int){list_iterator<T> tmp(*this);_node = _node->next;return tmp;}bool operator!=(const list_iterator<T>& it){return _node != it._node;}bool operator==(const list_iterator<T>& it){return _node == it._node;}
};
- 接下来我们编写list中的迭代器对应的begin和end函数让其返回对应的头尾节点的指针即可,由于头节点中不存放数据,头节点中指向的下一个节点才存储数据,所以迭代器对应的begin函数返回对头应该是头节点指向的下一个节点的指针,迭代器对应end函数对应的是存储的最后一个有效数据的节点的下一个节点位置,那么就为尾节点,这里我们返回头结点的指针即可
iterator begin()
{return _head->next; //隐式类型转换,节点node类型-》iterator类型
}iterator end()
{return _head;
}
我们使用下面代码来进行尾插,并使用迭代器遍历list并打印出数据
void test_list1()
{gy::list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);lt.push_back(5);lt.push_back(6);gy::list<int>::iterator it = lt.begin();while (it != lt.end()){cout << *it << " ";++it;}cout << endl;
}
结果如下:
- 如果list存放的数据不是内置类型(int、char等),而是自定义类型,如何使用迭代器访问数据?
比如list存储的数据是我们自定义的struct类型(A)
struct A
{A(int a1, int a2):_a1(a1),_a2(a2){ }A(const A& a):_a1(a._a1),_a2(a._a2){ }int _a1;int _a2;
};
- 使用(*it )再去访问:
- 运算符重载->:list迭代器模拟的是原生指针行为,当list存储的是int类型数据时,it类似于int * 类型,所以访问int * 数据,直接 * it得到数据,A类型的 it 迭代器的类型是A *,所以访问A类型数据使用->
template<class T>
struct list_iterator
{typedef list_node<T> Node;Node* _node;list_iterator(Node* node)//使用节点指针构造一个迭代器:_node(node){ }T& operator*(){return _node->_data;}T* operator->(){return &_node->_data;}};
4. const_iterator
- const 迭代器本身是可以修改的,指向内容不能被修改
- 实现const迭代器我们可以向普通迭代器一样创建
struct list_const_iterator
来实现const迭代器 - 同时完善迭代器重载操作符:前置++/后置++、前置–/后置–、!=、==
template<class T>
struct list_const_iterator
{typedef list_const_iterator<T> Node;Node* _node;list_const_iterator(Node* node):_node(node){}const T& operator*(){return _node->_data;}const T* operator->(){return &_node->_data;}//前置++list_const_iterator<T>& operator++(){_node = _node->next;return *this;}//后置++list_const_iterator<T>& operator++(int){list_const_iterator<T> 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 list_const_iterator<T>& it){return _node != it._node;}bool operator==(const list_const_iterator<T>& it){return _node == it._node;}
};typedef list_const_iterator<T> const_iterator;
const_iterator begin() const
{return _head->next;
}const_iterator end() const
{return _head;
}
struct list_iterator
和struct list_const_iterator
实现逻辑是一样的,仅仅只是返回值不同,怎么将这两个struct合并呢?这个时候就可以考虑使用模板,传不同的参数,编译器生成不同的迭代器
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& it){return _node != it._node;}bool operator==(const self& it){return _node == it._node;}
};typedef list_iterator<T, T&, T*> iterator;
typedef list_iterator <T, const T&, const T*> const_iterator;
iterator begin()
{return _head->next; //隐式类型转换,节点node类型-》iterator类型
}iterator end()
{return _head;
}const_iterator begin() const
{return _head->next;
}const_iterator end() const
{return _head;
}
5. insert
- 在迭代器pos位置之前插入元素x,记录pos节点和pos前一个节点
- new一个节点存储x数据,作为将要插入的新节点
- 插入一个新的节点,_size++
void insert(iterator pos, const T& x)
{Node* cur = pos._node;Node* prev = cur->prev;Node* newnode = new Node(x);prev->next = newnode;newnode->prev = prev;newnode->next = cur;cur->prev = newnode;++_size;
}
- 有linsert之后,前面push_back可以直接复用insert,push_back即在尾部插入一个数据
void push_back(const T& x)
{//Node* newNode = new Node(x);//Node* tail = _head->prev; // _head->prev 指向的节点就是尾节点//tail->next = newNode;//newNode->prev = tail;//_head->prev = newNode;//newNode->next = _head;insert(end(), x);
}
6. erase
- erase:删除某个迭代器位置的节点,删除该节点之后,迭代器就失效了,为了方便继续后续erase操作,返回其next位置迭代器
iterator erase(iterator pos)
{assert(pos != end())Node* cur = pos._node;Node* next = cur->next;Node* prev = cur->prev;prev->next = next;next->prev = prev;delete cur;cur = nullptr;--_size;return next;
}
7. push_front
复用insert,在begin迭代器位置插入元素x
void push_front(const T& x)
{insert(begin(), x);
}
8. pop_back/pop_front
- pop_back/pop_front:直接复用erase即可
- pop_back:尾删,即删除尾部数据,erase传入迭代器–end即可
- pop_front:头删,即删除第一个节点数据,erase传入begin()
void pop_front()
{erase(begin());
}void pop_back()
{erase(--end());
}
void Print(gy::list<int>& lt)
{for (auto& e : lt){cout << e << " ";}cout << endl;
}
运行结果如下,完成了尾插头插、尾删头删
9. clear
- clear函数,清除链表全部节点,因此我们可以使用迭代器遍历整个链表删除
void clear()
{iterator it = begin();while (it != end()){it = erase(it);}
}
10. 析构函数
析构函数:在清除所有数据节点后,还要delete头节点,将头节点置空
~list()
{clear();delete _head;_head = nullptr;
}
11. size/empty
- size函数:返回list链表当前有效数据个数
- empty:判断链表是否为空,即
_size == 0
则链表为空 /begin() == end()
size_t size()const
{return _size;
}
bool empty()
{return _size == 0;
}
12. 拷贝构造函数
- lt2拷贝构造lt2时,lt2需要先进行初始化
- lt2初始化后,再遍历lt1,将lt1的数据push_back给lt2
//lt2(lt1)
list(const list<T>& lt)
{empty_init();for (auto& e : lt){push_back(e);}
}
13. swap
- 传参使用引用传参,需要交换两个链表的数据
void swap(list<T>& lt)
{std::swap(_head, lt._head);std::swap(_size, lt._size);
}
14. operator=
- operator=:使用传值传参(形参lt是拷贝的lt1的数据),再使用swap交换this和lt,实现赋值操作
list<T>& operator=(list<T> lt) // 传值传参
{swap(lt);return *this;
}
三、源码
源代码点击查看