- 一、STL - list 的介绍
- 二、list 的使用
- 2.1 常见构造
- 2.2 迭代器及其遍历操作
- 2.3 容量操作
- 2.4 访问操作
- 2.5 修改操作
- ① 迭代器失效(⭐)
- ② 成员 / 非成员 swap 函数
- 2.6 容器操作(了解,用的很少)
- 三、容器迭代器(重要)
- 3.1 容器迭代器的分类(⭐重要)
- 3.2 迭代器的实现方式分析(⭐重要)
- 四、stl_list 源码剖析
- 4.1 list 的底层结构
- 五、list 的模拟实现
- 5.1 list 的节点结构
- 5.2 list 的迭代器(⭐重要)
- ① 迭代器的结构
- ② 一些思考(⭐重要)
- 5.3 list 的底层结构
- 5.4 list 的一些成员函数
- ① 默认成员函数
- ② 容量操作
- ③ 修改操作
- 5.5 补充:类模板成员函数的实例化
- 六、list 和 vector 的对比
文档介绍:list - C++ Reference (cplusplus.com)
- list 是序列式容器,可以在序列中的任意位置进行常数时间 O(1) 的插入和删除操作,以及双向迭代。
- list 的底层是「带头双向循环链表」结构,双向链表中每个元素存储在不同且不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。
- list 与 forward_list 非常相似:主要区别在于 forward_list 是单链表,因此只能向前迭代,让其更简单高效。
- 与其他的序列式容器相比 (array,vector,deque),list 在容器内任意位置进行插入、删除、移动元素方面的执行效率通常表现更好。
- 与其他序列式容器相比,list 和 forward_list 「最大的缺陷是不支持任意位置的随机访问」,比如:要访问 list 中的第 6 个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在它们之间的距离上迭代需要线性的时间开销;list 还需要一些额外的空间,以保存每个元素相关联的链接信息(对于存储类型较小元素的大型列表来说这可能是一个重要的因素)
forward_list 实际中用的很少,单链表尾插和尾删效率太低了,尤其是尾删,不实用
template < class T, class Alloc = allocator> class list;
二、list 的使用 2.1 常见构造
| Constructor | 接口说明 |
|---|---|
| list () | 构造一个没有元素的空容器,size = 0 |
| list (size_type n, const value_type& val = value_type()) | 构造一个包含 n 个元素的容器。每个元素都是 val |
| list (const list& x) | 拷贝构造 |
| template | 用迭代器区间 [first, last) 中的元素构造 list |
Example:
// constructing lists #include2.2 迭代器及其遍历操作#include int main () { // constructors used in the same order as described above: std::list
first; // empty list of ints std::list second (4,100); // four ints with value 100 std::list third (second.begin(),second.end()); // iterating through second std::list fourth (third); // a copy of third // the iterator constructor can also be used to construct from arrays: int myints[] = {16,2,77,29}; std::list fifth (myints, myints + sizeof(myints) / sizeof(int) ); return 0; }
| Iterators | 接口说明 |
|---|---|
| begin(iterator / const_iterator) | 返回指向容器中第一个元素的迭代器 |
| end | 返回指向容器中的最后一个元素下一个位置的迭代器(即头节点) |
| rbegin(reverse_iterator / const_reverse_iterator) | 反向迭代器 |
| rend | 反向迭代器 |
| 范围for | C++11 支持更简介的范围 for 的新遍历方式(底层其实是被替换成迭代器,所以支持迭代器就支持范围 for) |
Example:
void test1()
{
list lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
for (auto it = lt.begin(); it != lt.end(); it++)
cout << *it << " ";
cout << endl;
for (auto& e : lt)
cout << e << " ";
cout << endl;
}
2.3 容量操作
| Capacity | 接口说明 |
|---|---|
| empty | 判断容器是否为空 |
| size | 返回容器中有效元素的个数(需要遍历完链表,时间代价为O(n)) |
不支持 [] 运算符,因为 list 容器物理地址不是连续的。
| Element access | 接口说明 |
|---|---|
| front | 返回容器中第一个元素的引用 |
| back | 返回容器中最后一个元素的引用 |
| Modifiers | 接口说明 |
|---|---|
| push_front | 头插,在 list 的第一个元素之前插入一个新元素 |
| pop_front | 头删,删除 list 中第一个元素 |
| push_back | 尾插,在容器的最后一个元素之后添加一个新元素 |
| pop_back | 尾删,删除容器中的最后一个元素 |
| insert | 在指定迭代器位置的元素之前插入新元素来扩展容器 插入单个元素:iterator insert (iterator position, const value_type& val); |
| erase | 从列表容器中删除单个元素(迭代器位置)或一系列元素([first,last)) 删除单个元素:iterator erase (iterator position); |
| swap | 交换两个容器的内容 |
| resize | 调整有效元素个数(size),多出的位置用缺省值填充 |
| clear | 清空容器中的有效元素,size = 0,并没有清除头节点哦! |
注意:list 可以在 任意位置 进行 常数时间 的 插入 和 删除 操作。
insert 函数:
// 返回指向第一个新插入元素的迭代器 iterator insert (iterator position, const value_type& val); // 传值传参
erase 函数:
// 返回指向被删除元素的下一个元素的迭代器,如果操作擦除了序列中的最后一个元素,则这是容器端 iterator erase (iterator position); // 传值传参
① 迭代器失效(⭐)
思考:使用插入(insert)删除(erase)操作会导致迭代器失效吗?
因为 list 的底层结构为带头双向循环链表,因此在 list 中进行插入时是不会导致 list 的迭代器失效的,只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他节点的迭代器不会受到影响。
insert 不会导致迭代器失效,因为 list 物理空间不是连续的,是一个个独立节点,pos 始终指向当前节点
erase 会导致迭代器失效,因为 pos 指向的节点已经被释放了,pos 指向了非法空间,所以 erase 会返回指向被删除节点的下一个位置的迭代器
void test1() { listlt; lt.push_back(1); lt.push_back(2); lt.push_back(3); auto it = lt.begin(); while (it != lt.end()) it = lt.erase(it); // 想要使用迭代器继续访问list,需要给 it 重新赋值 };
② 成员 / 非成员 swap 函数
拓展:关于 swap 函数,标准容器中的成员函数 swap 和库中的非成员函数 std::swap 效率对比
-
string、vector、list 等容器都有自己的 swap 成员函数,如果要交换两个容器的内容,尽量使用这个容器自己的 swap 成员函数,而不是使用非成员 std::swap 函数。
-
注意:非成员 std::swap 函数,可能不是交换存储大量数据的类的内容的最有效方式,因为这些操作中的每一个通常在其大小上以线性时间操作。大数据类型可以提供此函数的重载版本,以优化其性能。值得注意的是,所有标准容器都以这样一种方式对其进行了专门化(重载),即只交换少数内部指针而不是它们的全部内容,从而使它们在恒定时间内运行。
-
样例:
void test2() { listlt1; lt1.push_back(1); lt1.push_back(2); lt1.push_back(3); list lt2; lt2.push_back(10); lt2.push_back(20); // cpp98版本情况下 lt1.swap(lt2); std::swap(lt1, lt2); // 非成员std::swap函数,不建议使用,运行效率低 } 非成员函数 std::swap 为什么运行效率很低呢?(C++98版本),因为它是深拷贝式的交换。
而成员函数 list::swap 只需要交换成员变量头指针就可以了。
2.6 容器操作(了解,用的很少)
| Operations | 接口说明 |
|---|---|
| sort | 对 list 容器中的元素进行排序(注意:这是 list 的成员函数哦),效率很低,链表排序也没有太大的作用和价值。 |
| unique | 去重(删除重复值),必须要先排序,才能去完重复值 |
| remove | 从容器中移除所有值等于 val 的元素(void remove (const value_type& val);) |
| reverse | 反转容器中元素的顺序 |
| merge | 合并排序序列(合并前两个 list 容器都要排好序) |
sort 函数:
-
void sort(); // 1 template
// 2 void sort (Compare comp); -
等价元素的结果顺序是稳定的:即等价元素保留它们在调用之前的相对顺序。
-
整个操作不涉及任何元素对象的构造、销毁或复制。元素在容器内移动。
函数默认是排升序(<),如果想要排降序(>),需要传仿函数,比如:
void test3()
{
int arr[] = { 10,2,5 };
list lt(arr, arr + sizeof(arr) / sizeof(int));
// > 排降序
// 写法一:
greater g; // 类模板,在头文件中
lt.sort(g);
// 写法二:
lt.sort(greater()); // 匿名对象
// lt: 10 5 2
}
unique 函数:
void test4()
{
int arr[] = { 2,4,3,2,4 };
list lt(arr, arr + sizeof(arr) / sizeof(int));
lt.sort(); // 去重前,必须要先排序
lt.unique(); // 去除重复值
// lt: 2 3 4
}
remove 函数:
void test5()
{
int arr[] = { 1,2,3,3,4 };
list lt(arr, arr + sizeof(arr) / sizeof(int));
lt.remove(3); // lt: 1 2 4
}
reverse 函数:
void test6()
{
int arr[] = { 1,2,3,4,5 };
list lt(arr, arr + sizeof(arr) / sizeof(int));
lt.reverse(); // rlt: 5 4 3 2 1
}
三、容器迭代器(重要) 3.1 容器迭代器的分类(⭐重要)
-
从使用功能角度:正向 / 反向 + const
-
从容器底层结构角度:单向迭代器、双向迭代器、随机访问迭代器
- 单向(Unidirectional Iterator):单链表 forward_list、哈希表 unordered_set/map 迭代器,可以 ++
- 双向(Bidirectional Iterator):双向链表 list、红黑树 set/map 迭代器,可以 ++ / –
- 随机(Random Access Iterator):string、vector、deque 迭代器,一般容器的底层都是连续的数组,可以 ++ / – / + / -
- 单向 <-- 双向 <-- 随机
迭代器的本质是:在不暴露容器底层实现细节的情况下,提供了一种统一的方式来访问 / 修改容器中存储的数据。
容器、迭代器、算法间的关系:
算法不能直接访问和修改容器中存储的数据,而迭代器就是容器和算法之间的胶合剂。
我们来看看 stl_iterator 源码中对于迭代器的分类:
3.2 迭代器的实现方式分析(⭐重要)
迭代器有两种实现方式,具体应根据「容器底层数据结构的实现」来确定。
因为每个特定的容器,都有特定的内部内存结构,也就意味着遍历/迭代过程的细节是不一样的。
方式一: 前提条件:原生指针指向的空间 物理地址是连续的,比如 string、vector
-
所以让原生指针 ++ 可以直接走到下一个位置,-- 可以直接回到上一个位置,* 解引用可以直接得到这个位置的数据。
比如 vector:
-
此时原生指针就是一个天然的迭代器(但并不是原生指针牛逼,而是因为它指向的空间物理地址是连续的)。
方式二: 原生指针指向的空间 物理地址不连续,比如:list、map/set
-
所以让原生指针 ++ 不能直接走到下一个位置,-- 不能直接回到上一个位置,* 解引用不能直接得到这个位置的数据,只能得到一个结构体(结构体中包含数据和指针域)。
比如 list:
-
此时原生指针不是一个天然的迭代器,所以我们需要用一个类封装原生指针,重载相关运算符,控制对象使用这些运算符的行为,让这个类的对象,用起来像指针一样。
-
为了让迭代器的使用形式与指针完全相同,因此在自定义的类中必须实现以下方法:
- 指针可以解引用,则迭代器的类中必须重载 operator*()
- 指针可以通过 -> 访问其所指空间成员,则迭代器类中必须重载 oprator->()
- 指针可以 ++ 向后移动,则迭代器类中必须重载 operator++() 与 operator++(int) ,至于 operator–() / operator–(int) 是否需要重载,要根据容器具体的底层结构来抉择,比如 list 双向链表可以向前移动,所以需要重载 operator–() 和 operator–(int),如果是 forward_list 就不需要重载 –
- 迭代器需要进行是否相等的比较,因此还需要重载 operator==() 与 operator!=()
四、stl_list 源码剖析 4.1 list 的底层结构
SGI 版本 STL 中 list 部分源码:
// 链表节点结构 templatestruct __list_node { typedef void* void_pointer; void_pointer next; void_pointer prev; T data; }; // 迭代器 -- 一个类封装了节点指针 template struct __list_iterator { typedef __list_iterator iterator; typedef __list_iterator const_iterator; typedef __list_iterator self; // ... typedef Ptr pointer; typedef Ref reference; // ... typedef __list_node * link_type; link_type node; // 节点指针 // ... }; // 带头双向循环链表结构 template class list { protected: typedef __list_node list_node; // 节点 // ... public: typedef T value_type; typedef list_node* link_type; // 节点指针 // ... public: // 迭代器(这里的iterator和const_iterator是定义在list类域中的内嵌类型) typedef __list_iterator iterator; // 传了T,T&,T*模板参数 typedef __list_iterator const_iterator; // 传了T,const T&,const T*模板参数 // ... protected: link_type node; // 成员变量,节点指针 // ... };
五、list 的模拟实现 5.1 list 的节点结构
双向循环链表节点结构:一个数据域,两个指针
namespace winter
{
// 节点结构
// T: 节点中数据的类型
template
struct __list_node
{
T _data; // 数据域
__list_node* _prev; // 前驱指针
__list_node* _next; // 后继指针
// 默认构造函数,T()是缺省值(T类型的默认构造函数)
__list_node(const T& data = T())
:_data(data)
,_prev(nullptr)
,_next(nullptr)
{}
};
}
5.2 list 的迭代器(⭐重要) ① 迭代器的结构
前面学习的 string 和 vector 容器的正向迭代器其实就是原生指针,++ 可以直接走到下一个元素,而 list 的正向迭代器是用一个类,把节点指针封装起来,然后通过实现运算符重载函数,让迭代器具有像指针一样的行为,比如:重载了 * 运算符,实现了 *it 解引用返回节点中的存储的数据的引用,重载了 ++ 运算符,实现了 it++ 直接走到下一个元素 ……
list 正向迭代器的结构:
namespace winter
{
template
struct __list_iterator
{
typedef __list_iterator self; // 自己这个类型
typedef __list_node Node; // 节点
Node* _node; // 节点指针
// 构造函数
__list_iterator(Node* node) // 用节点指针来构造
:_node(node)
{}
// 运算符重载
// 让迭代器具有像指针一样的行为
Ref operator*() const;
Ptr operator->() const;
// 让迭代器可以移动(双向)
self& operator++();
self operator++(int); // 不能用引用返回
self& operator--();
self operator--(int); // 不能用引用返回
// 让两个迭代器可以进行比较
// 判断两个迭代器是否相等,其实判断的是节点指针是否相等
bool operator==(const self& it);
bool operator!=(const self& it);
};
}
迭代器中的运算符重载函数:
让迭代器具有像指针一样的行为,重载 * 和 -> 运算符函数:
这两个运算符重载函数也是实现 普通迭代器 和 const 迭代器 的关键!
// *it 本质是:it.operator*()
Ref operator*() const
{
// 返回节点中数据(对象)的引用 / const常引用,具体返回什么取决于在list类域中定义迭代器时传给 Ref 的参数是什么
return _node->_data;
// 我们想要得到的是节点中存储的数据(对象),而不是整个节点(节点中包含数据和2个指针)
}
// it-> 本质是:it.operator->()
Ptr operator->() const
{
// 返回节点中数据(对象)的指针 / const常指针,具体返回什么取决于在list类域中定义迭代器时传给 Ptr 的参数是什么
return &(_node->_data);
}
让迭代器可以移动(双向),重载 ++ 和 – 运算符函数:
// 这些函数返回的是自己这个类型 __list_iterator的对象 // ++it 本质是:it.operator++() self& operator++() { _node = _node->_next; return *this; // 返回++后的迭代器 } // it++ 本质是:it.operator++(0) self operator++(int) // 不能用引用返回 { self tmp(*this); _node = _node->_next; return tmp; // 返回++前的迭代器 } // --it 本质是:it.operator--() self& operator--() { _node = _node->_prev; return *this; // 返回--后的迭代器 } // it-- 本质是:it.operator--(0) 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;
}
② 一些思考(⭐重要)
思考 1:如何实现 const 迭代器
string 和 vector 的迭代器就是一个原生指针,我们给原生指针加上 const 就可以变成 const 迭代器,比如:
// T: 容器中存储的数据的类型 typedef T* iterator; 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 中节点的方式
如果迭代器管理的节点中的数据是内置类型:
void test1()
{
int arr[] = { 1,2,3,4,5 };
list lt(arr, arr + sizeof(arr) / sizeof(int));
list::iterator it = lt.begin();
while (it != lt.end())
{
// 对指向当前节点的迭代器解引用 *it 可以得到当前节点中存储的数据
cout << *it << " "; // it.operator*()
++it;
}
}
如果迭代器管理的节点中的数据是自定义类型 / 结构体:
struct TreeNode
{
int _val;
struct TreeNode* _left;
struct TreeNode* _right;
TreeNode(int val = -1)
:_val(val)
,_left(nullptr)
,_right(nullptr)
{}
};
void test2()
{
list lt;
lt.push_back(TreeNode(1));
lt.push_back(TreeNode(2));
lt.push_back(TreeNode(3));
list::iterator it = lt.begin();
while (it != lt.end())
{
// cout << *it << " "; // error!!!
cout << (*it)._val << " ";
// 注意:一般结构体的指针才会使用'->'来访问成员
// 当迭代器管理的节点中的数据是结构体的时候,就可以用'->'
cout << it->_val << " "; // 这种写法常用
it++;
}
}
思考 3:迭代器( __list_iterator 类)中需要写拷贝构造、赋值重载、析构函数吗?
不需要,默认生成的就可以,而且也不需要我们去实现,虽然迭代器中封装了节点指针,但节点是归链表管理的,链表 list 中的节点也不需要迭代器去释放,list 自己会去释放。
思考 4:vector 和 list 的迭代器哪个更复杂呢?有什么区别呢?
它们的大小都是一样的(32位下),vector 的迭代器是一个原生指针,大小为 4 字节;list 的迭代器是一个类封装了一个节点指针,大小也是 4 字节,物理上没啥区别。
list 的迭代器之所以要定义成自定义类型,是因为原生指针无法直接做到像指针一样的行为,所以需要用自定义的类把原生指针封装起来,然后在类中定义一些运算符重载函数,使得这个自定义类型的对象通过调用这些函数,从而可以做到像指针一样的行为。
我们在物理上可以看到,都是存了一个指针,但在运行逻辑上完全不同。
vector 迭代器 ++,是一个指针直接 ++ 到下一个位置;
而 list 迭代器 ++,是一个自定义类型的对象调用了 operator++() 函数,在函数内算出下一个位置。
我们可以看到类型的力量,C++ 的类,运算符重载的能力是非常强大滴。
5.3 list 的底层结构
带头双向循环链表结构:
namespace winter
{
// 带头双向循环链表结构
// T: 节点中数据的类型
template
class list
{
public:
typedef __list_node Node; // 节点
public:
// 迭代器
// iterator是内嵌类型,在list类域里面定义的类型(类中类)
// 为啥要typedef呢?为了统一规范名称,所有容器迭代器都叫iterator,用起来更方便
// 普通迭代器,指明模板参数为 T, T&, T*
typedef __list_iterator iterator;
// const迭代器,指明模板参数为 T, const T&, const T*
typedef __list_iterator const_iterator;
iterator begin(); // 返回指向第一个有效节点的迭代器
iterator end(); // 返回指向头节点的迭代器
const_iterator begin() const;
const_iterator end() const;
private:
Node* _head; // 头节点指针
public:
// 默认成员函数
list(); // 默认构造函数
template
list(InputIterator first, InputIterator last); // 带参构造函数
list(const list& lt); // 拷贝构造函数(深拷贝)
list& operator=(list lt) // 赋值运算符重载函数(深拷贝)
~list(); // 析构函数
// 容量操作
size_t size(); // 有效元素个数
bool empty(); // 判空
// 修改操作
iterator insert(iterator pos, const T& data); // 指定迭代器位置之前插入元素
iterator erase(iterator pos); // 删除指定迭代器位置的元素
void push_back(const T& data); // 尾插
void push_front(const T& data); // 头插
void pop_back(); // 尾删
void pop_front(); // 头删
void clear(); // 清空有效元素
};
}
注意:
list 类域中的 __list_iterator
用 typedef 重命名为 iterator 和 const_iterator ,目的是为了统一规范迭代器的名称,所有容器的迭代器都叫 iterator 等,用起来更方便。要不然,每个容器都定义迭代器,如果每个容器迭代器的名字都不一样,那用户使用起来成本太高了。
// 普通迭代器,指明模板参数为 T, T&, T* typedef __list_iteratoriterator; // const迭代器,指明模板参数为 T, const T&, const T* typedef __list_iterator const_iterator;
其中 T 、 T& 、 T* 都是类型参数,当 list 的类型确定了,T 、 T& 、 T* 的类型自动确定,那么 iterator 的类型同时也自动确定了,这看起来就像 iterator 和 list 是共生的一样。有什么样的螺丝就需要什么样的螺丝刀,否则螺丝刀再漂亮,没有对号的螺丝也是没用的。
5.4 list 的一些成员函数 ① 默认成员函数
构造函数:
// 默认构造函数
list()
{
// 构造头节点,自己指向自己
_head = new Node;
_head->_prev = _head;
_head->_next = _head;
}
// 用迭代器区间初始化[first,last)
template
list(InputIterator first, InputIterator last)
:_head(new Node) // 当前对象是一个正在构造的对象,成员变量是随机值,需要进行初始化
{
// 建立头节点自身的链接
_head->_prev = _head;
_head->_next = _head;
// 用迭代器遍历元素,尾插到新容器中
while (first != last)
{
push_back(*first);
first++;
}
}
拷贝构造函数:传统写法和现代写法都可以
//拷贝构造函数(深拷贝),传统写法 // lt2(lt1) list(const list& lt) :_head(new Node) // 当前对象是一个正在构造的对象,成员变量是随机值,需要进行初始化 { // 建立头节点自身的链接 _head->_prev = _head; _head->_next = _head; // 把lt中所有节点拷贝插入到新容器中 for (const auto& e : lt) { push_back(e); } } // 拷贝构造函数(深拷贝),现代写法 // lt2(lt1) list(const list & lt) :_head(new Node) // 当前对象是一个正在构造的对象,成员变量是随机值,需要进行初始化 { // 建立头节点自身的链接 _head->_prev = _head; _head->_next = _head; list tmp(lt.begin(), lt.end()); std::swap(_head, tmp._head); // 交换两个容器的内容 }
赋值运算符重载函数(深拷贝),传统写法
list& operator=(const list & lt) { if (this != <) // 防止自己给自己赋值 { // 清除当前容器所有有效元素 clear(); // 把lt中所有节点拷贝插入到当前容器中 for (const auto& e : lt) { push_back(e); } } return *this; // 返回当前容器 }
赋值运算符重载函数(深拷贝),现代写法(赋值重载一般都是推荐现代写法,很方便,也很香)
list& operator=(list lt) // 传值传参,拷贝构造出临时对象 { std::swap(_head, lt._head); // 交换两个容器的内容 return *this; // 返回当前容器 }
析构函数:
~list()
{
clear();
delete _head;
_head = nullptr;
}
② 容量操作
size 函数:统计有效元素个数
size_t size() // 这是一个O(n)的接口,尽量少用
{
size_t n = 0; // 遍历整个链表,统计有效元素个数
iterator it = begin();
while (it != end())
{
n++;
it++;
}
return n;
}
empty 函数:判空
bool empty()
{
return begin() == end();
}
③ 修改操作
insert 函数:
iterator insert(iterator pos, const T& data)
{
Node* cur = pos._node; // 当前节点(pos中封装了节点的指针)
Node* prev = cur->_prev; // 前驱节点
Node* newnode = new Node(data); // 新节点
// 建立前驱节点和新节点的链接
prev->_next = newnode;
newnode->_prev = prev;
// 建立新节点与当前节点的链接
newnode->_next = cur;
cur->_prev = newnode;
// 返回指向第一个新插入元素的迭代器
return iterator(newnode); // 注意:此处调用iterator的构造函数构造出一个匿名对象
// return newnode; // 也可以这样写,因为单参数的构造函数支持隐式类型的转换
}
erase 函数:
iterator erase(iterator pos)
{
assert(pos != end()); // 不能删除头节点
Node* cur = pos._node; // 当前节点
Node* prev = cur->_prev; // 前驱节点
Node* next = cur->_next; // 后继节点
// 建立前驱节点与后继节点的链接
prev->_next = next;
next->_prev = prev;
// 删除当前节点
delete cur;
// 返回指向被删除节点的下一个节点的迭代器
return iterator(next); // 注意:此处调用iterator的构造函数构造出一个匿名对象
}
push_back 和 push_front 函数:
void push_back(const T& data) // 尾插
{
insert(end(), data); // 在头节点的前面插入,相当于是尾插
}
void push_front(const T& data) // 头插
{
insert(begin(), data); // 在第一个有效节点前面插入,相当于是头插
}
pop_back 和 pop_front 函数:
void pop_back() // 尾删
{
erase(--end());
}
void pop_front() // 头删
{
erase(begin());
}
clear 函数:清空容器中所有有效元素
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it); // 删除当前节点,返回指向下一个节点的迭代器
}
}
5.5 补充:类模板成员函数的实例化
void test4()
{
// 编译器进行类模板推演,实例化出一个 list 类,然后再实例化出这个 lt 对象
// 类模板中的成员函数都是函数模板
// 类模板成员函数的实例化,是按需实例化,只有当程序用到它时才进行实例化
// 一个模板,如果没有实例化,编译器是不会去检查它内部的语法的,也不会报错
list lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.clear();
}
六、list 和 vector 的对比
list 与 vector 都是 STL 中非常重要的序列式容器,由于两个容器的底层结构不同,导致其特性以及应用场景不同,其主要不同如下:
| vector | list | |
|---|---|---|
| 底层结构 | 动态顺序表,一段连续物理空间 | 带头结点的双向循环链表 |
| 随机访问 | 支持随机访问,访问某个元素效率 O(1) | 不支持随机访问,访问某个元素效率 O(N) |
| 插入和删除 | 任意位置插入和删除效率低,需要搬移元素,时间复杂度为 O(N),插入时有可能需要增容(经过开辟新空间,拷贝元素,释放旧空间等过程),导致效率更低 | 任意位置插入和删除效率高,不需要搬移元素,时间复杂度为 O(1) |
| 空间利用率 | 底层为连续空间,不容易造成内存碎片,空间利用率高,缓存命中率高 | 底层节点动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低 |
| 迭代器 | 原生态指针 | 对原生态指针(节点指针)进行封装 |
| 迭代器失效问题 | 在插入元素时,要给当前迭代器重新赋值,因为插入元素有可能会导致扩容,致使原来迭代器失效;删除时,当前迭代器也需要重新赋值,否则会失效 | 插入元素不会导致迭代器失效;删除元素时,只会导致当前迭代器失效,其他迭代器不受影响 |
| 使用场景 | 需要高效存储,支持随机访问,不关心插入删除效率的场景 | 大量插入和删除操作,不关心随机访问的场景 |



