C++ STL概念之 序列式容器1(vector / list)

vector常见接口使用:

push_back

复制代码
void push_back (const value_type& val);
void push_back (value_type&& val);

push_back有两种重载方式

一、 参数是一个vector 存储类型的元素

二、 参数是一个vector 存储类型的右值元素,是右值类型的重载版本,用于向容器移动右值(临时对象),而不是复制

operator [ ]

复制代码
reference operator[] (size_type n);
const_reference operator[] (size_type n) const;

operator [ ] 是vector重载了 [ ] 让它可以像数组一样使用,参数是下标,返回值是该下标的元素的引用,支持了const和非const版本

size

复制代码
size_type size() const noexcept;

作用是返回当前vector的元素个数

noexcept 关键字是 指定这个函数不会抛异常。暗示了调用size函数是安全的,因为它不会导致程序因为异常而突然终止。

resize

复制代码
void resize (size_type n);
void resize (size_type n, const value_type& val);
  • void resize (size_type n);

    这个版本的 resize 函数将容器的大小调整为 n 元素。如果当前大小大于 n,则会移除多余的元素。如果当前大小小于 n,则会添加元素直到容器的大小为 n。新添加的元素将会进行默认初始化。

  • void resize (size_type n, const value_type& val);

    这个版本的 resize 函数也是将容器的大小调整为 n 元素,但是它允许你指定新增元素的初始值 val。如果当前大小大于 n,多余的元素会被移除;如果当前大小小于 n,则会添加值为 val 的元素到容器的末尾直到其大小为 n

reserve

复制代码
void reserve (size_type n);

目的是提前为容器分配足够的内存空间,以存储至少 n 个元素。这是一种优化内存管理和提高性能的方法。

resize 和reserve : resize开空间+初始化,可以用[ ]访问修改。reserve是单纯开空间。不能访问[ ],因为[ ]会做 i < size 的检查

insert

| single element (1) | iterator insert (const_iterator position, const value_type& val); |
| fill (2) | iterator insert (const_iterator position, size_type n, const value_type& val); |
| range (3) | template <class InputIterator> iterator insert (const_iterator position, InputIterator first, InputIterator last); |
| move (4) | iterator insert (const_iterator position, value_type&& val); |

initializer list (5) iterator insert (const_iterator position, initializer_list<value_type> il);
  1. 单元素版 (single element):

    • iterator insert (const_iterator position, const value_type& val);
      这个函数在**position** 指示的位置之前插入一个新元素 val,并返回指向新插入元素的迭代器。
  2. 填充版 (fill):

    • iterator insert (const_iterator position, size_type n, const value_type& val);
      这个函数在 position 指示的位置之前插入 n 个新元素,每个新元素的值都是**val**,并返回指向第一个新插入元素的迭代器。
  3. 范围版 (range):

    • template <class InputIterator>
      iterator insert (const_iterator position, InputIterator first, InputIterator last);
      这个函数在 position 指示的位置之前插入由范围 **[first, last)**指定的元素,返回指向第一个新插入元素的迭代器。
    • 例: {1,2,3 }插入{4,5,6}
      position是2。
      结果:{1,4,5,6,2,3}
  4. 移动版 (move):

    • iterator insert (const_iterator position, value_type&& val);
      这个函数使用右值引用将一个新元素 val 移动到 **position**指示的位置之前,并返回一个迭代器,该迭代器指向被移动的元素。
  5. 初始值列表版 (initializer list):

    • iterator insert (const_iterator position, initializer_list<value_type> il);
      这个函数在 position 指示的位置之前插入由 initializer_list il 指定的元素,并返回指向第一个新插入元素的迭代器。

erase

复制代码
iterator erase (const_iterator position);
iterator erase (const_iterator first, const_iterator last);
  1. 单迭代器版本:
    这个函数删除位于 position 的单个元素,并返回指向被删除元素后一个元素的迭代器。如果删除的元素是 std::vector 的最后一个元素,则返回的迭代器等同于 end()
  2. 迭代器范围版本:
    这个函数删除闭区间 [first, last) 内所有的元素,就是说,从 firstlast 之间的元素会被删除,但不包括 last 指向的元素。并返回指向原来 last 所在位置的新元素的迭代器。如果 last 等同于 end(),返回的迭代器也将是 end()

iterator

vector的迭代器是一种支持随机访问的迭代器。随机访问迭代器允许我们迅速访问向量内任何位置的元素,就像使用数组下标一样。这种类型的迭代器提供了以下基本操作:

  • 递增 (++):迭代器可以通过 ++ 操作符进行递增,指向向量中的下一个元素。
  • 递减 (--):迭代器可以通过 -- 操作符进行递减,指向向量中的前一个元素。
  • 解引用 (*):迭代器可以被解引用来访问它指向的元素的值,就像指针一样。
  • 指针算术 :迭代器支持加减算术操作。例如,可以执行 iter + 5 来快速移动迭代器到当前位置后的第五个元素。

std::vector 提供了几种类型的迭代器:

  • Iterator:这是最常见的迭代器类型,允许读写向量内的元素。
  • Const_iterator:这种类型的迭代器不允许修改它所指向的元素,只能用来读取元素。常用于不需要更改向量内容的场景。
  • Reverse_iterator:以相反的顺序遍历向量。
  • Const_reverse_iterator:以相反的顺序遍历向量,但不允许修改元素。

向量的迭代器还提供了与容器的关系方面的操作:

  • **begin()/end()**:begin() 返回指向向量第一个元素的迭代器,end() 返回指向向量最后一个元素之后位置的迭代器(也就是说,不是最后一个元素,而是最后一个元素的下一个位置)。
  • **rbegin()/rend()**:与 begin()end() 类似,但返回的是相应的反向迭代器,rbegin() 返回向量末尾的迭代器,rend() 返回向量开头之前位置的迭代器。

使用迭代器时要特别注意迭代器失效的问题。向量在进行插入(insert)或删除(erase)操作后,如果这些操作导致内存重新分配,则从那个点开始到向量末尾的所有迭代器都将失效。必须重新获取迭代器。在编写依赖迭代器的代码时,这是一个重要的安全注意事项。

lsit常见接口使用:

push_back

复制代码
void push_back (const value_type& val);
void push_back (value_type&& val);
  1. void push_back (const value_type& val);
    这个版本的 push_back 接受一个对常量引用 (const value_type&) 参数。这意味着你可以传递一个对现有对象的引用,函数将复制这个对象并在列表的末尾添加一个新元素,这个新元素的内容就是传入对象的副本。这个过程通常涉及到调用对象的拷贝构造函数。
  2. void push_back (value_type&& val);
    第二个版本是 C++11 新增的,接受一个右值引用 (value_type&&)。这允许使用移动语义来传递临时对象或者可以被"移动"的对象作为参数,这样可以避免不必要的拷贝,提高效率。如果对象支持移动语义,push_back 将利用移动构造函数直接在列表的末尾构建新元素,通常而言,这比拷贝构造过程更高效。

pop_back

复制代码
void pop_back();

其作用是移除容器末尾的元素。当调用 pop_back() 时,会发生以下几件事情:

  1. 它会销毁列表末尾的最后一个元素,即调用该元素的析构函数。
  2. 容器的大小会减少1。
  3. 如果容器为空(没有元素可以弹出),调用 pop_back() 可能导致未定义的行为,因为试图删除不存在的元素。因此,在调用 pop_back() 之前,检查容器是否为空通常是一个好习惯。

push_front

复制代码
void push_front (const value_type& val);
void push_front (value_type&& val);
  1. void push_front (const value_type& val);
    这个版本的 push_front 接受一个对常量引用 (const value_type&) 参数。这会将一个元素的副本添加到容器的开始处。这个操作通常会调用元素的拷贝构造函数创建一个新的实例。
  2. void push_front (value_type&& val);
    这个版本是带有 C++11 右值引用 (value_type&&) 参数的 push_front。它允许通过移动语义来插入元素,可以避免不必要的对象复制,提高效率。它将直接在容器起始位置构造新元素,通常使用移动构造函数。

pop_front

复制代码
void pop_front();

移除容器开始处的第一个元素。使用 pop_front() 函数时,将会发生以下情况:

  1. 它将销毁容器开始处的第一个元素,即调用该元素的析构函数。
  2. 容器的大小将减少一个元素。
  3. 如果容器是空的(已经没有元素可以删除了),调用 pop_front() 将导致未定义的行为,因为它会尝试去删除一个不存在的元素。在调用 pop_front() 之前,最好检查一下容器是否为空。

insert

| single element (1) | iterator insert (const_iterator position, const value_type& val); |
| fill (2) | iterator insert (const_iterator position, size_type n, const value_type& val); |
| range (3) | template <class InputIterator> iterator insert (const_iterator position, InputIterator first, InputIterator last); |
| move (4) | iterator insert (const_iterator position, value_type&& val); |

initializer list (5) iterator insert (const_iterator position, initializer_list<value_type> il);
  1. 单个元素(single element)的插入
    iterator insert (const_iterator position, const value_type& val);

    在迭代器 position 指定的位置之前插入一个由 val 指定的元素,并返回指向新插入元素的迭代器。

  2. 填充(fill
    iterator insert (const_iterator position, size_type n, const value_type& val);

    在迭代器 position 指定的位置之前插入 n 个由 val 指定的元素,并返回指向第一个被插入的元素的迭代器;如果 n 是0,则不会进行插入操作,返回 position

  3. 范围(range

    在迭代器 position 指定的位置之前插入由范围 [first, last) 指定的所有元素,并返回指向第一个被插入的元素的迭代器。

  4. 移动(move
    iterator insert (const_iterator position, value_type&& val);

    使用右值引用。在迭代器 position 指定的位置之前插入一个 val 元素(使用移动语义),并返回指向新插入元素的迭代器。

  5. 初始化列表(initializer list
    iterator insert (const_iterator position, initializer_list<value_type> il);

    在迭代器 position 指定的位置之前插入一个由初始化列表 il 指定的所有元素,并返回指向第一个被插入元素的迭代器。

erase

复制代码
iterator erase (const_iterator position);
iterator erase (const_iterator first, const_iterator last);
  1. 单个元素的删除

    • iterator erase (const_iterator position);
      这个函数从容器中删除位于 position 处的元素,并返回指向被删除元素之后元素的迭代器。如果被删除的元素是容器中的最后一个元素,则返回容器的 end()
  2. 范围内元素的删除

    • iterator erase (const_iterator first, const_iterator last);
      这个函数删除范围 [first, last) 内的所有元素,并返回指向最后被删除元素之后元素的迭代器。如果 last 等于容器的 end(),那么返回的迭代器也将是 end()

Iterator

下面是自实现的迭代器类

template<class T, class Ref, class Ptr>

class ListIterator

{

typedef ListNode<T>* PNode;

typedef ListIterator<T, Ref, Ptr> Self;

public:

ListIterator(PNode pNode = nullptr)

:_pNode(pNode)

{}

ListIterator(const Self& l)

{

_pNode = l._pNode;

}

Ref operator*()

{

return _pNode->_val;

}

Ptr operator->()

{

return &_pNode->_val;

}

Self& operator++()

{

_pNode = _pNode->_pNext;

return *this;

}

Self operator++(int)

{

Self tmp(*this);

_pNode = _pNode->_pNext;

return tmp;

}

Self& operator--()

{

_pNode = _pNode->_pPre;

return *this;

}

Self operator--(int)

{

Self tmp(*this);

_pNode = _pNode->_pPre;

return tmp;

}

bool operator!=(const Self& l)

{

return _pNode != l._pNode;

}

bool operator==(const Self& l)

{

return _pNode == l._pNode;

}

public:

PNode _pNode;

};

1. 迭代器设计的目的

迭代器的设计目的是提供一种统一的接口来访问容器内的元素,无论容器的内部结构如何。对于链表来说,迭代器隐藏了链表的节点链接逻辑,使得用户可以通过相同的方式来遍历链表、数组等不同的数据结构。

2. 链表迭代器的基本操作

  • 递增(operator++ : 迭代器提供了递增操作,使其指向链表中的下一个元素。对于双向链表的迭代器,通常提供了两种递增操作:前缀递增(++iter)和后缀递增(iter++)。前缀形式直接返回递增后的迭代器,后缀形式则返回递增前的拷贝,然后将自身递增。
  • 递减(operator--: 对于双向链表的迭代器,它还支持递减操作,即向前遍历元素。同样地,递减操作也有前缀和后缀两种形式。
  • 解引用(operator*operator-> : 解引用操作允许通过迭代器访问当前指向的元素的值。operator* 返回元素的引用,而 operator-> 允许访问元素成员。

3. 链表迭代器的实现

以双向链表为例,迭代器内部通常保存一个指向当前链表节点的指针。通过移动这个指针来实现递增和递减操作,通过解引用这个指针来访问当前节点的值。

迭代器通常区分为常量和非常量两种,非常量迭代器允许修改指向的元素,而常量迭代器不允许。

4. 开闭原则

迭代器的设计遵循了开闭原则,链表的内部实现可以自由改变,只要迭代器接口保持不变,使用迭代器的代码不需要做任何修改就可以继续正常工作。

5. 迭代器失效

需要注意的一个重要概念是迭代器失效。在对链表进行插入、删除等操作时,某些迭代器可能会失效,即它们不再指向原来的元素。比如,删除一个节点后,指向该节点的迭代器将变得无效。正确处理迭代器失效是使用迭代器时的一个重要考虑点。

vector / list 的常见经典问题

vector和list的区别

vector:

优点: 尾插尾删, 下标随机访问,空间连续,CPU高速缓存的命中率更高

缺点:中间的插入和删除需要挪动数据,效率低

扩容有代价:开新空间+拷贝数据

list:

优点:任意位置O(1)的插入和删除,按需申请和释放

缺点:不支持随机访问(内存碎片问题)

链表的内存碎片问题:

链表和内存碎片化

链表是一种通过节点链接的数据结构,每个节点包含数据和指向下一个(以及可能的上一个)节点的指针。由于链表的节点通常是单独分配的,链表可以引起两种类型的内存碎片:

  1. 外部碎片:链表节点可以在内存的任何位置被分配,这可能导致内存中未被利用的小空闲区域,也就是外部碎片。外部碎片化会随着多次分配和释放增加,最终可能导致内存利用率下降。

  2. 内部碎片:链表节点通常比实际需要存储的数据大(因为还需要存储指针),这可能导致为每个节点分配的内存中有未使用的部分,即内部碎片。

空间配置器和内存碎片化

空间配置器是一个系统级或库级的工具,用来管理内存的分配和释放。它通常会提供一些策略来减少内存分配导致的碎片化。例如:

  1. 内存池:空间配置器可以实现内存池策略,预先分配一大块内存,并从中分配小块内存给用户。当用户请求小块内存时,配置器从池中提取,可以减少外部碎片。

  2. 固定大小的分配器:对于链表这种结构,配置器可能提供了一种固定大小的分配器,专门用来分配节点大小一致的内存块。这样可以减少内部碎片以及释放内存时导致的外部碎片化。

vector的扩容

不同编译器的实现方式不同。

g++在vector的扩容策略是2倍扩容, 在 v s(visual studio)上是1.5倍的扩容。

并且在扩容后的数据移动方式上,C++11之前是拷贝构造,C++11之后就是移动构造了(通过move()变成右值)

相关推荐
残月只会敲键盘4 分钟前
面相小白的php反序列化漏洞原理剖析
开发语言·php
ac-er88886 分钟前
PHP弱类型安全问题
开发语言·安全·php
ac-er88887 分钟前
PHP网络爬虫常见的反爬策略
开发语言·爬虫·php
爱吃喵的鲤鱼17 分钟前
linux进程的状态之环境变量
linux·运维·服务器·开发语言·c++
懒惰才能让科技进步39 分钟前
从零学习大模型(十二)-----基于梯度的重要性剪枝(Gradient-based Pruning)
人工智能·深度学习·学习·算法·chatgpt·transformer·剪枝
DARLING Zero two♡43 分钟前
关于我、重生到500年前凭借C语言改变世界科技vlog.16——万字详解指针概念及技巧
c语言·开发语言·科技
7年老菜鸡44 分钟前
策略模式(C++)三分钟读懂
c++·qt·策略模式
Gu Gu Study1 小时前
【用Java学习数据结构系列】泛型上界与通配符上界
java·开发语言
Ni-Guvara1 小时前
函数对象笔记
c++·算法
似霰1 小时前
安卓智能指针sp、wp、RefBase浅析
android·c++·binder