拷贝构造函数
拷贝构造函数就是根据所给list容器,拷贝构造出一个对象。对于拷贝构造函数,我们先申请一个头结点,并让其前驱指针和后继指针都指向自己,然后将所给容器当中的数据,通过遍历的方式一个个尾插到新构造的容器后面即可。
//拷贝构造函数 list(const list<T>& lt) { _head = new node; //申请一个头结点 _head->_next = _head; //头结点的后继指针指向自己 _head->_prev = _head; //头结点的前驱指针指向自己 for (const auto& e : lt) { push_back(e); //将容器lt当中的数据一个个尾插到新构造的容器后面 } }
赋值运算符重载函数
对于赋值运算符的重载,这里提供两种写法:
写法一:现代写法
这是比较容易理解的一种写法,先调用clear函数将原容器清空,然后将容器lt当中的数据,通过遍历的方式一个个尾插到清空后的容器当中即可。
//传统写法 list<T>& operator=(const list<T>& lt) { if (this != <) //避免自己给自己赋值 { clear(); //清空容器 for (const auto& e : lt) { push_back(e); //将容器lt当中的数据一个个尾插到链表后面 } } return *this; //支持连续赋值 }
写法二:现代写法
现代写法的代码量较少,首先利用编译器机制,故意不使用引用接收参数,通过编译器自动调用list的拷贝构造函数构造出来一个list对象,然后调用swap函数将原容器与该list对象进行交换即可。
//现代写法 list<T>& operator=(list<T> lt) //编译器接收右值的时候自动调用其拷贝构造函数 { swap(lt); //交换这两个对象 return *this; //支持连续赋值 }
这样做相当于将应该用clear清理的数据,通过交换函数交给了容器lt,而当该赋值运算符重载函数调用结束时,容器lt会自动销毁,并调用其析构函数进行清理。
析构函数
对对象进行析构时,首先调用clear函数清理容器当中的数据,然后将头结点释放,最后将头指针置空即可。
//析构函数 ~list() { clear(); //清理容器 delete _head; //释放头结点 _head = nullptr; //头指针置空 }
迭代器相关函数
begin和end
首先我们应该明确的是:begin函数返回的是第一个有效数据的迭代器,end函数返回的是最后一个有效数据的下一个位置的迭代器。
对于list这个带头双向循环链表来说,其第一个有效数据的迭代器就是使用头结点后一个结点的地址构造出来的迭代器,而其最后一个有效数据的下一个位置的迭代器就是使用头结点的地址构造出来的迭代器。(最后一个结点的下一个结点就是头结点)
iterator begin() { //返回使用头结点后一个结点的地址构造出来的普通迭代器 return iterator(_head->_next); } iterator end() { //返回使用头结点的地址构造出来的普通迭代器 return iterator(_head); }
当然,还需要重载一对用于const对象的begin函数和end函数。
const_iterator begin() const { //返回使用头结点后一个结点的地址构造出来的const迭代器 return const_iterator(_head->_next); } const_iterator end() const { //返回使用头结点的地址构造出来的普通const迭代器 return const_iterator(_head); }
访问容器相关函数
front和back
front和back函数分别用于获取第一个有效数据和最后一个有效数据,因此,实现front和back函数时,直接返回第一个有效数据和最后一个有效数据的引用即可。
T& front() { return *begin(); //返回第一个有效数据的引用 } T& back() { return *(--end()); //返回最后一个有效数据的引用 }
当然,这也需要重载一对用于const对象的front函数和back函数,因为const对象调用front和back函数后所得到的数据不能被修改。
const T& front() const { return *begin(); //返回第一个有效数据的const引用 } T& back() { return *(--end()); //返回最后一个有效数据的引用 } const T& back() const { return *(--end()); //返回最后一个有效数据的const引用 }
插入,删除函数
insert
insert函数可以在所给迭代器之前插入一个新结点。
先根据所给迭代器得到该位置处的结点指针cur,然后通过cur指针找到前一个位置的结点指针prev,接着根据所给数据x构造一个待插入结点,之后再建立新结点与cur之间的双向关系,最后建立新结点与prev之间的双向关系即可。
//插入函数 void insert(iterator pos, const T& x) { assert(pos._pnode); //检测pos的合法性 node* cur = pos._pnode; //迭代器pos处的结点指针 node* prev = cur->_prev; //迭代器pos前一个位置的结点指针 node* newnode = new node(x); //根据所给数据x构造一个待插入结点 //建立newnode与cur之间的双向关系 newnode->_next = cur; cur->_prev = newnode; //建立newnode与prev之间的双向关系 newnode->_prev = prev; prev->_next = newnode; }
erase
erase函数可以删除所给迭代器位置的结点。
先根据所给迭代器得到该位置处的结点指针cur,然后通过cur指针找到前一个位置的结点指针prev,以及后一个位置的结点指针next,紧接着释放cur结点,最后建立prev和next之间的双向关系即可
//删除函数 iterator erase(iterator pos) { assert(pos._pnode); //检测pos的合法性 assert(pos != end()); //删除的结点不能是头结点 node* cur = pos._pnode; //迭代器pos处的结点指针 node* prev = cur->_prev; //迭代器pos前一个位置的结点指针 node* next = cur->_next; //迭代器pos后一个位置的结点指针 delete cur; //释放cur结点 //建立prev与next之间的双向关系 prev->_next = next; next->_prev = prev; return iterator(next); //返回所给迭代器pos的下一个迭代器 }
push_back和pop_back
push_back和pop_back函数分别用于list的尾插和尾删,在已经实现了insert和erase函数的情况下,我们可以通过复用函数来实现
push_back和pop_back函数。
push_back函数就是在头结点前插入结点,而pop_back就是删除头结点的前一个结点。
//尾插 void push_back(const T& x) { insert(end(), x); //在头结点前插入结点 } //尾删 void pop_back() { erase(--end()); //删除头结点的前一个结点 }
push_front和pop_front
当然,用于头插和头删的push_front和pop_front函数也可以复用insert和erase函数来实现。
push_front函数就是在第一个有效结点前插入结点,而pop_front就是删除第一个有效结点。
//头插 void push_front(const T& x) { insert(begin(), x); //在第一个有效结点前插入结点 } //头删 void pop_front() { erase(begin()); //删除第一个有效结点 }
其他函数
size
size函数用于获取当前容器当中的有效数据个数,因为list是链表,所以只能通过遍历的方式逐个统计有效数据的个数。
size_t size() const { size_t sz = 0; //统计有效数据个数 const_iterator it = begin(); //获取第一个有效数据的迭代器 while (it != end()) //通过遍历统计有效数据个数 { sz++; it++; } return sz; //返回有效数据个数 }
resize
resize函数的规则:
1.若当前容器的size小于所给n,则尾插结点,直到size等于n为止
2.若当前容器的size大于所给n,则只保留前n个有效数据。
实现resize函数时,不要直接调用size函数获取当前容器的有效数据个数,因为当你调用size函数后就已经遍历了一次容器了,而如果结果是size大于n,那么还需要遍历容器,找到第n个有效结点并释放之后的结点。
这里实现resize的方法是,设置一个变量len,用于记录当前所遍历的数据个数,然后开始变量容器,在遍历过程中:
1.当len大于或是等于n时遍历结束,此时说明该结点后的结点都应该被释放,将之后的结点释放即可
2.当容器遍历完毕时遍历结束,此时说明容器当中的有效数据个数小于n,则需要尾插结点,直到容器当中的有效数据个数为n时停止尾插即可。
void resize(size_t n, const T& val = T()) { iterator i = begin(); //获取第一个有效数据的迭代器 size_t len = 0; //记录当前所遍历的数据个数 while (len < n&&i != end()) { len++; i++; } if (len == n) //说明容器当中的有效数据个数大于或是等于n { while (i != end()) //只保留前n个有效数据 { i = erase(i); //每次删除后接收下一个数据的迭代器 } } else //说明容器当中的有效数据个数小于n { while (len < n) //尾插数据为val的结点,直到容器当中的有效数据个数为n { push_back(val); len++; } } }
clear
clear函数用于清空容器,我们通过遍历的方式,逐个删除结点,只保留头结点即可。
void clear() { iterator it = begin(); while (it != end()) //逐个删除结点,只保留头结点 { it = erase(it); } }
empty
empty函数用于判断容器是否为空,我们直接判断该容器的begin函数和end函数所返回的迭代器,是否是同一个位置的迭代器即可。(此时说明容器当中只有一个头结点)
bool empty() const { return begin() == end(); //判断是否只有头结点 }
swap
swap函数用于交换两个容器,list容器当中存储的实际上就只有链表的头指针,我们将这两个容器当中的头指针交换即可。
void swap(list<T>& lt) { ::swap(_head, lt._head); //交换两个容器当中的头指针即可 }
注意: 在此处调用库当中的swap函数需要在swap之前加上“::”(作用域限定符),告诉编译器这里优先在全局范围寻找swap函数,否则编译器会认为你调用的就是你正在实现的swap函数(就近原则)。