list类模拟实现¶
约 2497 个字 522 行代码 1 张图片 预计阅读时间 15 分钟
list类模拟实现¶
list类节点结构设计¶
因为list类为本质是带头双向循环链表,所以在设计list类时,需要先设计节点的结构,并且因为每一个节点是一个整体,所以可以考虑将每一个节点的初始化构造放在结构体中,如下代码所示:
| C++ | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
list类非const迭代器结构设计¶
因为list类是带头双向循环链表,所以迭代器不能单单使用普通指针来代替,因为普通指针可以指针的++/--等操作可以作用到连续空间,链表两个节点空间不一定连续,所以额外重载++/--等运算符
迭代器基本结构设计¶
| C++ | |
|---|---|
1 2 3 4 5 6 7 8 9 | |
迭代器构造函数¶
为了可以将普通指针看作迭代器,需要构造函数使用普通指针进行构造
| C++ | |
|---|---|
1 2 3 4 | |
考虑下面的代码
| C++ | |
|---|---|
1 2 3 4 5 6 7 8 9 | |
在上面的测试代码中,当使用迭代器遍历时,需要使用到3个运算符
!=:不等于运算符*:解引用运算符++:前置自增运算符
但是由于it不是原生指针(内置类型的指针),所以需要额外重载这三个运算符
operator++()函数¶
重载前置++运算符的本意是让迭代器可以从当前节点移动到下一个节点,所以只需要移动节点类型的指针指向下一个节点即可
| C++ | |
|---|---|
1 2 3 4 5 6 | |
operator*()函数¶
重载*运算符本意是为了获取当前有效数据节点数据域中的值,所以返回当前节点数据域的值即可
| C++ | |
|---|---|
1 2 3 4 5 | |
operator!=()函数¶
重载!=运算符本意是为了判断迭代器指向的当前节点是end()迭代器的位置(即是否已经遍历完链表)
| C++ | |
|---|---|
1 2 3 4 5 6 | |
operator++(int)函数¶
后置++运算符重载需要满足先使用再自增,所以需要提前记录当前节点再改变当前节点指向为下一个节点
| C++ | |
|---|---|
1 2 3 4 5 6 7 | |
operator--()函数¶
前置--运算符重载直接返回上一个节点的位置即可
| C++ | |
|---|---|
1 2 3 4 5 6 | |
operator--(int)函数¶
后置--运算符重载需要满足先使用再自增,所以需要提前记录当前节点再改变当前节点指向为下一个节点
| C++ | |
|---|---|
1 2 3 4 5 6 7 | |
operator==()函数¶
重载==运算符为了判断当前节点是否等于指定节点
| C++ | |
|---|---|
1 2 3 4 5 | |
operator->()函数¶
如果模板参数为自定义类型时,访问自定义类型的成员变量时除了可以解引用之后通过.成员变量的方式以外,还有->成员变量的方式,但是对于list类来说不存在原生指针,只有迭代器,所以迭代器需要重载->运算符,考虑下面的代码
| C++ | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
为了可以使用->运算符,需要重载->运算符
| C++ | |
|---|---|
1 2 3 4 5 | |
所以上面的代码可以转化为
| C++ | |
|---|---|
1 2 | |
list类const迭代器结构设计¶
设计const迭代器的思路和非const迭代器思路基本一致,但是设计const迭代器不可以单单是在已有的iterator前面加上const改为const iterator,这种写法会使编译器认为是T* const,此时实现的效果是,迭代器指向的内容可以修改,但是指向不可以修改,而需要的const迭代器实现的效果是迭代器指向的内容不可以修改,但是指向可以修改,即const T*,所以需要单独设计一个const_iterator类来解决这个问题
迭代器基本结构设计¶
| C++ | |
|---|---|
1 2 3 4 5 6 7 8 | |
迭代器构造函数¶
| C++ | |
|---|---|
1 2 3 4 5 | |
operator++()函数¶
因为是改变迭代器指针的指向,所以设计operator++()函数和非const版本的方式相同
| C++ | |
|---|---|
1 2 3 4 5 6 | |
operator*()函数¶
因为const版本迭代器不可以通过解引用修改指针指向的内容,所以需要使用const修饰返回值
| C++ | |
|---|---|
1 2 3 4 5 | |
operator!=()函数¶
对于!=运算符来说,和非const版本思路相同
| C++ | |
|---|---|
1 2 3 4 5 | |
operator++(int)函数¶
对于后置++来说与const版本同理
| C++ | |
|---|---|
1 2 3 4 5 6 7 | |
operator--()函数¶
因为--改变的是迭代器指向的内容,所以与非const版本迭代器思路相同
| C++ | |
|---|---|
1 2 3 4 5 | |
operator--(int)函数¶
设计后置--的思路与非const版本相同
| C++ | |
|---|---|
1 2 3 4 5 6 7 | |
operator==()函数¶
设计==运算符重载函数思路和非const版本相同
| C++ | |
|---|---|
1 2 3 4 5 | |
operator->()函数¶
需要满足返回值为const类型即可
| C++ | |
|---|---|
1 2 3 4 | |
const版本的迭代器使用问题¶
上面的const版本迭代器实现方式可以应用于对象为const类型时,例如下面的测试代码
| C++ | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
但是如果对象不是const类型,此时上面的代码就会出现错误,例如下面的测试代码
| C++ | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
在第二个测试代码中,因为cit为const类型的迭代器,所以不可以使用非const版本的迭代器,但是此时的begin()和end()均为非const版本迭代器,因为对象ls为非const对象
这里提出两种解决方案:
-
将
const版本的begin()和end()改为cbegin()和cend(),此时显式使ls调用const版本的cbegin()和cend()Note
缺点:没有使用到函数重载的优势
C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
//修改const版本的迭代器 //begin()函数——const版本 const_iterator cbegin() const { return _head->next; } //begin()函数——const版本 const_iterator cbegin() const { return _head->next; } //测试代码修改为 void test_const_iterator() { sim_list::list<int> ls; ls.push_back(1); ls.push_back(2); ls.push_back(3); ls.push_back(4); ls.push_back(5); sim_list::list<int>::const_iterator cit = ls.cbegin();// 显式指定const版本迭代器 while (cit != ls.cend()) { //*cit = 2; cout << *cit << " "; cit++; } cout << endl; } -
在
const版本的迭代器结构中添加非const对象向const对象转换的构造函数Note
缺点:没有调用
const版本的迭代器C++ 1 2 3 4 5
// 非const对象向const对象转换的构造函数 //传入非const对象,为了确保安全,使用const修饰形参 _list_iterator_const(const _list_iterator<T> nonConst) :_node(nonConst._node)//使用非const对象中的_node值构造const版本的对象中的_node {}
const版本迭代器和非const版本迭代器合并优化¶
在实现非const版本的迭代器时,可以很明显感觉到除了operator*()函数和operator->()函数两个有不同以外,其余均没有不同,而这两个版本中的这两个函数只是返回值类型不同,那么可以考虑通过模板单独控制这两个返回值类型,现在将类型引用T&用模板参数Ref指代,将类型指针T*用模板参数Ptr指代,则const版本迭代器和非const版本迭代器可以合并为下面的代码:
| C++ | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | |
Note
注意,此处也需要考虑到非const对象向const对象的转换问题,为了更方便解决,采取上面的第一种解决方案,如果直接是const对象调用则不需要考虑这个问题
list类无参构造函数¶
对于list类来说,因为只有一个头指针作为成员变量,所以无参构造时只需要考虑创建头结点即可
| C++ | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
list类析构函数¶
调用clear()函数后将头节点资源清理并置为空即可
| C++ | |
|---|---|
1 2 3 4 5 6 7 8 | |
list类拷贝构造函数¶
| C++ | |
|---|---|
1 2 3 4 5 6 7 8 9 10 | |
list类赋值运算符重载函数¶
| C++ | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
begin()函数¶
返回第一个有效数据节点的位置即可
| C++ | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 | |
end()函数¶
返回最后一个头节点的位置即可
| C++ | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 | |
insert()函数¶
插入节点思路参考带头双向循环链表
Note
可以考虑返回当前节点位置防止迭代器失效问题
| C++ | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
push_back()函数¶
设计思路参考带头双向循环链表的思路
| C++ | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
push_front()函数¶
设计思路参考带头双向循环链表的思路
| C++ | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
erase()函数¶
删除节点的思路类似于带头双向链表的删除思路
Note
注意返回删除节点位置的下一个节点的位置防止出现迭代器失效问题
| C++ | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
pop_back()函数¶
直接复用erase()函数即可,但是需要注意删除的位置不是end的位置,而是end前一个位置
| C++ | |
|---|---|
1 2 3 4 5 | |
pop_front()函数¶
直接复用erase()函数即可
| C++ | |
|---|---|
1 2 3 4 5 6 | |
clear()函数¶
调用erase()函数循环从头删除即可,但是不可以删除头结点
| C++ | |
|---|---|
1 2 3 4 5 6 7 8 9 | |
额外补充¶
打印函数
当链表中的类型为int时,打印函数中的list模板参数直接设置为int即可,例如下面的代码:
| C++ | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 | |
但是,上面的代码在模板参数处写定为int,如果更换类型,此时就需要在打印函数处同时更改类型。
假设现在的链表为如下:
| C++ | |
|---|---|
1 2 3 4 5 | |
此时必需更改打印函数中的类型为string类型,但是每一次更换类型就要改变函数步骤相对繁琐,如果一个函数中涉及到多个类型的list,此时就无法实现调用打印函数打印链表内容,此时可以考虑使用模板,将list的模板参数设置为模板参数,如下面的代码:
| C++ | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 | |
但是上面的代码没有通过编译,原因在于模板参数,当模板参数在函数模板或者类模板中,编译器开始编译时,可以实现替换,从而生成对应的函数或者类。但是在上面的代码中,const_iterator是一个被typedef的变量,但是编译器并不知道是重命名的变量,反之编译器可能会认为是静态变量,所以此时到底是const_iterator是静态变量还是重命名的变量编译器并不知道,编译器需要在类sim_list中确定const_iterator的类型,从而实现链接,最后再替换模板参数,因为在模板参数还未被替换时,编译器不能进类sim_list中寻找,因为此时类中可能存在未知的内容没有被处理,所以为了确保正常编译通过,此时不可以使用class T作为模板参数,而应该使用typename T,所以上面的代码修改为:
| C++ | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
另外还有一个问题,上面的打印代码仅仅实现的是list类的内容打印,但是如果此时需要为其他类打印,则需要另外再写一个打印,方式过于繁琐,所以可以考虑为各种类的内容打印写一个通用的函数,此时设计该函数时需要改变函数参数为各种容器,可以考虑使用函数模板,模板参数即为作为函数参数的容器,如下面的代码:
| C++ | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 | |
