【C++】C++ STL 探索:List使用与背后底层逻辑(一)

简介: 【C++】C++ STL 探索:List使用与背后底层逻辑

前文:List介绍

list文档介绍

  • list是可以在常数范围内在任意位置插入和删除的序列式容器,并且该容器可以前后双向迭代
  • list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前面一个元素和后一个元素
  • list与forward_list非常相似:最主要的不同在于forward_list是单链表,只能朝前迭代,已让其更简单高效
  • 与其他的序列式容器相比,list通常在任意位置进行插入,移除元素的执行效率更好
  • 与其他序列式容器相比,list和forward_list最大的缺陷式不支持任意位置的随机访问。

在模拟实现list过程中,为了避免跟库中list发生冲突,需要创建个命名空间,在命名空间中实现list。

namespace ls
{
  class list
  {
    
  };
}

list的底层是双向链表结构,而链表是通过每个独立的节点利用指针连接在一起的数据结构。节点是组成链表基本单位。模拟实现带头双向循环链表,为了适应不同的数据类型,选择采用类模板。

一、搭建创建List武器库

1.1 创建节点ListNode

template<class T>
    struct ListNode
    {
        ListNode<T>* _next;
        ListNode<T>* _prev;
        T _data;
        //创建节点,是传数据创建的
        ListNode(const T& x = T())
            :_next(nullptr)
                ,_prev(nullptr)
                ,_data(x)
            {}
    };

具体说明:为了适应不同类型的节点,将创建节点设计成类模板。访问限定符一般选用struct,由于默认访问权限是公开的,节点里面数据都是可以使用。当然使用class也是可以的,只需要将访问限定符设置为public。

节点对象实例化一般是传个数值,但是为了考虑类型和是否传参,可以设置一个全缺省构造函数

1.2 迭代器ListIterator

链表通过每个节点连接在一起,但这不能保证节点间地址是连续的。对此虽然原生指针是天然的迭代器,但是建立在物理空间连续的情况下

1.2.1 自定义类型迭代器

list<T> ::iterator it = lt.begin();
while (it != lt.end())
{
    *it += 10;
    cout << *it;
    it++;
}

由于链表节点间物理空间不连续,使用原生指针作为迭代器不能满足++或–操作。节点间又是通过指针连接在一起的,那么可以将指针封装到类中,通过运算符重载重新定义运算符的行为,达成我们的需求。不采用内置类型迭代器,选择自定义类型迭代器,其中自定义类型迭代器内部是通过指针实现的。

template<class T>
    struct ListIterator
    {
        typedef ListNode<T> Node;
        typedef  ListIterator<T> Self;
        Node* _node;
        ListIterator(Node* node)
            :_node(node)
            {}
    }

注意事项:

  • 将通过ListIterator类模板模拟实现迭代器,通过采用struct公开迭代器里面的数据
  • ListIterator(Node* node),这里迭代器的实现还是依赖指针,只是通过类封装改变运算符的行为,参数部分是传递指针类型(关键)
  • 其中迭代器是作为指向作用,申请资源不属于迭代器,而属于链表,不需要考虑析构的问题,迭代器就是玩数据
  • 数据公开意味着,内部数据可以被任意修改。没人会去跳过封装,使用内部的数据,没有意义。不同编译器中底层实现是不一样的(实现逻辑、名称),这本身就是一种隐式设置为私有的作用

1.2.2 交换语句

void swap(list<T>& it)
{
    std::swap(_head, it._head);
    std::swap(_size, it._size);
}

1.2.3 迭代器运算符重载

1.2.3.1 前置++与后置++
//前缀++
Self& operator++()
{
    _node = _node->_next;
    return *this;
}
//后缀++
Self operator++(int)
{
    //构造一个tmp临时变量
    Self tmp(*this);
    _node = _node->_next;
    return tmp;
}
1.2.3.2 前置–与后缀–
Self& operator--()
{
    _node = _node->_prev;
    return *this;
}
//后缀--
Self operator--(int)
{
    //构造一个tmp临时变量
    Self tmp(*this);
    _node = _node->_prev;
    return tmp;
}

具体说明:有拷贝构造就需要考虑深浅拷贝的问题。这里需要使用到浅拷贝,指向同一块空间,并且不需要考虑重复析构的问题,也说明了浅拷贝并都是坏处。临时对象tmp同指向一块空间,调用完临时对象被销毁,指向空间资源保留。这也导致了返回类型是指针还是引用。

1.2.3.4 *运算符重载
T& operator* ()
{
    return _node->_data;
}
1.2.3.5 ==与!=运算符重载
//通过指针指向的节点去判断是否相等
bool operator==(const Self& it)
{
    return _node == it._node;
}
bool operator!=(const Self& it)
{
    return _node != it._node;
}

具体说明:

  • 关于形参部分使用const修饰,其一为了提醒对象顺序问题,其二在while(it != lt.end())lt.end()调用返回临时对象具有常性,在参数部分进行接收将权限降低。
  • 迭代器间的比较并不是比较指向数据,而是比较迭代器指向的位置

二、正式模拟实现List

2.1 武器库使用

前面知识是为了我们实现List提供武器库

template<class T>
    class list
    {
        typedef ListNode<T> Node;
        public:
        typedef  ListIterator<T> Iterator;
        private:
        //创建哨兵位
        Node* _head;
        size_t _size;
    };

武器使用:

  • 关于typedef ListNode Nodetypedef ListIterator Iterator这两个类可以当作武器库,主要是为List类提供服务。那么对于ListIterator类来说ListNode Node也是当作一个武器进行使用。
  • 对于带头双向循环链表,成员对象需要哨兵位和记录个数对象。

2.2 获得List开头和结尾迭代器

Iterator begin()
{
    //return Iterator(_head->_next);
    return _head->_next;
}
Iterator end()
{
    //return Iterator(_head);
    return _head;
}

具体说明:

  • 由于正在使用自定义类型的迭代器,返回类型为Iterator自定义类型。返回类型可以写成Node*类型,单参数的拷贝构造函数支持隐式类型转换。

需要注意:

  • 返回值没有传引用返回,由于该接口功能是返回开头和节点迭代器,如果传引用返回,会影响下一次调用该节点。我们不需要拿到这个位置的迭代器,只需要拿到这个位置的信息。

小插曲:隐式类型转换

list<int> :: Iterator it = (lt._head->_next);

由于_ head属于list类中的私有对象,不能直接的访问私有成员,通过公共接口访问。不同编译器底层实现是不同的,这也体现了封装的重要性。

2.3 关于迭代器失效

关于vector学习,我们知道在扩容或缩容的时候,可能会出现迭代器失效的问题。在list这里insert不会出现迭代器失效,但是erase就会出现迭代器失效。

关于解决不同编译器下erase导致迭代器失效的问题。方法有两个:迭代器失效以后,不要直接使用,如何要使用按照规则重新更新使用,返回被删除数据的迭代器。

2.4 insert

void  insert(Iterator pos, const T& val)
{
    Node* newnode = new Node(val);
    Node* cur = pos._node;
    Node* prev = cur->_prev;
    newnode->_next = cur;
    newnode-> _prev = prev;
    cur->_prev = newnode;
    prev->_next = newnode;
    _size++;
}

这里跟数据结构实现双向链表任意位置插入数据的逻辑是一样的,不同的地方就是这里使用迭代器。

2.5 erase

//这些需要使用迭代器作为返回类型,怕出现迭代器失效
Iterator& erase(Iterator pos)
{
    Node* cur = pos._node;
    Node* next = cur->_next;
    Node* prev = cur->_prev;
    delete cur;
    next->_prev = prev;
    prev->_next = next;
    _size--;
    //注意点
    return Iterator(next);
}

具体说明:这里跟数据结构实现双向链表任意位置删除数据的逻辑是一样的。

需要注意:返回类型需要使用迭代器类型,怕出现迭代器失效,返回下一个位置的迭代器

2.6 复用insert与erase完成插入与删除

void push_back(const T& val)
{
    insert(end(), val);
}
void pop_back()
{
    erase(--end());
}
void push_front(const T& val)
{
    insert(begin(), val);
}
void pop_front()
{
    erase(begin());
}

具体说明:

  • 在vector实现push_back和pop_back时,通过begin和end得到迭代器指向的位置。返回变量为具有常性的临时变量,不能通过++或–对其修改。
  • 在List中迭代器可以进行++或–操作,由于不是对临时对象本身进行修改,而是在运算符重载中改变了运算符的行为,修改是临时对象指向的内容。在vector中修改是对象本身当然是不信的。

2.7 operator->重载

给出场景:关于之前类模板实例化中模板参数为内置类型,这次将T改为自定义类型,注意A是自定义类型。

struct A
{
    int a1 = 0, a2 = 0;
    A(int a1,int a2)
        :a1(1)
            ,a2(2)
        {}
};
list<A> lt;
list<A> ::Iterator it = lt.begin();
lt.push_back(A(1,2));
lt.push_back({3,3});
while (it != lt.end())
{
    cout << *it;
    it++;
}

在进行cout << *it中得到该类对象,并没有得到内部数据,对此有两个解决措施:

  1. (*it).a1直接访问成员
  2. 流插入的运算符重载

我们希望得到的效果是第二种写法,第一种感觉会冗余

  • 第一种:(*it).a1
  • 第二种:it->_a1

重点梳理:

//返回A类型对象指针,很合理啦
T* operator->()
{
    return &_node->_data;
}


【C++】C++ STL 探索:List使用与背后底层逻辑(二)https://developer.aliyun.com/article/1617349

相关文章
|
5天前
|
算法 C语言 C++
【c++丨STL】list的使用
本文介绍了STL容器`list`的使用方法及其主要功能。`list`是一种双向链表结构,适用于频繁的插入和删除操作。文章详细讲解了`list`的构造函数、析构函数、赋值重载、迭代器、容量接口、元素访问接口、增删查改操作以及一些特有的操作接口如`splice`、`remove_if`、`unique`、`merge`、`sort`和`reverse`。通过示例代码,读者可以更好地理解如何使用这些接口。最后,作者总结了`list`的特点和适用场景,并预告了后续关于`list`模拟实现的文章。
22 7
|
14天前
|
存储 编译器 C++
C++ initializer_list&&类型推导
在 C++ 中,`initializer_list` 提供了一种方便的方式来初始化容器和传递参数,而右值引用则是实现高效资源管理和移动语义的关键特性。尽管在实际应用中 `initializer_list&&` 并不常见,但理解其类型推导和使用方式有助于深入掌握现代 C++ 的高级特性。
16 4
|
23天前
|
存储 编译器 C语言
【c++丨STL】vector的使用
本文介绍了C++ STL中的`vector`容器,包括其基本概念、主要接口及其使用方法。`vector`是一种动态数组,能够根据需要自动调整大小,提供了丰富的操作接口,如增删查改等。文章详细解释了`vector`的构造函数、赋值运算符、容量接口、迭代器接口、元素访问接口以及一些常用的增删操作函数。最后,还展示了如何使用`vector`创建字符串数组,体现了`vector`在实际编程中的灵活性和实用性。
48 4
|
24天前
|
C语言 C++ 容器
【c++丨STL】string模拟实现(附源码)
本文详细介绍了如何模拟实现C++ STL中的`string`类,包括其构造函数、拷贝构造、赋值重载、析构函数等基本功能,以及字符串的插入、删除、查找、比较等操作。文章还展示了如何实现输入输出流操作符,使自定义的`string`类能够方便地与`cin`和`cout`配合使用。通过这些实现,读者不仅能加深对`string`类的理解,还能提升对C++编程技巧的掌握。
50 5
|
24天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
38 2
|
9天前
|
存储 编译器 C语言
【c++丨STL】vector模拟实现
本文深入探讨了 `vector` 的底层实现原理,并尝试模拟实现其结构及常用接口。首先介绍了 `vector` 的底层是动态顺序表,使用三个迭代器(指针)来维护数组,分别为 `start`、`finish` 和 `end_of_storage`。接着详细讲解了如何实现 `vector` 的各种构造函数、析构函数、容量接口、迭代器接口、插入和删除操作等。最后提供了完整的模拟实现代码,帮助读者更好地理解和掌握 `vector` 的实现细节。
18 0
|
1月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
83 5
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
80 4
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
86 4
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
31 4
下一篇
DataWorks