从零开始的C++学习生活 8:list的入门使用

个人主页:Yupureki-CSDN博客

C++专栏:C++_Yupureki的博客-CSDN博客

目录

前言

[1. list简介](#1. list简介)

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

[1.2 list的底层结构](#1.2 list的底层结构)

[2. list的基本使用](#2. list的基本使用)

[2.1 构造list](#2.1 构造list)

[2.2 迭代器使用](#2.2 迭代器使用)

[2.3 容量操作](#2.3 容量操作)

[2.4 元素访问](#2.4 元素访问)

[2.5 修改操作](#2.5 修改操作)

[2.6 list特有操作](#2.6 list特有操作)

[3. 迭代器失效问题](#3. 迭代器失效问题)

[3.1 安全的操作(不会导致迭代器失效)](#3.1 安全的操作(不会导致迭代器失效))

[3.2 危险的操作(会导致当前迭代器失效)](#3.2 危险的操作(会导致当前迭代器失效))

[3.3 批量删除的正确方式](#3.3 批量删除的正确方式)

[4. list的模拟实现](#4. list的模拟实现)

[4.1 节点结构](#4.1 节点结构)

[4.2 list基础框架](#4.2 list基础框架)

[4.3 反向迭代器实现](#4.3 反向迭代器实现)

[5. list与vector的对比](#5. list与vector的对比)

选择指南:

[6. 性能优化建议](#6. 性能优化建议)


上一篇:从零开始的C++学习生活 7:vector的入门使用-CSDN博客

前言

在C++标准模板库(STL)中,list是一个基于双向链表实现的序列容器,与vector的连续内存布局形成鲜明对比。虽然vector因其随机访问特性而广受欢迎,但在某些场景下,list的插入删除效率和无序存储特性使其成为不可替代的选择。

list中每个元素独立存储,通过指针相互连接。这种结构使得在任意位置的插入和删除操作都能在常数时间内完成,但代价是失去了随机访问的能力。

本文将深入探讨list的各个方面,从基本使用到底层实现,帮助你理解何时以及如何正确使用这个重要的容器。

1. list简介

1.1 什么是list?

list是C++标准库中的一个序列容器,它基于带头节点的双向循环链表实现。主要特点包括:

  • 双向遍历:支持从前向后和从后向前的遍历

  • 高效插入删除:在任意位置插入删除元素的时间复杂度为O(1)

  • 非连续存储:元素在内存中分散存储,无扩容开销

  • 迭代器稳定性:插入操作不会使迭代器失效

1.2 list的底层结构

简易的list由3个类构成:list本体(集成功能函数,头节点和节点个数),listnode(单个list的节点),iterator(list专属的迭代器)

cpp 复制代码
template<class T>
class List {//list本体的结构
	typedef ListNode<T> Node;//list节点
	typedef List_Iterator<T> iterator;//迭代器
public:
    //函数实现
    ......
private:
    Node* head;//链表的哨兵位
    size_t size;//链表的有效结点个数(不包含哨兵位)
};
cpp 复制代码
// list的节点结构示意
struct ListNode {
    T data;           // 存储的数据
    ListNode* prev;   // 指向前驱节点
    ListNode* next;   // 指向后继节点
};
cpp 复制代码
//list的迭代器
template<class T>
struct List_Iterator {
	typedef ListNode<T> Node;
	typedef List_Iterator iterator;
	Node* node;
	......
};

list的整体结构是一个带头节点的双向循环链表

  • 头节点的prev指向尾节点

  • 尾节点的next指向头节点

  • 头节点不存储实际数据

为什么要集成list的迭代器?

由于物理空间的特性,list不像string和vector是线性的,普通的++,--,*和其他操作符无法直接用在节点的指针上,而为了保持统一,例如我们当然也得实现list的迭代器++指向下一个节点,*表示该节点的值

2. list的基本使用

2.1 构造list

list和srting,vector的构造极为相似

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

// 1. 默认构造 - 空list
list<int> l1;

// 2. 构造包含n个val的list
list<int> l2(5, 10);  // {10, 10, 10, 10, 10}

// 3. 拷贝构造
list<int> l3(l2);     // 与l2相同

// 4. 使用迭代器范围构造
int arr[] = {1, 2, 3, 4, 5};
list<int> l4(arr, arr + 5);  // {1, 2, 3, 4, 5}

// 5. 初始化列表构造 (C++11)
list<int> l5 = {1, 2, 3, 4, 5};

2.2 迭代器使用

集成后的迭代器可像string和vector那样使用

cpp 复制代码
list<int> lst = {1, 2, 3, 4, 5};

// 正向迭代器
cout << "正向遍历: ";
for (auto it = lst.begin(); it != lst.end(); ++it) {
    cout << *it << " ";  // 1 2 3 4 5
}

// 反向迭代器
cout << "\n反向遍历: ";
for (auto rit = lst.rbegin(); rit != lst.rend(); ++rit) {
    cout << *rit << " ";  // 5 4 3 2 1
}

// 范围for循环
cout << "\n范围for: ";
for (auto& elem : lst) {
    cout << elem << " ";
}

注意 :list的迭代器是双向迭代器 ,不支持随机访问,不能进行it + n操作。

2.3 容量操作

list没有capacity容量的概念,只有size有效节点个数的概念

cpp 复制代码
list<int> lst = {1, 2, 3};

cout << lst.size();        // 元素个数: 3
cout << lst.empty();       // 是否为空: false
cout << lst.max_size();    // 理论最大容量

// list没有capacity概念,因为不需要预分配空间

2.4 元素访问

list和string,vector不同的是,list不是线性的,不能使用[]下标来访问数据(实际上也可以实现,但不觉的别扭?链表用[]来访问?)

cpp 复制代码
list<int> lst = {1, 2, 3, 4, 5};

// 访问首尾元素
cout << lst.front();      // 1
cout << lst.back();       // 5

// 注意:list不支持下标访问!
// cout << lst[0];       // 错误!编译不通过

2.5 修改操作

list的增删查改与string,vector也基本一致

cpp 复制代码
list<int> lst = {1, 2, 3};

// 头部操作
lst.push_front(0);        // {0, 1, 2, 3}
lst.pop_front();          // {1, 2, 3}

// 尾部操作
lst.push_back(4);         // {1, 2, 3, 4}
lst.pop_back();           // {1, 2, 3}

// 插入操作
auto it = lst.begin();
++it;                     // 指向第二个元素
lst.insert(it, 10);       // {1, 10, 2, 3}

// 删除操作
it = lst.begin();
++it;                     // 指向10
lst.erase(it);            // {1, 2, 3}

// 清空
lst.clear();              // {}

// 交换
list<int> lst2 = {4, 5, 6};
lst.swap(lst2);           // 交换两个list的内容

2.6 list特有操作

list不受连续空间的限制,相对自由且高效

cpp 复制代码
list<int> lst1 = {1, 3, 5};
list<int> lst2 = {2, 4, 6};

// 合并两个有序链表(lst2会被清空)
lst1.merge(lst2);         // lst1: {1, 2, 3, 4, 5, 6}, lst2: {}

// 排序
list<int> lst3 = {3, 1, 4, 2};
lst3.sort();              // {1, 2, 3, 4}

// 去重(需要先排序)
list<int> lst4 = {1, 2, 2, 3, 3, 3};
lst4.unique();            // {1, 2, 3}

// 反转
list<int> lst5 = {1, 2, 3};
lst5.reverse();           // {3, 2, 1}

// 拼接:将另一个list的部分元素移动到当前list
list<int> lst6 = {1, 2, 3};
list<int> lst7 = {4, 5, 6};
auto pos = lst6.begin();
++pos;                    // 指向2
lst6.splice(pos, lst7);   // lst6: {1, 4, 5, 6, 2, 3}, lst7: {}

3. 迭代器失效问题

vector不同,list的迭代器失效规则更加简单:

3.1 安全的操作(不会导致迭代器失效)

  • 插入操作push_back(), push_front(), insert()

  • 其他操作merge(), sort(), reverse()

3.2 危险的操作(会导致当前迭代器失效)

  • 删除操作pop_back(), pop_front(), erase()
cpp 复制代码
// 错误示例
list<int> lst = {1, 2, 3, 4, 5};
auto it = lst.begin();
++it;                    // 指向2

lst.erase(it);           // 删除2
// cout << *it;         // 错误!it已失效

// 正确做法
it = lst.begin();
++it;
it = lst.erase(it);      // erase返回下一个有效迭代器
cout << *it;             // 现在it指向3

节点删除后直接使it成为野指针,因此会导致迭代器失效

3.3 批量删除的正确方式

cpp 复制代码
list<int> lst = {1, 2, 3, 4, 5, 6};

// 删除所有偶数 - 正确方式
auto it = lst.begin();
while (it != lst.end()) {
    if (*it % 2 == 0) {
        it = lst.erase(it);  // 重要:接收返回值
    } else {
        ++it;
    }
}

// 或者使用remove_if(更简洁)
lst.remove_if([](int x) { return x % 2 == 0; });

4. list的模拟实现

4.1 节点结构

cpp 复制代码
template<typename T>
struct ListNode {
    T _data;
    ListNode<T>* _prev;
    ListNode<T>* _next;
    
    ListNode(const T& val = T())
        : _data(val)
        , _prev(nullptr)
        , _next(nullptr)
    {}
};

4.2 list基础框架

cpp 复制代码
template<typename T>
class list {
private:
    ListNode<T>* _head;  // 头节点(不存储实际数据)

public:
    // 迭代器类(简化版)
    class iterator {
    private:
        ListNode<T>* _node;
        
    public:
        iterator(ListNode<T>* node = nullptr) : _node(node) {}
        
        T& operator*() { return _node->_data; }
        T* operator->() { return &(_node->_data); }
        
        iterator& operator++() {
            _node = _node->_next;
            return *this;
        }
        
        iterator operator++(int) {
            iterator temp = *this;
            _node = _node->_next;
            return temp;
        }
        
        iterator& operator--() {
            _node = _node->_prev;
            return *this;
        }
        
        iterator operator--(int) {
            iterator temp = *this;
            _node = _node->_prev;
            return temp;
        }
        
        bool operator!=(const iterator& it) const {
            return _node != it._node;
        }
        
        bool operator==(const iterator& it) const {
            return _node == it._node;
        }
    };

public:
    // 构造函数
    list() {
        _head = new ListNode<T>;
        _head->_prev = _head;
        _head->_next = _head;  // 循环链表
    }
    
    // 析构函数
    ~list() {
        clear();
        delete _head;
        _head = nullptr;
    }
    
    // 迭代器相关
    iterator begin() { return iterator(_head->_next); }
    iterator end() { return iterator(_head); }
    
    // 容量相关
    bool empty() const { return _head->_next == _head; }
    
    size_t size() const {
        size_t count = 0;
        ListNode<T>* cur = _head->_next;
        while (cur != _head) {
            ++count;
            cur = cur->_next;
        }
        return count;
    }
    
    // 元素访问
    T& front() { return _head->_next->_data; }
    T& back() { return _head->_prev->_data; }
    
    // 修改操作
    void push_back(const T& val) {
        insert(end(), val);
    }
    
    void push_front(const T& val) {
        insert(begin(), val);
    }
    
    void pop_back() {
        erase(iterator(_head->_prev));
    }
    
    void pop_front() {
        erase(begin());
    }
    
    // 插入操作
    iterator insert(iterator pos, const T& val) {
        ListNode<T>* newNode = new ListNode<T>(val);
        ListNode<T>* cur = pos._node;
        ListNode<T>* prev = cur->_prev;
        
        // 调整指针
        newNode->_prev = prev;
        newNode->_next = cur;
        prev->_next = newNode;
        cur->_prev = newNode;
        
        return iterator(newNode);
    }
    
    // 删除操作
    iterator erase(iterator pos) {
        ListNode<T>* cur = pos._node;
        ListNode<T>* prev = cur->_prev;
        ListNode<T>* next = cur->_next;
        
        prev->_next = next;
        next->_prev = prev;
        
        delete cur;
        return iterator(next);
    }
    
    void clear() {
        ListNode<T>* cur = _head->_next;
        while (cur != _head) {
            ListNode<T>* next = cur->_next;
            delete cur;
            cur = next;
        }
        _head->_next = _head;
        _head->_prev = _head;
    }
};

4.3 反向迭代器实现

cpp 复制代码
template<class Iterator>
class ReverseListIterator {
private:
    Iterator _it;

public:
    typedef ReverseListIterator<Iterator> Self;
    
    ReverseListIterator(Iterator it) : _it(it) {}
    
    // 解引用:需要向前移动一位,因为反向迭代器指向实际的前一个位置
    typename Iterator::reference operator*() {
        Iterator temp = _it;
        --temp;
        return *temp;
    }
    
    typename Iterator::pointer operator->() {
        return &(operator*());
    }
    
    Self& operator++() {
        --_it;
        return *this;
    }
    
    Self operator++(int) {
        Self temp = *this;
        --_it;
        return temp;
    }
    
    Self& operator--() {
        ++_it;
        return *this;
    }
    
    Self operator--(int) {
        Self temp = *this;
        ++_it;
        return temp;
    }
    
    bool operator!=(const Self& rit) const {
        return _it != rit._it;
    }
    
    bool operator==(const Self& rit) const {
        return _it == rit._it;
    }
};

5. list与vector的对比

特性 vector list
底层结构 动态数组,连续内存 双向链表,非连续内存
随机访问 O(1),支持下标访问 O(n),不支持下标访问
插入删除 尾部O(1),中间O(n) 任意位置O(1)
内存使用 内存连续,利用率高 每个元素额外存储指针
缓存友好 是,空间局部性好 否,空间局部性差
迭代器失效 插入删除可能导致全部失效 只有被删除元素迭代器失效
适用场景 随机访问频繁,尾部操作多 任意位置插入删除频繁

选择指南:

使用vector的情况:

  • 需要频繁随机访问元素

  • 主要在尾部进行插入删除操作

  • 内存效率要求高

  • 元素数量相对稳定

使用list的情况:

  • 需要在任意位置频繁插入删除

  • 不需要随机访问,主要是顺序访问

  • 对迭代器稳定性要求高

  • 元素大小较大,移动成本高

6. 性能优化建议

  1. 批量操作:使用范围插入而不是多次单元素插入

  2. 算法选择:利用list特有的sort、merge等算法

  3. 迭代器缓存:对于频繁访问的位置可以缓存迭代器

  4. 避免不必要的拷贝:使用emplace操作

相关推荐
SunkingYang3 小时前
详细介绍C++中通过OLE操作excel时,一般会出现哪些异常,这些异常的原因是什么,如何来解决这些异常
c++·excel·解决方案·闪退·ole·异常类型·异常原因
jc06203 小时前
4.4-中间件之gRPC
c++·中间件·rpc
十五年专注C++开发4 小时前
C++类型转换通用接口设计实现
开发语言·c++·跨平台·类设计
胡萝卜3.04 小时前
掌握string类:从基础到实战
c++·学习·string·string的使用
江公望4 小时前
通过QQmlExtensionPlugin进行Qt QML插件开发
c++·qt·qml
Syntech_Wuz4 小时前
从 C 到 C++:容器适配器 std::stack 与 std::queue 详解
数据结构·c++·容器··队列
果粒chenl4 小时前
React学习(四) --- Redux
javascript·学习·react.js
im_AMBER5 小时前
CSS 01【基础语法学习】
前端·css·笔记·学习
向阳花开_miemie5 小时前
Android音频学习(二十二)——音频接口
学习·音视频