C++ STL全面解析:六大核心组件之一----序列式容器(vector和List)(STL进阶学习)

news2024/11/11 10:51:43

目录

序列式容器

Vector

vector概述

vector的迭代器

vector的数据结构

vector的构造和内存管理

vector的元素操作

List

List概述

List的设计结构

List的迭代器

List的数据结构

List的内存构造

List的元素操作


 C++标准模板库(STL)是一组高效的数据结构和算法的集合,广泛应用于C++程序设计中。STL由六大核心组件组成,分别是:

  1. 容器(Containers):各种数据结构,如vector,list,deque等等。
  2. 迭代器(Iterators):扮演容器域算法之间的胶合剂,是所谓的”泛型指针“
  3. 算法(Algorithms):各种常用算法,例如sort,search,copy,erase等等。
  4. 函数对象(Function Objects):又名为仿函数,行为类似函数,可作为算法的某种策略。
  5. 适配器(Adapters):一种用来修饰容器或者仿函数或迭代器接口的东西,例如:stack和queue。
  6. 分配器(Allocators):负责空间配置域管理,从实现角度来看他是一个管理空间的模板类。

        STL六大组件的交互关系,容器通过分配器获取数据存储空间,算法则通过迭代器去存取容器的内容,(这也是为什么说迭代器是类似于一种胶合剂的角色),仿函数则可以协助算法完成不同的策略变化,适配器可以修饰或者套接仿函数。

       在 C++ 中,STL(Standard Template Library,标准模板库)提供了一系列容器来帮助程序员组织和管理数据。常用的数据结构有:aray(数组)、list(链表)、tee(树)stack(堆栈)、queue(队列)、hashtable(散列表)、set(集合)、map(映射表)…等等。当提到STL时,许多人的第一印象便是容器,这也证明了容器的可靠性以及受欢迎程度。

        STL 容器大致可以分为两类:序列式容器(Sequence Containers)和关联式容器(Associative Containers)。如其各自的名字一般,序列式就是按照顺序排列的容器。而关联式容器就是通过键值和键值对进行查找的。

常见的序列式容器包括:

  • vector(向量):动态数组,支持随机访问,内部连续存储。
  • list(链表):双向链表,不支持随机访问,插入和删除操作非常快。
  • deque(双端队列):支持两端快速插入和删除的容器,支持随机访问。
  • forward_list(单向链表):C++11 引入的新容器,单向链表,不支持随机访问。

常见的关联式容器包括:

  • set(集合):存储唯一的键值,不允许重复,通常按照键的字典序排序。
  • multiset(多重集合):类似 set,但允许键值重复。
  • map(映射):存储键值对,键唯一,通常按照键的字典序排序。
  • multimap(多重映射):类似 map,但允许键值重复。

序列式容器

Vector

vector概述

        vector的数据结构跟array是非常相似的,只不过他们有一点不同,那就是array在定义时会被限制住大小,是静态的容量。而vector则是动态的容量,可以根据插入数据的数量去自动扩容容量。我们不必再去担心初始化数组的时候去定义一个大块头,使用vector时这个顾虑将烟消云散。

        vector的实现技术关键在于对其大小的控制以及重新分配时数据迁移的效率,一旦vector的空间满载,如果客户端每新增一个元素,vector随之去增加一个元素这种效率肯定是很慢的。所以vector是采用的未雨绸缪机制。

vector的迭代器

                vector维护的是一个连续线性空间,所以无论是什么类型的元素,普通指针都可以作为vector的迭代器而满足所有的必要条件。因为vector迭代器的操作指针都可以完成,无非就是加减乘除,加加,减减等操作。所以vector迭代器的定义正是普通指针。

vector的数据结构

        vector的数据结构也是非常简单:线性连续空间。它以两个迭代器start和finish分别指向所分配空间的目前已使用范围的头和尾,其中end_of_storage则是用来指向分配空间的尾。

        为了提高数据迁移时的效率,vector引入未雨绸缪的机制。这个机制就是vector实际配置的大小要比客户端需求的大小更大,以备将来的扩充,降低分配空间的次数。这个不难理解。

        通过start,finish,end_of_storage三个迭代器,便可轻易的提供首位标示,大小,容量,空容器判断等。

下方是vector的整体示意图:

vector的构造和内存管理

        我们围绕这个小测试程序说起。

// filename : vector-test.cpp
#include <vector>
#include <iostream>
#include <algorithm>

using namespace std;

int main() {
    int i;
    vector<int> iv(2, 9);
    cout << "size=" << iv.size() << endl;            // size=2
    cout << "capacity=" << iv.capacity() << endl;    // capacity=2

    iv.push_back(1);
    cout << "size=" << iv.size() << endl;            // size=3
    cout << "capacity=" << iv.capacity() << endl;    // capacity=4

    iv.push_back(2);
    cout << "size=" << iv.size() << endl;            // size=4
    cout << "capacity=" << iv.capacity() << endl;    // capacity=4

    iv.push_back(3);    
    cout << "size=" << iv.size() << endl;            // size=5
    cout << "capacity=" << iv.capacity() << endl;    // capacity=8

    iv.push_back(4);
    cout << "size=" << iv.size() << endl;            // size=6
    cout << "capacity=" << iv.capacity() << endl;    // capacity=8

    for (i = 0; i < iv.size(); ++i)
        cout << iv[i] << ' ';                        // 9 9 1 2 3 4
    cout << endl;

    iv.push_back(5);
    cout << "size=" << iv.size() << endl;            // size=7
    cout << "capacity=" << iv.capacity() << endl;    // capacity=8

    for (i = 0; i < iv.size(); ++i)
        cout << iv[i] << ' ';                        // 9 9 1 2 3 4 5
    cout << endl;

    iv.pop_back();
    iv.pop_back();
    cout << "size=" << iv.size() << endl;            // size=5
    cout << "capacity=" << iv.capacity() << endl;    // capacity=8
    cout << endl;

    iv.pop_back();
    cout << "size=" << iv.size() << endl;            // size=4
    cout << "capacity=" << iv.capacity() << endl;    // capacity=8

    vector<int>::iterator ite = find(iv.begin(), iv.end(), 1);
    if (ite != iv.end())
        iv.erase(ite);

    cout << "size=" << iv.size() << endl;            // size=3
    cout << "capacity=" << iv.capacity() << endl;    // capacity=8

    for (i = 0; i < iv.size(); ++i)
        cout << iv[i] << ' ';                        // 9 9 2
    cout << endl;

    ite = find(iv.begin(), iv.end(), 2);
    if (ite != iv.end())
        iv.insert(ite, 3, 7);

    cout << "size=" << iv.size() << endl;            // size=6
    cout << "capacity=" << iv.capacity() << endl;    // capacity=8

    for (int i = 0; i < iv.size(); ++i)
        cout << iv[i] << ' ';                        // 9 9 7 7 7 2
    cout << endl;

    iv.clear();
    cout << "size=" << iv.size() << endl;            // size=0

    cout << "capacity=" << iv.capacity() << endl;    // capacity=8

    return 0;
}

        从这段小的测试例子来看我们不难发现vector的分配空间的机制正如我们所了解的一样,它在扩容时会进行更大量级的扩容,又是双倍扩容。vector缺省使用alloc作为空间配置器,并据此定义了一个data_allocator,为的是方便以元素大小为配置单位。

        当我们使用push_back将新元素插入尾部时,该函数首先会去检测是否还有备用空间,如果有就不扩容,在备用空间上进行构造,并调整finish,如果没有就回去调用insert_aux扩容空间,重新配置,移动数据,释放空间。

template <class T, class Alloc>
void vector<T, Alloc>::insert_aux(iterator position, const T& x) {
    if (finish != end_of_storage) {  // 还有备用空间
        // 在备用空间起始处构造一个元素,并以vector最后一个元素值为其初值
        construct(&*finish, *finish-1);
        ++finish;

        // 复制待插入的元素值
        T x_copy = x;
        // 将 [position, finish-1) 区间的元素向后移动一位
        copy_backward(position, finish-1, finish);
        *position = x_copy;
    } else {  // 已无备用空间
        const size_type old_size = size();  // 记录旧的大小
        const size_type len = (old_size != 0 ? 2 * old_size : 1);  // 新大小:如果原大小为0,则配置1;否则配置原大小的两倍

        // 分配新的内存空间
        iterator new_start = data_allocator::allocate(len);
        iterator new_finish = new_start;

        try {
            // 将原 vector 的内容拷贝到新 vector
            new_finish = uninitialized_copy(start, position, new_start);
            // 为新元素设定初值 x
            construct(&*new_finish, x);
            ++new_finish;
            // 将原 vector 的备用空间中的内容也忠实复制过来
            new_finish = uninitialized_copy(position, finish, new_finish);
        } catch (...) {
            // 如果出现异常,销毁新分配的内存并释放
            destroy(new_start, new_finish);
            data_allocator::deallocate(new_start, len);
            throw;
        }

        // 析构并释放原 vector
        destroy(begin(), end());
        deallocate();

        // 调整迭代器,指向新 vector
        start = new_start;
        finish = new_finish;
        end_of_storage = new_start + len;
    }
}

        这里还需提一嘴,所谓的动态增加大小并非是像链表一般直接在后方增加新的扩容空间,因为vector的本质还是数组,所以它增加大小的方式和数组是相同的,大家不要误解。因此,一旦引起空间重新配置,那么原来vector的迭代器都将失效,切记。

vector的元素操作

        这里我不再多说,大家可以参考我之前学习vector操作时的一篇文章。

C++基础知识(八:STL标准库(Vectors和list))_c++ stl标准库-CSDN博客icon-default.png?t=O83Ahttps://blog.csdn.net/LKHzzzzz/article/details/136316825

List

List概述

        相对于vector,list则显得更为复杂一下。但是list本身的优势也是vector没有的。例如插入一个元素就会配置一个元素空间,这就做到了对空间运用的绝对精准,同时这也是许多大型项目中经常用到的一种数据结构,例有Linux内核,其中对list的运用更是出神入化,不得不感叹大神的智慧。但是vector和list各有各的优势,还需要取决于在什么场景下用那个数据结构。

List的设计结构

        list的本身和list的节点是不同的结构,需要分开进行设计,以下是一个双向链表的节点机构。

List的迭代器

        List的迭代器不能再像vector一般用一个普通指针作为迭代器,因为list的节点不是连续存在的,所以list迭代器必须要有能力指向list的节点,并可以进行++,--,等操作。这里我们可以看出来list是一个双向列表,那么我们的迭代器也就必须具备前移和后退的能力。

        注:list的迭代器在析构这个list之前都是有效的!与vector并不相同!

以下是list的迭代器结构

template <typename T, typename Ref, typename Ptr>
struct list_iterator {
    typedef list_iterator<T, T&, T*> Self; // 自定义类型别名
    typedef bidirectional_iterator_tag iterator_category; // 迭代器类别
    typedef T value_type; // 值类型
    typedef Ptr pointer; // 指针类型
    typedef Ref reference; // 引用类型
    typedef std::list<T>::node* link_type; // 节点类型
    typedef std::size_t size_type; // 大小类型
    typedef std::ptrdiff_t difference_type; // 差值类型

private:
    link_type node; // 迭代器内部的指针,指向 list 的节点

public:
    // 构造函数
    list_iterator(link_type x) : node(x) {}
    list_iterator() : node(nullptr) {} // 默认构造函数
    list_iterator(const Self& x) : node(x.node) {} // 拷贝构造函数

    // 比较运算符
    bool operator==(const Self& x) const { return node == x.node; }
    bool operator!=(const Self& x) const { return node != x.node; }

    // 解引用运算符
    reference operator*() const { return (*node).data; }

    // 成员访问运算符
    pointer operator->() const { return &(*this->operator*()); }

    // 前缀递增运算符
    Self& operator++() {
        node = link_type((*node).next);
        return *this;
    }

    // 后缀递增运算符
    Self operator++(int) {
        Self tmp = *this;
        ++(*this);
        return tmp;
    }

    // 前缀递减运算符
    Self& operator--() {
        node = link_type((*node).prev);
        return *this;
    }

    // 后缀递减运算符
    Self operator--(int) {
        Self tmp = *this;
        --(*this);
        return tmp;
    }
};
List的数据结构

        List不仅仅是一个双向链表,而且还是一个环状双向链表。所以它只需要一个指针便可以完整表现整个链表。

List的内存构造

让我们先从一段小的测试例子看起

#include <list>
#include <iostream>
#include <algorithm>

using namespace std;

int main() {
    int i;
    list<int> ilist;

    cout << "size=" << ilist.size() << endl; // size=0

    ilist.push_back(0);
    ilist.push_back(1);
    ilist.push_back(2);
    ilist.push_back(3);
    ilist.push_back(4);

    cout << "size=" << ilist.size() << endl; // size=5

    list<int>::iterator ite;
    for (ite = ilist.begin(); ite != ilist.end(); ++ite) {
        cout << *ite << " ";
    }
    cout << endl; // 输出 0 1 2 3 4
    cout << endl;

    ite = find(ilist.begin(), ilist.end(), 3);
    if (ite != ilist.end()) {
        ilist.insert(ite, 99);
    }
    cout << "size=" << ilist.size() << endl; // size=6
    cout << *ite << endl; // 输出 99

    for (ite = ilist.begin(); ite != ilist.end(); ++ite) {
        cout << *ite << " ";
    }
    cout << endl; // 输出 0 1 2 99 3 4
    cout << endl;

    ite = find(ilist.begin(), ilist.end(), 1);
    if (ite != ilist.end()) {
        cout << *(ilist.erase(ite)) << endl; // 输出 2
    }

    for (ite = ilist.begin(); ite != ilist.end(); ++ite) {
        cout << *ite << " ";
    }
    cout << endl; // 输出 0 2 99 3 4
    cout << endl;

    return 0;
}

        当我们以push_back()插入元素的时候他的底层调用的其实是Insert函数,

void push_back(const T& x){insert(end(),x);

        insert()是一个重载函数,有多种形式,但是在list中用的就是最简单一步操作,也就是单纯的去插入一个元素:首先构造一个元素空间,然后在尾部进行一系列的操作,把新元素插入进去。

list_node<T> *insert(list_node<T> *position, const T &x)
{
  list_node<T> *tmp = create_node(x); // 产生一个节点,内容为 x

  // 调整双向指针,使 tmp 插入进去
  tmp->next = position;
  tmp->prev = position->prev;

  // 更新前后节点的指针
  (static_cast<list_node<T> *>(position->prev))->next = tmp;
  position->prev = tmp;

  return tmp;
}

        插入之后的list状态也就如下图一般。由于list不像vector一般会在扩容时重新分配空间,然后转移过去,然后析构,因此list的迭代器是一直有效的。

List的元素操作

        这里大家可以去看一下我之前写的一篇基本操作文章关于List的,这里就不多说了。

C++基础知识(八:STL标准库(Vectors和list))_c++ stl标准库-CSDN博客icon-default.png?t=O83Ahttps://blog.csdn.net/LKHzzzzz/article/details/136316825

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2152230.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

利用 IDEA 快速管理 k8s 集群

简介 前置条件&#xff1a; minikube 已安装&#xff0c;JetBrains k8s 官方插件已安装&#xff0c;Helm 已安装&#xff0c;kubectl 已安装 打开插件面板 检查可执行文件 添加配置文件 添加集群 验证

Shiro-认证绕过漏洞(CVE-2020-1957)

文章目录 漏洞原理源码分析与复现影响版本 漏洞原理 核心点就是shiro和spring对uri进行配合匹配的时候有缺陷导致的&#xff0c;shiro中很轻易就能绕过&#xff0c;其次spring中对;分号好像情有独钟&#xff0c;被大牛们发现后也就一下子绕过了。 主流payload&#xff1a;/xxx…

【古籍下载】NO.111书格网站,免费搜索下载古籍的数字图书馆,推荐有价值的古籍善本、字画,将文化艺术作品数字化归档。

网站尽量挑选欣赏和在阅读价值较高的善本&#xff0c;更倾向于&#xff1a;艺术类、影像类、珍稀类以及部分刊印水平较高的书籍 下载地址&#xff1a;点击查看

基于51单片机的两路电压检测(ADC0808)

目录 一、主要功能 二、硬件资源 三、程序编程 四、实现现象 一、主要功能 基于51单片机&#xff0c;通过ADC0808获取两路电压&#xff0c;通过LCD1602显示 二、硬件资源 基于KEIL5编写C代码&#xff0c;PROTEUS8.15进行仿真&#xff0c;全部资源在页尾&#xff0c;提供…

笔记整理—内核!启动!—linux应用编程、网络编程部分(1)API概述与文件I/O

文件I/O即为文件的input和output的功能。 操作系统的API&#xff1a; 学习操作系统的本质就是学习一个操作系统提供的API。 常用的IO:open、close、write、read、lseek。 打开一个文件&#xff08;open&#xff09;得到一个文件描述符&#xff0c;读写文件使用&#xff08;read…

VScode快速配置c++(菜鸟版)

1.vscode是什么 Visual Stdio Code简称VS Code&#xff0c;是一款跨平台的、免费且开源的现代轻量级代码编辑器&#xff0c;支持几乎 主流开发语言的语法高亮、智能代码补全、自定义快捷键、括号匹配和颜色区分、代码片段提示、代码对比等特性&#xff0c;也拥有对git的开箱即…

2024 “华为杯” 中国研究生数学建模竞赛(D题)深度剖析|大数据驱动的地理综合问题|数学建模完整代码+建模过程全解全析

当大家面临着复杂的数学建模问题时&#xff0c;你是否曾经感到茫然无措&#xff1f;作为2022年美国大学生数学建模比赛的O奖得主&#xff0c;我为大家提供了一套优秀的解题思路&#xff0c;让你轻松应对各种难题&#xff01; CS团队倾注了大量时间和心血&#xff0c;深入挖掘解…

Java项目实战II基于Java+Spring Boot+MySQL的车辆管理系统(开发文档+源码+数据库)

目录 一、前言 二、技术介绍 三、系统实现 四、论文参考 五、核心代码 六、源码获取 全栈码农以及毕业设计实战开发&#xff0c;CSDN平台Java领域新星创作者&#xff0c;专注于大学生项目实战开发、讲解和毕业答疑辅导。获取源码联系方式请查看文末 一、前言 "随着…

嵌入式面试学习笔记(入门1)

目录 指针的大小问题 sizeof和strlen C语言分配内存的方式 数组&#xff08;的&#xff09;指针和指针&#xff08;的&#xff09;数组 union 指针的大小问题 指针对于不少新手而言是一道难关&#xff0c;但是不必恐惧于指针。他的本质其实就是一个地址。请冷静下来仔细思…

基于yolov8和openpose人体骨骼关键点实现的摔倒姿态识别检测系统实现

【参考源码】 GitHub - HRonaldo/Openpose_YOLO 本项目参考上面框架进行全面改进&#xff0c;改进如下&#xff1a; &#xff08;1&#xff09;将检测框架换成当前最流行框架yolov8&#xff0c;并封装成类实现模块化设计。关于yolov5优化项目可以访问&#xff1a;https://bl…

【华为杯研赛赛题】2024年中国研究生数学建模竞赛赛题已出

2024年中国研究生数学建模竞赛所有赛题已出&#xff01; A题 B题 C题 D题 E题 F题

【Verilog学习日常】—牛客网刷题—Verilog企业真题—VL77

编写乘法器求解算法表达式 描述 编写一个4bit乘法器模块&#xff0c;并例化该乘法器求解c12*a5*b&#xff0c;其中输入信号a,b为4bit无符号数&#xff0c;c为输出。注意请不要直接使用*符号实现乘法功能。 模块的信号接口图如下&#xff1a; 要求使用Verilog HDL语言实现以上…

使用 Elasticsearch Reindex API 迁移数据

使用 Elasticsearch Reindex API 迁移数据 在 Elasticsearch 中&#xff0c;随着需求的变化&#xff0c;可能需要对索引进行重建或更新。这通常涉及创建新索引、迁移数据等步骤。本文介绍如何使用 Reindex API 将旧索引中的数据迁移到新索引中 一、步骤概述 创建新索引&#…

OpenCV_距离变换的图像分割和Watershed算法详解

在学习watershed算法的时候&#xff0c;书写代码总会出现一些错误&#xff1a; 上述代码运行报错&#xff0c;显示OpenCV(4.10.0) Error: Assertion failed (src.type() CV_8UC3 && dst.type() CV_32SC1) in cv::watershed 查找资料&#xff1a;目前已解决 这个错…

idea 编辑器常用插件集合

SequenceDiagram 用于生成时序图的插件&#xff0c;支持一键生成功能。 使用&#xff1a;选择某个具体的方法&#xff0c;点击右键菜单&#xff0c;选择“Sequence Diagram” 便可生成相应的时序图 例子&#xff1a; 效果&#xff1a; Code Iris Code Iris可以根据代码自动…

c++day3 手动封装一个顺序表(SeqList),分文件编译实现

要求: 有私有成员&#xff1a;顺序表数组的起始地址 ptr、 顺序表的总长度&#xff1a;size、顺序表的实际长度&#xff1a;len 成员函数&#xff1a;初始化 init(int n) 判空&#xff1a;empty 判满&#xff1a;full 尾插&#xff1a;push_back 插入&#xff1a;insert&…

优数:助力更高效的边缘计算

在数字化时代的浪潮中&#xff0c;数据已成为企业最宝贵的资产之一。随着物联网&#xff08;IoT&#xff09;设备的激增和5G技术的兴起&#xff0c;我们正迅速步入一个新时代&#xff0c;在这个时代中&#xff0c;数据不仅在量上爆炸性增长&#xff0c;更在速度和实时性上提出了…

Hadoop里面MapReduce的序列化与Java序列化比较

什么是序列化&#xff1f; jvm中的一个对象&#xff0c;不是类&#xff0c;假如你想把一个对象&#xff0c;保存到磁盘上&#xff0c;必须序列化&#xff0c;你把文件中的对象进行恢复&#xff0c;是不是的反序列化。 假如你想把对象发送给另一个服务器&#xff0c;需要通过网…

线性dp 总结详解

就是感觉之前 dp 的 blog 太乱了整理一下。 LIS(最长上升子序列) 例题 给定一个整数序列&#xff0c;找到它的所有严格递增子序列中最长的序列&#xff0c;输出其长度。 思路 拿到题目&#xff0c;大家第一时间想到的应该是的暴力(dp)做法&#xff1a; #include <bits/s…

基于Windows系统以tomcat为案例,讲解如何新增自启动服务,定时重启服务。

文章目录 引言I 设置服务自启动的常规操作II 安装多个tomcat服务,并设置自启动。III 定时重启服务引言 为了同一个版本安装多个tomcat服务,并设置自启动。使用Windows的任务计划程序来创建一个定时任务,用于重启Tomcat服务。I 设置服务自启动的常规操作 运行窗口输入control…