• 【C++】List -- 详解


    一、list 的介绍及使用

    cplusplus.com/reference/list/list/?kw=list

    1. list 是可以在常数时间 O(1) 内在任意位置进行插入删除的序列式容器,并且该容器可以前后双向迭代
    2. list 的底层是带头双向循环链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。
    3. list forward_list 非常相似:最主要的不同在于 forward_list 是单链表,只能朝前迭代,不支持尾插、尾删,对比双向链表的唯一优势就是每个节点少存一个指针。但是forward_list 在实际中用的很少,单链表尾插和尾删效率太低,尤其是尾删,不实用。
    4. 与其他的序列式容器相比(array,vector,deque),list 在容器内任意位置进行插入、删除、移动元素方面的执行效率通常表现更好。
    5. 与其他序列式容器相比,list 和 forward_list 最大的缺陷不支持任意位置的随机访问,比如:要访问 list 的第 6 个元素,必须从已知的位置(比如头部 / 尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list 还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大 list 来说这可能是一个重要的因素)。


    1、list 的使用

    list 中的接口比较多,此处类似,只需要掌握如何正确的使用,然后再去深入研究背后的原理,已达到可扩展的能力。以下为 list 中一些常见的重要接口。

    (1)list 的构造

    1. // list的构造
    2. void TestL1()
    3. {
    4. list<int> l1; // 构造空的l1
    5. list<int> l2(4, 100); // l2中放4个值为100的元素
    6. list<int> l3(l2.begin(), l2.end()); // 用l2的[begin(), end())左闭右开的区间构造l3
    7. list<int> l4(l3); // 用l3拷贝构造l4
    8. // 以数组为迭代器区间构造l5
    9. int array[] = {16, 2, 77, 29};
    10. list<int> l5(array, array + sizeof(array) / sizeof(int));
    11. // 列表格式初始化C++11
    12. list<int> l6{1, 2, 3, 4, 5};
    13. }

    (2)迭代器的使用

    可以暂时将迭代器理解成一个指针,该指针指向 list 中的某个节点

    1. // list迭代器的使用
    2. void PrintList(const list<int>& l)
    3. {
    4. // 注意这里调用的是list的 begin() const,返回list的const_iterator对象
    5. for (list<int>::const_iterator it = l.begin(); it != l.end(); ++it)
    6. {
    7. cout << *it << " ";
    8. // *it = 10; // 不能改变该值 -- 否则编译不通过
    9. }
    10. cout << endl;
    11. }
    1. void TestL2()
    2. {
    3. int array[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
    4. list<int> l(array, array + sizeof(array) / sizeof(array[0]));
    5. // 使用正向迭代器正向list中的元素
    6. // list::iterator it = l.begin(); // C++98中语法
    7. auto it = l.begin(); // C++11之后推荐写法
    8. while (it != l.end())
    9. {
    10. cout << *it << " ";
    11. ++it;
    12. }
    13. cout << endl;
    14. // 使用反向迭代器逆向打印list中的元素
    15. // list::reverse_iterator rit = l.rbegin();
    16. auto rit = l.rbegin();
    17. while (rit != l.rend())
    18. {
    19. cout << *rit << " ";
    20. ++rit;
    21. }
    22. cout << endl;
    23. // 以数组为迭代器区间构造l5
    24. list<int> l(array, array + sizeof(array) / sizeof(int));
    25. // 用迭代器方式打印元素
    26. list<int>::iterator it = l.begin();
    27. while (it != l.end())
    28. {
    29. cout << *it << " ";
    30. ++it;
    31. }
    32. cout << endl;
    33. // C++11范围for的方式遍历
    34. for (auto& e : l)
    35. {
    36. cout << e << " ";
    37. }
    38. cout << endl;
    39. }

    遍历链表只能用迭代器范围for

    注意
    1. begin 与 end 为正向迭代器,对迭代器执行 ++ 操作,迭代器移动
    2. rbegin(end) 与 rend(begin) 为反向迭代器,对迭代器执行 ++ 操作,迭代器移动

    (3)容量操作

    cplusplus.com/reference/list/list/empty/

    cplusplus.com/reference/list/list/size/


    (4)访问操作

    list 不支持 [] 运算符,因为 list 容器物理地址不是连续的。

    cplusplus.com/reference/list/list/front/

    cplusplus.com/reference/list/list/back/


    (5)修改操作

    注意list 可以在任意位置进行 O(1) 插入删除操作。 

    1. // list插入和删除
    2. // push_back/pop_back/push_front/pop_front
    3. void TestL3()
    4. {
    5. int array[] = {1, 2, 3};
    6. list<int> L(array, array + sizeof(array) / sizeof(array[0]));
    7. L.push_back(4); // 在list的尾部插入4
    8. L.push_front(0); // 在list的头部插入0
    9. PrintList(L);
    10. L.pop_back(); // 删除list尾部节点
    11. L.pop_front(); // 删除list头部节点
    12. PrintList(L);
    13. }
    1. // insert /erase
    2. void TestL4()
    3. {
    4. int array1[] = {1, 2, 3};
    5. list<int> L(array1, array1 + sizeof(array1) / sizeof(array1[0]));
    6. auto pos = ++L.begin(); // 获取链表中第二个节点
    7. cout << *pos << endl;
    8. L.insert(pos, 4); // 在pos前插入值为4的元素
    9. PrintList(L);
    10. L.insert(pos, 5, 5); // 在pos前插入5个值为5的元素
    11. PrintList(L);
    12. vector<int> v{7, 8, 9};
    13. L.insert(pos, v.begin(), v.end()); // 在pos前插入[v.begin(), v.end)区间中的元素
    14. PrintList(L);
    15. L.erase(pos); // 删除pos位置上的元素
    16. PrintList(L);
    17. L.erase(L.begin(), L.end()); // 删除list中[begin, end)区间中的元素,即删除list中的所有元素
    18. PrintList(L);
    19. }
    为什么 C++98 建议使用各自容器里的 swap,而不建议使用算法里的 swap?

    可以看到算法里 swap 的 C++98 的实现,无论是 string、vector、list 使用它都会涉及深拷贝问题,而且这里的深拷贝代价极大,需要深拷贝 3 次 —— 当 l1 和 l2 交换,这里会把 l1 拷贝构造一份 c,然后把 l2 赋值于 l1,c 赋值于 l2,完成交换。

    而如果是容器里的 swap,需要交换 l1 和 l2,只需要头指针交换即可。假设是 vector,只要把 l1 和 l2 对应的 _start、_finish、_endofstorage 交换即可。相比算法里的 C++98 里的 swap,这里可以认为没有任何代价。

    1. // resize/swap/clear
    2. void TestL5()
    3. {
    4. // 用数组来构造list
    5. int array1[] = {1, 2, 3};
    6. list<int> l1(array1, array1 + sizeof(array1) / sizeof(array1[0]));
    7. PrintList(l1);
    8. list<int> l2;
    9. l1.swap(l2); // 交换l1和l2中的元素
    10. PrintList(l1);
    11. PrintList(l2);
    12. l2.clear(); // 将l2中的元素清空
    13. cout << l2.size() << endl;
    14. }

    🔺list 的迭代器失效 
    先将迭代器暂时理解成类似于指针, 迭代器失效即迭代器所指向的节点无效,即该节点被删除了
    因为 list 的底层结构为带头结点的双向循环链表,因此在 list 中进行 插入 时是 不会 导致 list 的迭代器失效的,只有在 删除 时才 失效,并且 失效的只是指向被删除节点的迭代器 ,其他迭代器不会受到影响

     

    1. void TestListIterator()
    2. {
    3. int array[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
    4. list<int> l(array, array+sizeof(array)/sizeof(array[0]));
    5. auto it = l.begin();
    6. while (it != l.end())
    7. {
    8. // erase()函数执行后,it所指向的节点已被删除,因此it无效,在下一次使用it时,必须先给其赋值
    9. l.erase(it);
    10. ++it;
    11. }
    12. }
    13. // 改正
    14. void TestListIterator()
    15. {
    16. int array[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
    17. list<int> l(array, array+sizeof(array)/sizeof(array[0]));
    18. auto it = l.begin();
    19. while (it != l.end())
    20. {
    21. l.erase(it++); // it = l.erase(it);
    22. }
    23. }


    【补充】
    a. 容器迭代器的分类

    容器迭代器的分类:

    1. 使用功能的角度可分为:正向 / 反向 + const。
    2. 容器底层结构的角度可分为:单向迭代器、双向迭代器、随机访问迭代器。

    单链表 forward_list、哈希表 unordered_set/map 迭代器就是单向(Unidirectional Iterator),特征是能 ++,不能 --;

    双向链表 list、红黑树 set/map 迭代器就是双向(Bidirectional Iterator),特征是能 ++ / --;

    string、vector、deque 迭代器就是随机迭代器(Random Access Iterator),特征是能可以++ / -- / + / -,一般随机迭代器底层都是一个连续的空间。

    迭代器的本质是:

    在不暴露容器底层实现细节的情况下,提供了一种统一的方式来访问 / 修改容器中存储的数据。 

    容器、迭代器、算法间的关系:

    算法不能直接访问和修改容器中存储的数据,而迭代器就是容器和算法之间的胶合剂。

    stl_iterator 源码中对于迭代器的分类: 


    b. 迭代器的实现方式分析

    迭代器有两种实现方式,具体应根据容器底层数据结构的实现来确定。

    因为每个特定的容器,都有特定的内部内存结构,也就意味着遍历 / 迭代过程的细节是不一样的。

    方式一:前提条件:原生指针指向的空间物理地址是连续的,比如 string、vector

    • 所以让原生指针 ++ 可以直接走到下一个位置,-- 可以直接回到上一个位置,* 解引用可以直接得到这个位置的数据。比如 vector:

    • 此时的原生指针就是一个天然的迭代器(但并不是原生指针厉害,而是因为它指向的空间物理地址是连续的)。

    方式二:原生指针指向的空间物理地址不连续,比如:list、map / set

    • 所以让原生指针 ++ 不能直接走到下一个位置,-- 不能直接回到上一个位置,* 解引用不能直接得到这个位置的数据,只能得到一个结构体(结构体中包含数据和指针域)。比如 list:

    • 此时原生指针不是一个天然的迭代器,所以我们需要用一个类封装原生指针,重载相关运算符,控制对象使用这些运算符的行为,让这个类的对象,用起来像指针一样

    • 为了让迭代器的使用形式与指针完全相同,因此在自定义的类中必须实现以下方法:

    1. 指针可以解引用,则迭代器的类中必须重载 operator*()。
    2. 指针可以通过 -> 访问其所指空间成员,则迭代器类中必须重载 oprator->()。
    3. 指针可以 ++ 向后移动,则迭代器类中必须重载 operator++() 与 operator++(int) ,至于 operator–() / operator–(int) 是否需要重载,要根据容器具体的底层结构来抉择,比如 list 双向链表可以向前移动,所以需要重载 operator--() 和 operator--(int),如果是 forward_list 就不需要重载 --。
    4. 迭代器需要进行是否相等的比较,因此还需要重载 operator==() 与 operator!=()。

    (6)容器操作(了解)
    a. sort

    cplusplus.com/reference/list/list/sort/

    • 等价元素的结果顺序是稳定的:即等价元素保留它们在调用之前的相对顺序。

    • 整个操作不涉及任何元素对象的构造、销毁或复制。元素在容器内移动。

    sort() 对 list 容器中的元素进行排序,效率很低链表排序也没有太大的作用和价值

    函数默认是排升序(<),如果想要排降序(>),需要传仿函数。

    1. void test()
    2. {
    3. int arr[] = { 10, 2, 5 };
    4. list<int> lt(arr, arr + sizeof(arr) / sizeof(int));
    5. // > 排降序
    6. // 写法一:
    7. greater<int> g; // 类模板,在头文件中
    8. lt.sort(g);
    9. // 写法二:
    10. lt.sort(greater<int>()); // 匿名对象
    11. // lt: 10 5 2
    12. }

    b. unique 

    cplusplus.com/reference/list/list/unique/ 

    去重,必须要先排序,才能去完重复值。

    1. void test()
    2. {
    3. int arr[] = { 2, 4, 3, 2, 4 };
    4. list<int> lt(arr, arr + sizeof(arr) / sizeof(int));
    5. lt.sort(); // 去重前,必须要先排序
    6. lt.unique(); // 去除重复值
    7. // lt: 2 3 4
    8. }

    c. remove 

    cplusplus.com/reference/list/list/remove/ 

     从容器中移除所有值等于 val 的元素。

    1. void test()
    2. {
    3. int arr[] = { 1, 2, 3, 3, 4 };
    4. list<int> lt(arr, arr + sizeof(arr) / sizeof(int));
    5. lt.remove(3); // lt: 1 2 4
    6. }

    d. reverse 

    cplusplus.com/reference/list/list/reverse/ 

    反转容器中元素的顺序。  

    1. void test()
    2. {
    3. int arr[] = { 1, 2, 3, 4, 5 };
    4. list<int> lt(arr, arr + sizeof(arr) / sizeof(int));
    5. lt.reverse(); // rlt: 5 4 3 2 1
    6. }

    e. merge 

    cplusplus.com/reference/list/list/merge/ 

    合并排序序列(合并前两个 list 容器都要排好序)。


    二、STL_list 源码剖析

    1、list 的底层结构

    SGI 版本 STL 中 list 部分源码:

    1. // 链表节点结构
    2. template <class T>
    3. struct __list_node {
    4. typedef void* void_pointer;
    5. void_pointer next;
    6. void_pointer prev;
    7. T data;
    8. };
    9. // 迭代器 -- 一个类封装了节点指针
    10. template<class T, class Ref, class Ptr>
    11. struct __list_iterator {
    12. typedef __list_iterator iterator;
    13. typedef __list_iteratorconst T&, const T*> const_iterator;
    14. typedef __list_iterator self;
    15. // ...
    16. typedef Ptr pointer;
    17. typedef Ref reference;
    18. // ...
    19. typedef __list_node* link_type;
    20. link_type node; // 节点指针
    21. // ...
    22. };
    23. // 带头双向循环链表结构
    24. template <class T, class Alloc = alloc>
    25. class list {
    26. protected:
    27. typedef __list_node list_node; // 节点
    28. // ...
    29. public:
    30. typedef T value_type;
    31. typedef list_node* link_type; // 节点指针
    32. // ...
    33. public:
    34. // 迭代器(这里的iterator和const_iterator是定义在list类域中的内嵌类型)
    35. typedef __list_iterator iterator; // 传了T,T&,T*模板参数
    36. typedef __list_iteratorconst T&, const T*> const_iterator; // 传了T,const T&,const T*模板参数
    37. // ...
    38. protected:
    39. link_type node; // 成员变量,节点指针
    40. // ...
    41. };

    三、list的模拟实现

    1、list 的节点结构

    双向循环链表节点结构:一个数据域,两个指针。

    1. namespace xyl
    2. {
    3. // List的节点类
    4. template<class T>
    5. struct list_node
    6. {
    7. T _data; // 数据域
    8. list_node* _prev; // 前驱指针
    9. list_node* _next; // 后继指针
    10. // 默认构造函数,T()是缺省值(T类型的默认构造函数)
    11. list_node(const T& x= T())
    12. :_data(x)
    13. ,_prev(nullptr)
    14. ,_next(nullptr)
    15. {}
    16. };
    17. }

    2、 list 的迭代器

    前面学习的 string 和 vector 容器的正向迭代器其实就是原生指针,++ 可以直接走到下一个元素,而 list 的正向迭代器是用一个类,把节点指针封装起来,然后通过实现运算符重载函数,让迭代器具有像指针一样的行为,比如:重载了 * 运算符,实现了 *it 解引用返回节点中的存储的数据的引用,重载了 ++ 运算符,实现了 it++ 直接走到下一个元素。

    list 正向迭代器的结构: 

    1. namespace xyl
    2. {
    3. // T:节点中数据的类型 Ref:节点中数据的引用 Ptr:节点中数据的指针(地址)
    4. template<class T, class Ref, class Ptr>
    5. struct __list_iterator
    6. {
    7. typedef list_node Node; // 节点
    8. typedef __list_iterator self; // 自己这个类型
    9. Node* _node; // 节点指针
    10. // 构造函数
    11. __list_iterator(Node* node) // 用节点指针来构造
    12. :_node(node)
    13. {}
    14. // 让迭代器具有像指针一样的行为
    15. Ref operator*() // *it 本质是:it.operator*()
    16. {
    17. return _node->_val; // 返回节点中数据(对象)的引用/const常引用,具体返回什么取决于在list类域中定义迭代器时传给Ref的参数是什么
    18. // 我们想要得到的是节点中存储的数据(对象),而不是整个节点(节点中包含数据和2个指针)
    19. }
    20. Ptr operator->() // it-> 本质是:it.operator->()
    21. {
    22. return &(operator*()); // 返回节点中数据(对象)的指针/const常指针,具体返回什么取决于在list类域中定义迭代器时传给Ptr的参数是什么
    23. }
    24. // 让迭代器可以移动(双向)
    25. // ++it 本质是:it.operator++()
    26. Self& operator++()
    27. {
    28. _node = _node->_next;
    29. return *this;
    30. }
    31. // it++ 本质是:it.operator++(0)
    32. Self operator++(int) // 不能用引用返回
    33. {
    34. Self tmp(*this);
    35. _node = _node->_next;
    36. return tmp;
    37. }
    38. // --it 本质是:it.operator--()
    39. Self& operator--()
    40. {
    41. _node = _node->_prev;
    42. return *this;
    43. }
    44. // it-- 本质是:it.operator--(0)
    45. Self operator--(int) // 不能用引用返回
    46. {
    47. Self tmp(*this);
    48. _node = _node->_prev;
    49. return tmp;
    50. }
    51. // 让两个迭代器可以进行比较,判断两个迭代器是否相等,其实判断的是节点指针是否相等
    52. bool operator!=(const Self& l)const
    53. {
    54. return _node != l._node;
    55. }
    56. bool operator==(const Self& l)const
    57. {
    58. return _node != l._node;
    59. }
    60. };
    61. }

    迭代器有两种实现方式,具体应根据容器底层数据结构实现:

    1. 原生态指针比如:vector。
    2. 将原生态指针进行封装,因迭代器使用形式与指针完全相同。

    因此在自定义的类中必须实现以下方法:

    1. 指针可以解引用,迭代器的类中必须重载 operator*()
    2. 指针可以通过 -> 访问其所指空间成员,迭代器类中必须重载 oprator->()
    3. 指针可以 ++ 向后移动,迭代器类中必须重载 operator++() 与 operator++(int)
    4. 至于operator--() / operator--(int) 释放需要重载,根据具体的结构来抉择,双向链表可以向前移动,所以需要重载,如果是 forward_list 就不需要重载 -- 。
    5. 迭代器需要进行是否相等的比较,因此还需要重载 operator==() 与 operator!=()

    (1)如何实现 const 迭代器?

    string 和 vector 的迭代器就是一个原生指针,我们给原生指针加上 const 就可以变成 const 迭代器,比如:

    1. // T: 容器中存储的数据的类型
    2. typedef T* iterator;
    3. typedef const T* const_iterator;

    但 list 的迭代器不是一个原生指针,而是一个类里面封装了原生指针,那如何实现 list 的 const 迭代器呢?

    const 迭代器,顾名思义,就是不能改变的迭代器,是不能通过 * 和 -> 修改指向成员的,所以我们把 operator*() 和 operator->() 函数的返回值改成常引用即可。比如:

    const T& operator*() ; // 返回节点中数据(对象)的const常引用

    那我们需要实现一个 __list_const_iterator 的类吗?在类中重载一个返回值为常引用的 operator*() 函数。

    普通写法需要这样,因为 T& operator*(); 和 const T& operator*(); 这两个函数只是返回值不同,无法放在一个类里面构成重载。

    SGI 版本 stl_list 源码中是怎么样实现的:

    通过增加模板参数来控制,普通迭代器传过来的参数就是 T&,const 迭代器传过来的参数就是 const T&。 


    (2)迭代器遍历 list 中节点的方式是什么?

    如果迭代器管理的节点中的数据是内置类型

    1. void test()
    2. {
    3. int arr[] = { 1, 2, 3, 4, 5 };
    4. list<int> lt(arr, arr + sizeof(arr) / sizeof(int));
    5. list<int>::iterator it = lt.begin();
    6. while (it != lt.end())
    7. {
    8. // 对指向当前节点的迭代器解引用 *it 可以得到当前节点中存储的数据
    9. cout << *it << " "; // it.operator*()
    10. ++it;
    11. }
    12. }

    如果迭代器管理的节点中的数据是自定义类型 / 结构体

    1. struct TreeNode
    2. {
    3. int _val;
    4. struct TreeNode* _left;
    5. struct TreeNode* _right;
    6. TreeNode(int val = -1)
    7. :_val(val)
    8. ,_left(nullptr)
    9. ,_right(nullptr)
    10. {}
    11. };
    12. void test()
    13. {
    14. list lt;
    15. lt.push_back(TreeNode(1));
    16. lt.push_back(TreeNode(2));
    17. lt.push_back(TreeNode(3));
    18. list::iterator it = lt.begin();
    19. while (it != lt.end())
    20. {
    21. /* 错误写法:
    22. * 对指向当前节点的迭代器解引用*it可以得到当前节点中存储的TreeNode结构体
    23. * TreeNode结构体中存储的数据_val是不能直接输出出来的
    24. * 除非重载了专门针对输出TreeNode结构体中数据的流插入运算符,比如:ostream& operator<<(ostream& out, const TreeNode node);
    25. */
    26. // cout << *it << " "; // error!!!
    27. /* 正确写法1:
    28. * 对指向当前节点的迭代器解引用*it得到当前节点中存储的TreeNode结构体
    29. * 然后用'.'再去访问 TreeNode 结构体中存储的数据_val
    30. */
    31. cout << (*it)._val << " ";
    32. /* 正确写法2:
    33. * 调用 operator->() 函数
    34. * 迭代器->,返回迭代器指向节点中存储的TreeNode结构体的地址(指针):TreeNode*
    35. * 然后该指针再使用'->'访问结构体中存储的数据_val
    36. * 代码为:it->->_val,但可读性太差,编译器进行了特殊处理,省略掉了一个箭头,保持了程序的可读性
    37. */
    38. // 注意:一般结构体的指针才会使用'->'来访问成员
    39. // 当迭代器管理的节点中的数据是结构体的时候,就可以用'->'
    40. cout << it->_val << " "; // 这种写法常用
    41. it++;
    42. }
    43. }

    (3)迭代器( __list_iterator 类)中需要写拷贝构造、赋值重载、析构函数吗?

    不需要,默认生成的就可以,而且也不需要我们去实现。虽然迭代器中封装了节点指针,但节点是归链表管理的,链表 list 中的节点也不需要迭代器去释放,list 自己会去释放。


    (4) vector 和 list 的迭代器哪个更复杂呢?有什么区别呢?

    它们的大小都是一样的(32 位下),vector 的迭代器是一个原生指针,大小为 4 字节;list 的迭代器是一个类封装了一个节点指针,大小也是 4 字节,物理上没什么区别。

    list 的迭代器之所以要定义成自定义类型,是因为原生指针无法直接做到像指针一样的行为,所以需要用自定义的类把原生指针封装起来,然后在类中定义一些运算符重载函数,使得这个自定义类型的对象通过调用这些函数,从而可以做到像指针一样的行为。

    我们在物理上可以看到,都是存了一个指针,但在运行逻辑上完全不同。

    • vector 迭代器 ++,是一个指针直接 ++ 到下一个位置;
    • list 迭代器 ++,是一个自定义类型的对象调用了 operator++() 函数,在函数内算出下一个位置。

    3、list 的底层结构

    带头双向循环链表结构:

    1. // 带头双向循环链表结构
    2. namespace xyl
    3. {
    4. template<class T> // T: 节点中数据的类型
    5. class list
    6. {
    7. public:
    8. typedef __list_node Node; // 节点
    9. public:
    10. // 迭代器
    11. // iterator是内嵌类型,在list类域里面定义的类型(类中类)
    12. // 为啥要typedef呢?为了统一规范名称,所有容器迭代器都叫iterator,用起来更方便
    13. // 普通迭代器,指明模板参数为 T, T&, T*
    14. typedef __list_iterator iterator;
    15. // const迭代器,指明模板参数为 T, const T&, const T*
    16. typedef __list_iteratorconst T&, const T*> const_iterator;
    17. iterator begin(); // 返回指向第一个有效节点的迭代器
    18. iterator end(); // 返回指向头节点的迭代器
    19. const_iterator begin() const;
    20. const_iterator end() const;
    21. private:
    22. Node* _head; // 头节点指针
    23. public:
    24. // 默认成员函数
    25. list(); // 默认构造函数
    26. template<class InputIterator>
    27. list(InputIterator first, InputIterator last); // 带参构造函数
    28. list(const list& lt); // 拷贝构造函数(深拷贝)
    29. list& operator=(list lt) // 赋值运算符重载函数(深拷贝)
    30. ~list(); // 析构函数
    31. // 容量操作
    32. size_t size(); // 有效元素个数
    33. bool empty(); // 判空
    34. // 修改操作
    35. iterator insert(iterator pos, const T& data); // 指定迭代器位置之前插入元素
    36. iterator erase(iterator pos); // 删除指定迭代器位置的元素
    37. void push_back(const T& data); // 尾插
    38. void push_front(const T& data); // 头插
    39. void pop_back(); // 尾删
    40. void pop_front(); // 头删
    41. void clear(); // 清空有效元素
    42. };
    43. }

    注意:list 类域中的 __list_iterator 和 __list_iterator 是定义在类内部的类(嵌套类型)。

    用 typedef 重命名为 iterator 和 const_iterator ,目的是为了统一规范迭代器的名称,所有容器的迭代器都叫 iterator 等,用起来更方便。否则每个容器都定义迭代器,如果每个容器迭代器的名字都不一样,那用户的使用成本就太高了。

    1. // 普通迭代器,指明模板参数为 T, T&, T*
    2. typedef __list_iterator iterator;
    3. // const迭代器,指明模板参数为 T, const T&, const T*
    4. typedef __list_iteratorconst T&, const T*> const_iterator;

    其中 T 、 T& 、 T* 都是类型参数。当 list 的类型确定了,T 、 T& 、 T* 的类型自动确定,那么 iterator 的类型同时也自动确定了,这看起来就像 iterator 和 list 是共生的一样。有什么样的螺丝就需要什么样的螺丝刀,否则螺丝刀再漂亮,没有对号的螺丝也是没用的。


    4、list 的一些成员函数

    (1)默认成员函数
    a. 构造函数
    1. // 默认构造函数
    2. list()
    3. {
    4. // 构造头节点,自己指向自己
    5. _head = new Node;
    6. _head->_prev = _head;
    7. _head->_next = _head;
    8. }
    9. // 用迭代器区间初始化[first,last)
    10. template<class InputIterator>
    11. list(InputIterator first, InputIterator last)
    12. :_head(new Node) // 当前对象是一个正在构造的对象,成员变量是随机值,需要进行初始化
    13. {
    14. // 建立头节点自身的链接
    15. _head->_prev = _head;
    16. _head->_next = _head;
    17. // 用迭代器遍历元素,尾插到新容器中
    18. while (first != last)
    19. {
    20. push_back(*first);
    21. first++;
    22. }
    23. }

    b. 拷贝构造函数(传统写法和现代写法都可以)
    1. //拷贝构造函数(深拷贝--传统写法)
    2. // lt2(lt1)
    3. list(const list& lt)
    4. :_head(new Node) // 当前对象是一个正在构造的对象,成员变量是随机值,需要进行初始化
    5. {
    6. // 建立头节点自身的链接
    7. _head->_prev = _head;
    8. _head->_next = _head;
    9. // 把lt中所有节点拷贝插入到新容器中
    10. for (const auto& e : lt)
    11. {
    12. push_back(e);
    13. }
    14. }
    15. // 拷贝构造函数(深拷贝--现代写法)
    16. // lt2(lt1)
    17. list(const list& lt)
    18. :_head(new Node) // 当前对象是一个正在构造的对象,成员变量是随机值,需要进行初始化
    19. {
    20. // 建立头节点自身的链接
    21. _head->_prev = _head;
    22. _head->_next = _head;
    23. list tmp(lt.begin(), lt.end());
    24. std::swap(_head, tmp._head); // 交换两个容器的内容
    25. }

    c. 赋值运算符重载函数(深拷贝——传统写法)
    1. list& operator=(const list& lt)
    2. {
    3. if (this != <) // 防止自己给自己赋值
    4. {
    5. // 清除当前容器所有有效元素
    6. clear();
    7. // 把lt中所有节点拷贝插入到当前容器中
    8. for (const auto& e : lt)
    9. {
    10. push_back(e);
    11. }
    12. }
    13. return *this; // 返回当前容器
    14. }

    d. 赋值运算符重载函数(深拷贝——现代写法)
    1. list& operator=(list lt) // 传值传参,拷贝构造出临时对象
    2. {
    3. std::swap(_head, lt._head); // 交换两个容器的内容
    4. return *this; // 返回当前容器
    5. }

    e. 析构函数 
    1. ~list()
    2. {
    3. /*
    4. Node* cur = _head->_next; // 从第一个有效节点开始删除
    5. while (cur != _head)
    6. {
    7. Node* next = cur->_next; // 先记录下一个节点
    8. delete cur; // 释放当前节点
    9. cur = next; // 遍历下一个节点
    10. }
    11. delete _head; // 释放头节点
    12. _head = nullptr;
    13. */
    14. clear();
    15. delete _head;
    16. _head = nullptr;
    17. }

    (2)容量操作 
    a. size
    1. // O(N)
    2. size_t size()
    3. {
    4. size_t n = 0; // 遍历整个链表,统计有效元素个数
    5. iterator it = begin();
    6. while (it != end())
    7. {
    8. n++;
    9. it++;
    10. }
    11. return n;
    12. }

    b. empty
    1. bool empty()
    2. {
    3. return begin() == end();
    4. }

    (3)修改操作
    a.  insert

    1. iterator insert(iterator pos, const T& data)
    2. {
    3. Node* cur = pos._node; // 当前节点(pos中封装了节点的指针)
    4. Node* prev = cur->_prev; // 前驱节点
    5. Node* newnode = new Node(data); // 新节点
    6. // 建立前驱节点和新节点的链接
    7. prev->_next = newnode;
    8. newnode->_prev = prev;
    9. // 建立新节点与当前节点的链接
    10. newnode->_next = cur;
    11. cur->_prev = newnode;
    12. // 返回指向第一个新插入元素的迭代器
    13. return iterator(newnode); // 注意:此处调用iterator的构造函数构造出一个匿名对象
    14. // return newnode; // 也可以这样写,因为单参数的构造函数支持隐式类型的转换
    15. }

    b. erase
    1. iterator erase(iterator pos)
    2. {
    3. assert(pos != end()); // 不能删除头节点
    4. Node* cur = pos._node; // 当前节点
    5. Node* prev = cur->_prev; // 前驱节点
    6. Node* next = cur->_next; // 后继节点
    7. // 建立前驱节点与后继节点的链接
    8. prev->_next = next;
    9. next->_prev = prev;
    10. // 删除当前节点
    11. delete cur;
    12. // 返回指向被删除节点的下一个节点的迭代器
    13. return iterator(next); // 注意:此处调用iterator的构造函数构造出一个匿名对象
    14. }

    c. push_back 和 push_front
    1. void push_back(const T& data) // 尾插
    2. {
    3. /*
    4. Node* newNode = new Node(data); // 构造新节点
    5. Node* tail = _head->_prev; // 尾节点
    6. // 建立尾节点和新节点的链接
    7. tail->_next = newNode;
    8. newNode->_prev = tail;
    9. // 建立新节点和头节点的链接
    10. newNode->_next = _head;
    11. _head->_prev = newNode;
    12. */
    13. insert(end(), data); // 在头节点的前面插入,相当于是尾插
    14. }
    15. void push_front(const T& data) // 头插
    16. {
    17. insert(begin(), data); // 在第一个有效节点前面插入,相当于是头插
    18. }

    d. pop_back 和 pop_front
    1. void pop_back() // 尾删
    2. {
    3. erase(--end());
    4. }
    5. void pop_front() // 头删
    6. {
    7. erase(begin());
    8. }

    e. clear
    1. void clear()
    2. {
    3. /*
    4. iterator it = begin();
    5. while (it != end())
    6. {
    7. Node* cur = it._node; // 记录当前节点
    8. it++; // 遍历下一个节点
    9. delete cur; // 删除当前节点
    10. }
    11. // 建立头节点自身的链接
    12. _head->_next = _head;
    13. _head->_prev = _head;
    14. */
    15. iterator it = begin();
    16. while (it != end())
    17. {
    18. it = erase(it); // 删除当前节点,返回指向下一个节点的迭代器
    19. }
    20. }

    【补充】 类模板成员函数的实例化

    编译器进行类模板推演,实例化出一个 list 类,然后再实例化出这个 lt 对象。

    • 类模板中的成员函数都是函数模板。
    • 类模板成员函数的实例化,是按需实例化,只有当程序用到它时才进行实例化。
    • 一个模板,如果没有实例化,编译器是不会去检查它内部的语法的,也不会报错。
    1. void test()
    2. {
    3. list<int> lt;
    4. lt.push_back(1);
    5. lt.push_back(2);
    6. lt.push_back(3);
    7. lt.clear();
    8. }

    四、listvector的对比

    vector list 都是 STL 中非常重要的序列式容器,由于两个容器的底层结构不同,导致其特性以及应用场景不同,其主要不同如下:
  • 相关阅读:
    鸿蒙实战项目开发:【短信服务】
    阿里云服务器使用经验总结
    Nanoprobes FluoroNanogold 偶联物的特色和应用
    成都易佰特的坑——E103-W06
    R语言机器学习之caret包详解
    APP攻防--安卓逆向&JEB动态调试&LSPosed模块&算法提取&Hook技术
    Nacos 认识和安装-1
    修改MySQL密码的四种方法(适合初学者)
    Win7安装VMware
    初步了解Vite
  • 原文地址:https://blog.csdn.net/weixin_74531333/article/details/133280769