> 技术文档 > 【C++ 】STL详解(五)—玩转 list,你真的懂了吗?

【C++ 】STL详解(五)—玩转 list,你真的懂了吗?


坚持用 清晰易懂的图解 + 代码语言,让每个知识点变得简单!
🚀呆头个人主页详情
🌱 呆头个人Gitee代码仓库
📌 呆头详细专栏系列
座右铭: “不患无位,患所以立。”在这里插入图片描述


【C++ 】STL详解(五)—玩转 list,你真的懂了吗?

  • list文档参考----------请点击
  • 摘要
  • 目录
  • 一、`list`的介绍
  • 二、`list`的使用
    • 1.构造函数
    • 2.特殊成员函数
      • 析构函数`~list`
      • 赋值运算符重载`operator=`
    • 3.迭代器的使用
      • `begin` 和 `end`
      • `rbegin` 和 `rend`
    • 4.容器相关函数
      • `size`和`empty`
    • 5.修改容器内容相关函数
      • `push_front()` 和 `emplace_front()`
      • `emplace_back()`- `push_back()`
      • `insert()` 和 `emplace()`
      • `pop_front()`和 `pop_back()`
      • `erase()`和 `clear()`
      • `resize`
      • `assign`
      • `swap`
    • 6.list的相关操作函数
      • `sort()`
      • 2. **`remove()`**
      • 3. **`unique()`**
      • 4. **`merge()`**
      • 5. **`reserve()`**

list文档参考----------请点击


摘要

🚀 欢迎来到《C++修炼之路》!

这里是C++程序员的成长乐园,带你领略从面向对象到现代C++的精彩世界。我们将>用简洁的代码和生动的案例,助你掌握C++核心精髓。

🔍 专栏亮点:

  • 现代C++特性解析(C++11/14/17)
  • STL源码剖析与实战应用
  • 内存管理与性能优化技巧

💡 收获预期:
✔️ 写出更健壮的C++代码
✔️ 深入理解面向对象设计
✔️ 掌握模板编程基础

📌 编程箴言:

“好的C++代码就像好酒,需要时间沉淀。”

(正文开始👇)---- 本文学习list的使用


目录

一、list的介绍

【C++ 】STL详解(五)—玩转 list,你真的懂了吗?

  1. list 是C++标准库中一个序列式容器,它采用双向链表结构,使得在常数时间内能够在任意位置进行插入和删除操作。这一特性使得list在需要频繁修改数据的场景中,表现得尤为高效。与vectordeque这类基于数组的容器相比,list能够更轻松地应对复杂的数据操作,尤其是插入和删除数据。
  2. list 的底层结构是双向链表,每个元素(节点)都存储在独立的内存空间中,并且通过两个指针相互关联:一个指向前一个节点,另一个指向后一个节点。这种结构使得list能够灵活地在任意位置插入或删除元素,而不需要像vector那样频繁地移动其他元素。
  3. listforward_list 有些相似,二者的主要区别在于链表的类型:list 是双向链表,而 forward_list 是单向链表。这意味着 forward_list 仅能进行单方向的迭代(从前到后),而 list 支持双向迭代,可以从前向后和从后向前遍历。这一差异使得 list 在某些需要双向遍历的场景中,显得更加灵活和高效。

优点:

  1. 高效的插入和删除操作:在 list 中,插入和删除元素的时间复杂度是 O(1),无论是在容器的头部、尾部,还是在中间位置,这使得它在频繁进行数据修改时,具有明显的性能优势。
  2. 双向迭代list 支持双向迭代,可以从前到后,也可以从后到前,这对于某些需要逆向遍历的操作非常方便。

缺点:

  1. 缺乏随机访问:与 vectordeque 不同,list 不支持通过下标来访问元素,这意味着不能像数组那样直接访问任意位置的元素。这是它的一大局限,尤其是在需要快速随机访问元素的场景下。
  2. 额外的内存开销list 需要为每个元素存储前后指针,这会导致比其他容器(如 vector)消耗更多的内存。对于存储小型数据的容器来说,这一额外的空间开销可能是一个不可忽视的问题。

二、list的使用

1.构造函数

【C++ 】STL详解(五)—玩转 list,你真的懂了吗?
让我们如上图的构造进行解释:

explicit list(const allocator_type& alloc = allocator_type());----默认构造函数

这个构造函数创建一个空的 list,即没有任何元素。它接受一个 allocator_type 类型的参数,这个参数用于自定义内存分配策略,默认为标准分配器 allocator_type()。(免了频繁的向堆上申请空间,提高效率)

explicit list(size_type n, const value_type& val = value_type(),
const allocator_type& alloc = allocator_type());----填充构造函数

该构造函数会创建一个含有 n 个元素的 list,并且所有元素的初始值都为 val(默认为类型 value_type 的默认值)。同样地,您可以选择使用自定义的分配器来管理内存。

template
list(InputIterator first, InputIterator last, const allocator_type& alloc = allocator_type());----范围构造函数

此构造函数接受两个迭代器,first 和 last,并根据这两个迭代器所指向的范围来初始化 list 中的元素。这种构造函数通常用于通过已有容器或数组来初始化 list。

list(const list& x);----拷贝构造函数

该构造函数用于创建一个与已有 list 完全相同的新 list,也就是说,所有的元素和分配器都会被复制到新的容器中。

#include#includeusing namespace std;void test01(){list<int> l1; //默认构造list<int> l2(6, 6); //填充构造list<int> l3(l2.begin(), l2.end()); //迭代器范围构造int arr[] = { 5,2,0,1,3,1,4 };//数组初始化范围构造list<int> l4(arr, arr + 7);list<int> l5(l2);//拷贝构造}int main(){test01();return 0;}

运行结果如下:
【C++ 】STL详解(五)—玩转 list,你真的懂了吗?


2.特殊成员函数

析构函数~list

  • 析构函数 是类中的一个特殊成员函数,它在对象生命周期结束时自动调用。其主要作用是释放对象所占用的资源。
  • 对于 std::list 来说,析构函数会在 list 对象销毁时自动释放它所管理的内存,包括容器中的每个元素(即每个节点)所占的内存。

赋值运算符重载operator=

赋值运算符重载(operator=) 允许我们定义如何将一个对象赋值给另一个对象。它是在 C++ 中非常常见的操作,尤其是在处理对象之间的赋值时。对于 std::list 等容器类来说,赋值运算符重载的目标是确保容器内部的元素被正确地复制。【C++ 】STL详解(五)—玩转 list,你真的懂了吗?


3.迭代器的使用

begin(), end(), rbegin(), rend() 用于获取 std::list 容器的开头或结尾位置的迭代器。要接收迭代器时,通常使用 iterator 类型,它是 std::list 类内部定义的类型。由于 iterator 是在类内部通过 typedef 定义的,它属于类的成员类型,因此在使用时需要使用 :: 来指定类的域,明确指定是哪个类的迭代器。

值得注意的是,迭代器区间是左闭右开区间的,即区间包含起始迭代器指向的元素,但不包含结束迭代器指向的元素。

由于 std::list 支持迭代器,范围 for 循环的原理是被编译器转换为迭代器循环,因此它也可以用于 list 中。与直接使用迭代器相比,范围 for 的功能稍显有限:它只能实现正向遍历,而迭代器则支持正向和反向遍历(通过 rbegin()rend())。


beginend

int arr[] = { 5,2,0,1,3,1,4};list<int> l1(arr, arr + 7);list<int>::iterator it = l1.begin();while (it != l1.end()){cout << *it << \' \';it++;}cout << endl;

rbeginrend

int arr[] = { 5,2,0,1,3,1,4};list<int> l1(arr, arr + 7);list<int>::reverse_iterator rit = l1.rbegin();while (rit != l1.rend()){cout << *rit << \' \';rit++;}cout << endl;

4.容器相关函数

sizeempty

size()获取当前容器有效数据个数,然后empty通过bool判断容器是否为空

list<int> l1;list<int> l2(6, 6);cout << \"l1 size: \" << l1.size() << endl;cout << \"l2 size: \" << l2.size() << endl;cout << \"l1 : \" << l1.empty() << endl;cout << \"l2 : \" << l2.empty() << endl;

【C++ 】STL详解(五)—玩转 list,你真的懂了吗?


5.修改容器内容相关函数

push_front()emplace_front()

  • push_front():将一个元素添加到容器的前端。这个元素是通过拷贝或移动构造的。
  • emplace_front():直接在容器前端原地构造元素,避免了拷贝或移动操作,效率较高,尤其是对于复杂类型。
int arr[] = { 5,2,0,1,3,1,4};int arr1[] = { 1,3,1,4,5,2,0 };list<int> l1(arr, arr + 7);list<int> l2(arr1, arr1 + 7);l1.emplace_front(6);l2.push_front(100);

emplace_back()- push_back()

  • push_back():将一个元素添加到容器的末端,通过拷贝或移动。
  • emplace_back():直接在容器的末端构造元素,避免拷贝或移动,效率更高。
int arr[] = { 5,2,0,1,3,1,4};int arr1[] = { 1,3,1,4,5,2,0 };list<int> l1(arr, arr + 7);list<int> l2(arr1, arr1 + 7);l1.emplace_back(6);l2.push_back(100);

insert()emplace()

  • insert()
  1. 迭代器不失效

std::list 中,insert() 操作不会导致迭代器失效,因为插入只是修改了节点间的链接关系。

  1. 插入单个元素

insert() 可以在指定位置之前插入一个元素,返回指向新插入元素的迭代器。

  1. 插入多个相同元素

可以在指定位置之前插入多个相同的元素,返回指向第一个新插入元素的迭代器。

  1. 插入区间

可以在指定位置之前插入一个迭代器区间中的所有元素,返回指向第一个新插入元素的迭代器。

  1. 位置迭代器的要求

若要在特定位置(非开头或结尾)插入,必须提前将迭代器移动到该位置。

  • emplace():与 insert() 相似,但在指定位置 直接构造 元素,避免了拷贝操作。
int arr[] = { 5,2,0,1,3,1,4};int arr1[] = { 1,3,1,4,5,2,0 };list<int> l1(arr, arr + 7);list<int> l2(arr1, arr1 + 7);list<int> l3(arr, arr + sizeof(arr) / sizeof(arr[0])); //初始化数组范围构造list<int>::iterator pos = l3.begin(); //开始位置 5int flag = 1;for (int i = 0; i < flag; i++){pos++; //pos位置变成2}l3.insert(pos, 0);//在2的位置插入0,其他数据后移for (auto e : l3){cout << e << \' \';}cout << endl;l3.insert(pos, 5, 6); //50201314 pos的值为2,插入5个6 for (auto e : l3){cout << e << \' \';}cout << endl;list<int> lt4(6, 5);l3.insert(pos, lt4.begin(), lt4.end());//把l4的数据6个5从pos位置插入for (auto e : l3){cout << e << \' \';}cout << endl;

pop_front()pop_back()

  • pop_front():删除容器前端的元素。
  • pop_back():删除容器末端的元素。
  • 这两者的差别在于删除的位置不同,前者删除的是第一个元素,后者删除的是最后一个元素。
list<int> l1;l1.push_back(0);l1.push_back(1);l1.push_back(2);l1.push_back(3);for (auto e : l1){cout << e << \" \";}cout << endl; //0 1 2 3lt.pop_back();lt.pop_back();for (auto e : l1){cout << e << \" \";}cout << endl;//0 1

erase()clear()

  • erase()
  1. 迭代器失效

std::list 中,erase() 会删除指定迭代器指向的节点,并改变节点之间的链接关系。因此,删除的节点的迭代器会失效,再次使用会导致错误。

  1. 返回有效迭代器

erase() 会返回一个有效的迭代器,指向被删除节点的下一个节点。如果继续操作容器,应该使用 erase() 返回的迭代器,而不是已失效的迭代器。

  1. 删除单个元素

erase() 可以删除指定迭代器位置的单个元素,操作完成后返回下一个位置的迭代器。

  1. 删除区间元素

erase() 还可以删除一个迭代器区间内的所有元素,操作完成后返回删除区域后一个位置的迭代器。

  1. 避免迭代器失效

删除元素后,继续操作时要使用 erase() 返回的新迭代器,而不是旧的、已经失效的迭代器。

  • clear():删除容器中的所有元素,使容器变为空。
int arr[] = { 5,2,0,1,3,1,4};list<int> l1(arr, arr + sizeof(arr) / sizeof(arr[0]));for (auto e : l1){cout << e << \' \';}cout << endl;l1.erase(--l1.end());//end返回的是最后一个数据的下一个位置的迭代器//所以删除最后一个数据所以需要--end使迭代器指向最后一个位置的数据for (auto e : l1){cout << e << \' \';}cout << endl;//5 2 0 1 3 1l1.clear(); //清空容器for (auto e : l1){cout << e << \" \";}cout << endl; //(无数据)cout << l1.size() << endl; //0

resize

  1. resize是用于改变list有效数据的大小的,如果n小于当前大小,那么会将多余的部分删除销毁
  2. 如果n大于当前大小,那么会尾插val,如果用户没有显示给出val,那么会使用缺省值,即默认构造生成的值。
list<int> l1(5, 3);for (auto e : l1){cout << e << \" \";}cout << endl; //3 3 3 3 3l1.resize(7, 6); //将size扩大为7,扩大的值为6for (auto e : l1){cout << e << \" \";}cout << endl; //3 3 3 3 3 6 6l1.resize(2); //将size缩小为2for (auto e : l1){cout << e << \" \";}cout << endl; //3 3}

assign

  1. 清空原数据assign() 会清空容器中的所有原数据,再进行数据替换。
  2. 使用迭代器区间替代数据:可以通过传入一个迭代器区间来替代容器中的数据,替换后容器大小调整。
  3. 使用相同值替代数据:也可以通过指定值和重复次数来填充容器,容器中将包含指定数量的相同元素。
  4. 迭代器区间要求:传入的区间是前闭后开的(即包括起始位置,不包括结束位置)。
int arr[] = { 5,2,0,1,3,1,4 };list<int> l1(arr, arr + 7);list<int> l2;l2.assign(l1.begin(), l1.end());for (auto e : l2){cout << e << \' \';}cout << endl;//5201314

swap

交换两个容器的数据

list<int> l1(4, 2);list<int> l2(4, 6);lt1.swap(l2); //交换两个容器的内容for (auto e : l1){cout << e << \" \";}cout << endl; //6 6 6 6for (auto e : l2){cout << e << \" \";}cout << endl; //2 2 2 2}

6.list的相关操作函数

sort()

sort() 函数用于对 std::list 中的元素进行排序。排序的默认顺序是从小到大,也可以传入一个自定义比较函数来进行自定义排序。

注意事项*:

  • sort() 会修改 list 内部元素的顺序,原有的顺序将丢失。
  • 它使用的是双向链表的内部结构进行排序,因此时间复杂度是 O(n log n),其中 n 是元素的个数。
  • 默认排序是基于 < 运算符进行的,若需要自定义排序,必须提供一个比较函数或函数对象。
list<int> lt;lt.push_back(4);lt.push_back(7);lt.push_back(5);lt.push_back(9);lt.push_back(6);lt.push_back(0);lt.push_back(3);for (auto e : lt){cout << e << \" \";}cout << endl; //4 7 5 9 6 0 3lt.sort(); //默认将容器内数据排为升序for (auto e : lt){cout << e << \" \";}cout << endl; //0 3 4 5 6 7 9return 0;

2. remove()

remove() 函数用于移除 std::list 中所有等于指定值的元素。

注意事项

  • remove() 不会改变列表的顺序,只会删除匹配的元素。
  • 删除的元素是通过值来匹配的,如果你需要根据条件删除元素,可以考虑使用 remove_if()
  • 该函数是原地修改容器的,操作后迭代器指向的元素会失效。
list<int> lt;lt.push_back(1);lt.push_back(4);lt.push_back(3);lt.push_back(3);lt.push_back(2);lt.push_back(2);lt.push_back(3);for (auto e : lt){cout << e << \" \";}cout << endl; //1 4 3 3 2 2 3lt.remove(3); //删除容器当中值为3的元素for (auto e : lt){cout << e << \" \";}cout << endl; //1 4 2 2

3. unique()

unique() 函数用于删除连续的重复元素。它会删除相邻的重复元素,只保留一个副本。

注意事项

  • unique() 只会删除相邻的重复元素,因此需要先对 list 进行排序,以确保相同的元素是相邻的。
  • 如果希望自定义判断条件,可以传入一个二元函数来判断元素是否相等。
list<int> lt;lt.push_back(1);lt.push_back(4);lt.push_back(3);lt.push_back(3);lt.push_back(2);lt.push_back(2);lt.push_back(3);for (auto e : lt){cout << e << \" \";}cout << endl; //1 4 3 3 2 2 3lt.sort(); //将容器当中的元素排为升序lt.unique(); //删除容器当中连续的重复元素for (auto e : lt){cout << e << \" \";}cout << endl; //1 2 3 4

4. merge()

merge() 函数用于将两个已排序的 std::list 合并为一个新的排序好的 std::list。合并时,元素会按照排序顺序被放入目标列表中。

注意事项

  • merge() 要求两个 list 都是已排序的,否则行为不可预测。
  • 合并后,list2 将为空,所有的元素都会移动到 list1 中。
  • 如果需要自定义排序顺序,可以传入一个比较函数。
list<int> lt1;lt1.push_back(3);lt1.push_back(8);lt1.push_back(1);list<int> lt2;lt2.push_back(6);lt2.push_back(2);lt2.push_back(9);lt2.push_back(5);lt1.sort(); //将容器lt1排为升序lt2.sort(); //将容器lt2排为升序lt1.merge(lt2); //将lt2合并到lt1当中for (auto e : lt1){cout << e << \" \";}cout << endl; //1 2 3 5 6 8 9 

5. reserve()

reserve() 函数用于为 std::list 容器预分配空间。然而,std::list 并不直接支持 reserve(),因为它是基于链表实现的,链表的内存分配是动态的。因此,它没有类似于 std::vector 中的 reserve() 方法。

注意事项

  • 对于链表容器 std::list,你无法预先分配空间,reserve() 仅适用于基于连续内存的容器,如 std::vector
  • std::list 不支持 reserve(),因此无法提前分配空间。容器会根据需要动态分配空间。

📢 如果你也喜欢这种“不呆头”的技术风格:
👁️ 【关注】 看一个非典型程序员如何用野路子解决正经问题
👍 【点赞】 给“不写八股文”的技术分享一点鼓励
🔖 【收藏】 把这些“奇怪但有用”的代码技巧打包带走
💬 【评论】 来聊聊——你遇到过最“呆头”的 Bug 是啥?
🗳️ 【投票】 您的投票是支持我前行的动力
技术没有标准答案,让我们一起用最有趣的方式,写出最靠谱的代码! 🎮💻