C++基础:(十二)list类的基础使用

目录

前言

[一、list 基础介绍](#一、list 基础介绍)

[1.1 什么是 list?](#1.1 什么是 list?)

[1.2 list 的结构特性](#1.2 list 的结构特性)

[二、list 核心接口使用:从构造到元素操作](#二、list 核心接口使用:从构造到元素操作)

[2.1 list 的构造函数:初始化链表](#2.1 list 的构造函数:初始化链表)

[2.2 list 迭代器:遍历链表的 "钥匙"](#2.2 list 迭代器:遍历链表的 “钥匙”)

[2.3 list 容量接口:判断链表状态](#2.3 list 容量接口:判断链表状态)

[2.4 list 元素访问接口:获取头尾元素](#2.4 list 元素访问接口:获取头尾元素)

[2.5 list 元素修改接口:插入、删除与清空](#2.5 list 元素修改接口:插入、删除与清空)

[2.6 list 迭代器失效问题:删除操作的 "陷阱"](#2.6 list 迭代器失效问题:删除操作的 “陷阱”)

总结


前言

在 C++ 标准模板库(STL)中,序列式容器是我们日常开发中频繁使用的工具,而 list 作为其中极具特色的一员,凭借其独特的底层结构,在处理频繁插入、删除操作的场景中展现出显著优势。本文将从 list 的基础介绍与使用入手,深入剖析其底层原理,帮助大家全面掌握 list 的特性与适用场景,为实际开发中的容器选择提供清晰指引。下面就让我们正式开始吧!


一、list 基础介绍

1.1 什么是 list?

list 是 C++ STL 中的序列式容器,其底层实现是基于带头结点的环状双向链表(也称为双向循环链表)。这种结构意味着链表中的每个节点都包含三个核心部分:

  • 存储数据的区域(data
  • 指向前驱节点的指针(prev
  • 指向后继节点的指针(next

同时,链表还存在一个头结点 (又叫做哨兵节点),该节点不存储有效数据,仅用于简化链表的插入、删除操作逻辑 ------ 无需额外判断链表是否为空或操作位置是否为头尾,统一了所有位置的操作接口。

从上图的示例 **list<int> ilist;**可以看出,list 是一个模板类,支持存储任意数据类型(如 intstring、自定义结构体等),因此我们要在使用时需指定具体的数据类型,这体现了 STL 的泛型特性。

1.2 list 的结构特性

为了更直观地理解 list 的结构,我们结合上面的示意图进行拆解分析:

  • 正向遍历 :从 ilist.begin() 开始(指向第一个有效数据节点),通过节点的 next 指针依次访问后续节点,直到 ilist.end()(指向头结点,即最后一个有效节点的下一个位置)。
  • 逆向遍历 :从 ilist.rbegin()开始(指向最后一个有效数据节点,对应正向迭代器的end() 位置),通过节点的 prev 指针依次访问前驱节点,直到 ilist.rend()(指向头结点,对应正向迭代器的 begin() 位置)。
  • 环状特性 :头结点的 prev 指针指向最后一个有效节点,最后一个有效节点的 next 指针指向头结点,形成一个闭环,确保遍历过程中不会出现 "越界" 问题。

例如,如果 ilist 中存储的数据为 [0, 1, 2, 3, 4],那么其结构可表示为:

cpp 复制代码
头结点 <-> 0(begin()) <-> 1 <-> 2 <-> 3 <-> 4 <-> 头结点(end())

当我们使用 find(ilist.begin(), ilist.end(), 3) 查找数据 3 时,函数会从 begin() 开始遍历,最终返回指向数据 3 所在节点的迭代器。

二、list 核心接口使用:从构造到元素操作

list 提供了丰富的接口用于完成容器的初始化、遍历、容量判断、元素访问与修改等操作。掌握这些接口的正确用法是高效使用 list 的基础,以下将按功能分类详解关键接口,并结合代码示例说明。

2.1 list 的构造函数:初始化链表

list 提供了 4 种常用的构造方式,适用于不同的初始化场景,具体接口说明如下表所示:

构造函数原型 接口说明 适用场景
list (size_type n, const value_type& val = value_type()) 构造一个包含 n 个值为 val 的元素的 list 需要初始化固定数量、相同值的链表(如初始化 5 个 0)
list() 构造空的 list 初始时链表为空,后续通过插入操作添加元素
list (const list& x) 拷贝构造函数,用已有的 list x 构造新链表 需要创建一个与已有链表完全相同的副本
list (InputIterator first, InputIterator last) 用区间 [first, last) 中的元素构造 listfirstlast 为迭代器) 从其他容器(如数组、vector)中获取元素初始化链表

list 构造函数的使用示例如下:

cpp 复制代码
#include <list>
#include <iostream>
using namespace std;

int main() {
    // 1. 构造包含 5 个 10 的 list
    list<int> l1(5, 10);
    cout << "l1: ";
    for (auto it = l1.begin(); it != l1.end(); ++it) {
        cout << *it << " ";  // 输出:10 10 10 10 10
    }
    cout << endl;

    // 2. 构造空 list
    list<int> l2;
    cout << "l2 是否为空:" << (l2.empty() ? "是" : "否") << endl;  // 输出:是

    // 3. 拷贝构造(用 l1 构造 l3)
    list<int> l3(l1);
    cout << "l3: ";
    for (auto it = l3.begin(); it != l3.end(); ++it) {
        cout << *it << " ";  // 输出:10 10 10 10 10
    }
    cout << endl;

    // 4. 用数组区间构造
    int arr[] = {1, 2, 3, 4, 5};
    list<int> l4(arr, arr + 5);  // 区间 [arr, arr+5) 包含 arr[0] 到 arr[4]
    cout << "l4: ";
    for (auto it = l4.begin(); it != l4.end(); ++it) {
        cout << *it << " ";  // 输出:1 2 3 4 5
    }
    cout << endl;

    return 0;
}

2.2 list 迭代器:遍历链表的 "钥匙"

迭代器是连接容器与算法的桥梁,对于 list 而言,迭代器可以暂时理解为 "指向节点的指针",通过迭代器可以访问节点数据、移动到相邻节点。list 提供了正向迭代器和反向迭代器,满足不同遍历需求。

迭代器的接口说明如下:

函数声明 接口说明 遍历方向
begin() 返回指向第一个有效元素的正向迭代器 从前往后(++ 移动到下一个元素)
end() 返回指向最后一个有效元素的下一个位置(头结点)的正向迭代器 作为正向遍历的结束条件
rbegin() 返回指向最后一个有效元素 的反向迭代器(对应正向迭代器的 end() 位置) 从后往前(++ 移动到前一个元素)
rend() 返回指向第一个有效元素的前一个位置 (头结点)的反向迭代器(对应正向迭代器的 begin() 位置) 作为反向遍历的结束条件

在使用迭代器时,我们需要注意以下事项:

  1. 正向迭代器操作 :对 begin() 返回的迭代器执行 ++ 操作,会移动到下一个有效元素;执行 * 操作,会获取当前节点的元素值。
  2. 反向迭代器操作 :对 rbegin() 返回的迭代器执行 ++ 操作,会移动到前一个有效元素(本质是正向迭代器的 -- 操作);执行 * 操作,同样获取当前节点的元素值。
  3. 迭代器范围 :STL 中所有迭代器相关的算法(如 findfor_each)均遵循 "左闭右开" 原则,即范围 [first, last) 包含 first 指向的元素,不包含 last 指向的元素。

list 迭代器的使用示例如下所示:

cpp 复制代码
#include <list>
#include <iostream>
#include <algorithm>  // 包含 find 函数
using namespace std;

int main() {
    list<int> l = {1, 2, 3, 4, 5};

    // 1. 正向遍历:begin() + end()
    cout << "正向遍历 l: ";
    for (auto it = l.begin(); it != l.end(); ++it) {
        cout << *it << " ";  // 输出:1 2 3 4 5
    }
    cout << endl;

    // 2. 反向遍历:rbegin() + rend()
    cout << "反向遍历 l: ";
    for (auto rit = l.rbegin(); rit != l.rend(); ++rit) {
        cout << *rit << " ";  // 输出:5 4 3 2 1
    }
    cout << endl;

    // 3. 用迭代器查找元素(find 函数)
    auto it = find(l.begin(), l.end(), 3);
    if (it != l.end()) {
        cout << "找到元素 3,其下一个元素为:" << *(++it) << endl;  // 输出:4
    } else {
        cout << "未找到元素 3" << endl;
    }

    return 0;
}

2.3 list 容量接口:判断链表状态

list 提供了 2 个核心容量接口,用于判断链表是否为空和获取有效元素个数,接口说明如下:

函数声明 接口说明 返回值类型
empty() 检测 list 是否为空(有效元素个数为 0) bool(空返回 true,非空返回 false
size() 返回 list 中有效元素的个数 size_type(无符号整数类型,通常为 unsigned int

list 容量接口的使用示例如下:

cpp 复制代码
#include <list>
#include <iostream>
using namespace std;

int main() {
    list<int> l;

    // 初始为空
    cout << "初始时,l 是否为空:" << (l.empty() ? "是" : "否") << endl;  // 输出:是
    cout << "初始时,l 的大小:" << l.size() << endl;  // 输出:0

    // 插入元素后
    l.push_back(1);
    l.push_back(2);
    l.push_back(3);
    cout << "插入 3 个元素后,l 是否为空:" << (l.empty() ? "是" : "否") << endl;  // 输出:否
    cout << "插入 3 个元素后,l 的大小:" << l.size() << endl;  // 输出:3

    // 清空元素后
    l.clear();
    cout << "清空后,l 是否为空:" << (l.empty() ? "是" : "否") << endl;  // 输出:是
    cout << "清空后,l 的大小:" << l.size() << endl;  // 输出:0

    return 0;
}

2.4 list 元素访问接口:获取头尾元素

由于 list 底层是链表结构,不支持随机访问(如 vector[] 运算符),因此仅提供了获取第一个元素最后一个元素的接口,避免用户通过下标访问导致效率低下(链表下标访问需遍历整个链表,时间复杂度为 O (N))。

函数声明 接口说明 返回值类型
front() 返回 list 第一个有效节点中元素的引用 value_type&(可修改元素值)
back() 返回 list 最后一个有效节点中元素的引用 value_type&(可修改元素值)

list 元素访问接口的使用示例如下:

cpp 复制代码
#include <list>
#include <iostream>
using namespace std;

int main() {
    list<int> l = {10, 20, 30, 40};

    // 获取头尾元素
    cout << "第一个元素:" << l.front() << endl;  // 输出:10
    cout << "最后一个元素:" << l.back() << endl;  // 输出:40

    // 修改头尾元素(通过引用)
    l.front() = 100;
    l.back() = 400;
    cout << "修改后,第一个元素:" << l.front() << endl;  // 输出:100
    cout << "修改后,最后一个元素:" << l.back() << endl;  // 输出:400

    // 遍历验证
    cout << "修改后的链表:";
    for (auto it = l.begin(); it != l.end(); ++it) {
        cout << *it << " ";  // 输出:100 20 30 400
    }
    cout << endl;

    return 0;
}

2.5 list 元素修改接口:插入、删除与清空

函数声明 接口说明 时间复杂度
push_front(const value_type& val) 在链表头部 插入值为 val 的元素 O(1)
pop_front() 删除链表头部的第一个有效元素 O (1)(需注意链表不能为空)
push_back(const value_type& val) 在链表尾部 插入值为 val 的元素 O(1)
pop_back() 删除链表尾部的最后一个有效元素 O (1)(需注意链表不能为空)
insert(iterator pos, const value_type& val) 在迭代器 pos 指向的位置之前 插入值为 val 的元素 O (1)(需先通过迭代器找到 pos 位置,查找过程为 O (N))
erase(iterator pos) 删除迭代器 pos 指向的元素 O (1)(同理,查找 pos 位置为 O (N))
swap(list& x) 交换当前链表与 x 中的所有元素 O (1)(仅交换链表的头指针等核心成员,不交换元素本身)
clear() 清空链表中的所有有效元素(头结点保留) O (N)(需遍历所有节点释放内存)

list 元素修改接口的使用示例如下所示:

cpp 复制代码
#include <list>
#include <iostream>
using namespace std;

int main() {
    list<int> l;

    // 1. 头部/尾部插入(push_front / push_back)
    l.push_back(20);    // 链表:[20]
    l.push_front(10);   // 链表:[10, 20]
    l.push_back(30);    // 链表:[10, 20, 30]
    cout << "插入后链表:";
    for (auto it = l.begin(); it != l.end(); ++it) {
        cout << *it << " ";  // 输出:10 20 30
    }
    cout << endl;

    // 2. 头部/尾部删除(pop_front / pop_back)
    l.pop_front();      // 链表:[20, 30]
    l.pop_back();       // 链表:[20]
    cout << "删除后链表:";
    for (auto it = l.begin(); it != l.end(); ++it) {
        cout << *it << " ";  // 输出:20
    }
    cout << endl;

    // 3. 任意位置插入(insert)
    auto it = l.begin();  // it 指向 20
    l.insert(it, 15);     // 在 20 之前插入 15,链表:[15, 20]
    cout << "insert 后链表:";
    for (auto it = l.begin(); it != l.end(); ++it) {
        cout << *it << " ";  // 输出:15 20
    }
    cout << endl;

    // 4. 任意位置删除(erase)
    it = l.begin();        // it 指向 15
    l.erase(it);           // 删除 15,链表:[20]
    cout << "erase 后链表:";
    for (auto it = l.begin(); it != l.end(); ++it) {
        cout << *it << " ";  // 输出:20
    }
    cout << endl;

    // 5. 交换链表(swap)
    list<int> l2 = {100, 200, 300};
    cout << "交换前 l2:";
    for (auto it = l2.begin(); it != l2.end(); ++it) {
        cout << *it << " ";  // 输出:100 200 300
    }
    cout << endl;

    l.swap(l2);
    cout << "交换后 l:";
    for (auto it = l.begin(); it != l.end(); ++it) {
        cout << *it << " ";  // 输出:100 200 300
    }
    cout << endl;
    cout << "交换后 l2:";
    for (auto it = l2.begin(); it != l2.end(); ++it) {
        cout << *it << " ";  // 输出:20
    }
    cout << endl;

    // 6. 清空链表(clear)
    l.clear();
    cout << "clear 后 l 是否为空:" << (l.empty() ? "是" : "否") << endl;  // 输出:是

    return 0;
}

2.6 list 迭代器失效问题:删除操作的 "陷阱"

迭代器失效是使用 list 时最容易踩的 "坑" 之一。所谓迭代器失效,是指迭代器指向的节点已被删除,此时再使用该迭代器(如 ++*)会导致未定义行为(程序崩溃、结果异常等)。

由于 list 底层是双向循环链表,因此其迭代器失效是具有明确的规律的:

  • 插入操作 :不会导致任何迭代器失效。插入新节点时,仅修改相邻节点的 prevnext 指针,原有节点的地址不变,因此指向原有节点的迭代器依然有效。
  • 删除操作 :仅导致指向被删除节点的迭代器失效,其他迭代器(指向未删除节点的)不受影响。因为删除节点后,该节点的内存被释放,迭代器指向的地址变为 "野地址",无法再使用。

下面给大家看一个迭代器失效导致崩溃的错误示例:

cpp 复制代码
#include <list>
#include <iostream>
using namespace std;

void TestInvalidIterator() {
    int arr[] = {1, 2, 3, 4, 5};
    list<int> l(arr, arr + 5);
    auto it = l.begin();

    // 错误写法:erase 后 it 已失效,再执行 ++it 会导致未定义行为
    while (it != l.end()) {
        l.erase(it);  // erase 后,it 指向的节点被删除,it 失效
        ++it;         // 对失效的迭代器执行 ++,程序可能崩溃
    }
}

int main() {
    TestInvalidIterator();  // 运行时可能崩溃
    return 0;
}

正确写法应该是:使用erase 返回值重置迭代器

listerase 函数有一个重要特性:删除节点后,会返回指向被删除节点的下一个节点的迭代器 。因此,我们可以通过 it = l.erase(it) 的方式重置迭代器,避免失效问题。此外,也可以通过 l.erase(it++) 实现(先将 it 传入 erase,再执行 it++,此时传入的是删除前的 itit 自身已指向 next 节点)。如下所示:

cpp 复制代码
#include <list>
#include <iostream>
using namespace std;

void TestValidIterator() {
    int arr[] = {1, 2, 3, 4, 5};
    list<int> l(arr, arr + 5);
    auto it = l.begin();

    // 正确写法 1:用 erase 的返回值重置迭代器
    while (it != l.end()) {
        it = l.erase(it);  // erase 返回下一个节点的迭代器,重置 it
    }
    cout << "正确写法 1 后,l 是否为空:" << (l.empty() ? "是" : "否") << endl;  // 输出:是

    // 重新初始化链表,测试正确写法 2
    l.assign(arr, arr + 5);
    it = l.begin();
    while (it != l.end()) {
        l.erase(it++);  // 先传 it 给 erase,再 it++(指向 next 节点)
    }
    cout << "正确写法 2 后,l 是否为空:" << (l.empty() ? "是" : "否") << endl;  // 输出:是
}

int main() {
    TestValidIterator();  // 正常运行,无崩溃
    return 0;
}

总结

本期博客为大家介绍了list容器的基础概念、底层原理以及核心接口的使用,希望能够对大家有所帮助!下期博客我将为大家介绍list容器的模拟实现,请大家多多关注!

相关推荐
ONE_PUNCH_Ge6 小时前
Go 语言变量
开发语言
幼稚园的山代王6 小时前
go语言了解
开发语言·后端·golang
晚风残6 小时前
【C++ Primer】第六章:函数
开发语言·c++·算法·c++ primer
满天星83035776 小时前
【C++】AVL树的模拟实现
开发语言·c++·算法·stl
weixin_456904276 小时前
基于.NET Framework 4.0的串口通信
开发语言·c#·.net
ss2736 小时前
手写MyBatis第107弹:@MapperScan原理与SqlSessionTemplate线程安全机制
java·开发语言·后端·mybatis
Mr_WangAndy7 小时前
C++设计模式_行为型模式_责任链模式Chain of Responsibility
c++·设计模式·责任链模式·行为型模式
麦麦鸡腿堡7 小时前
Java的动态绑定机制(重要)
java·开发语言·算法
时间之里7 小时前
【c++】:Lambda 表达式介绍和使用
开发语言·c++