12. STL的原理

发布于:2025-03-31 ⋅ 阅读:(14) ⋅ 点赞:(0)

目录

1. 容器、迭代器、算法

什么是迭代器?

迭代器的作用?

迭代器的类型?

迭代器失效

迭代器的实现细节:

2. 适配器

什么是适配器?

适配器种类:

3. 仿函数

什么是仿函数?

仿函数与算法和容器的关系:

4.空间配置器

5.STL的优缺点是什么?设计的好的地方和不好的地方?


STL包含六大组件,这些组件是杂糅到一起的,互相之间有关联。

问到就详细讲容器和迭代器

1. 容器、迭代器、算法

上一篇讲了容器,这里主要介绍它和其他组件的关系

组件 与容器的关系 典型协作场景
迭代器 容器提供迭代器接口 begin()、end() 算法通过迭代器访问容器内容
算法 通过迭代器操作容器 sort(), find()等算法处理容器数据
仿函数 定义容器排序/比较规则 set的比较函数
适配器 基于容器构建特殊接口 stack基于deque实现
分配器 控制容器内存管理 自定义内存分配策略
  • 算法通过迭代器操作容器

  • 同一算法可作用于不同容器

  • 不同算法对迭代器类别有不同要求:

    • sort需要随机访问迭代器(不能用于list

    • find只需要输入迭代器(可用于所有容器)

什么是迭代器?

        迭代器的设计非常的天才,它充分体现面向对象的封装特性,容器的底层各不相同(哈希、树、数组等),容器内部是私有的,外部访问不了他们的内部结构,使用迭代器封装,在不需要暴露底层复杂结构的同时,还提供了统一、简单的方式访问容器,也就是它提供了一种统一的方法来访问容器中的元素,而不需要了解容器的内部结构。

迭代器的作用?

        迭代器把容器和算法连接了起来,作为算法和容器之间的桥梁,算法通过迭代器直接访问各种容器,如果不符合算法要求又可以直接报错。比如sort算法,在设计时设计的是需要给他传一个随机迭代器,如果给他一个其他类型的迭代器则会报错。

        算法的具体实现,直接脱离了具体的存储结构,也就是各种容器,直接通过迭代器来实现,依赖各种迭代器的类型,体现了设计角度的复用,使得同一算法可应用于任何满足迭代器要求的容器

        指针本身就是随机访问迭代器,使得传统数组也能参与算法:

int arr[10] = {...};
std::sort(arr, arr + 10);

迭代器的类型?

在C++标准库中,不同容器支持不同类型的迭代器:

  •     std::list:使用双向迭代器,支持前向和后向遍历。
  •     std::vector 和 std::deque、std::string:使用随机访问迭代器,支持任意位置的访问和高效的算术运算。
  •     std::forward_list:使用前向迭代器,仅支持单向遍历。
  •     std::set, std::map, std::multiset, std::multimap:使用双向迭代器。
  •     std::unordered_set, std::unordered_map, std::unordered_multiset, std::unordered_multimap:使用前向迭代器。

        层级低的容器不能访问层级高的算法。层级高的容器可以访问层级低的函数。比如:vector和string就可以调用reverse(双向)。

按功能从弱到强排列如下:

  1.     输入迭代器(Input Iterator)
  2.     输出迭代器(Output Iterator)
  3.     前向迭代器(Forward Iterator)
  4.     双向迭代器(Bidirectional Iterator)
  5.     随机访问迭代器(Random Access Iterator)

        比如说 sort 使用的就是随机迭代器, list 就不能调用它,因为 list 使用的是双向迭代器。

迭代器失效

        迭代器失效是指一个原本指向容器元素的迭代器在容器被修改后,可能变得无效。使用失效的迭代器会导致程序运行错误甚至崩溃。

对于 std::vector 来说,以下操作可能导致迭代器失效:

  1. 内存重新分配:

    • 扩容时,底层存储数据的内存地址会重新分配。
    • 所有现有的迭代器、指针和引用都会失效
  2. 元素插入或删除
    • 插入或删除可能会导致部分迭代器失效,尤其是插入删除位置的迭代器

        vector扩容,也就是说vector底层旧空间被释放掉,而在打印时,it还使用的是释放前的旧空间,在对it进行操作时,实际操作的是一块已经被释放的空间,从而引起崩溃。

         erase删除某位置的元素后,pos位置之后的元素会向前搬移,没有导致底层空间的改变,理论上迭代器应该不会失效,但是如果pos刚好是最后一个元素,删完之后pos刚好是end的位置,而end位置是没有元素的,那么pos就失效了。因此删除vector中任意位置上元素时,vs就认为该位置迭代器失效。

其他:

        string下的失效,与vector类似,string在插入+扩容操作+erase后,迭代器也会失效,不能再访问。erase函数执行后,it所指向的节点已经被删除,因此it无效,在下一次使用it时,必须先给其赋值。也就是用迭代器接受一下erase的返回值。

        list的底层结构为带头结点的双向循环链表,因此在list中进行插入时是不会导致list的迭代器失效的,只有在被删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响。

迭代器的实现细节:

08.vector-CSDN博客

2. 适配器

什么是适配器?

        适配器是一种结构型设计模式,它将一个类的接口转换成客户所期望的另一个接口。在STL中,适配器主要作用于容器和迭代器,通过封装现有组件并提供新的接口来满足不同的需求 。

        主要和容器有关,也可能和迭代器有关,比如说反向迭代器就是一个适配器,适配器体现的是封装复用。所以可以分为容器适配器和迭代器适配器

适配器种类:

容器适配器:10.stack 和 queue_queue 与stack-CSDN博客

迭代器适配器(以反向迭代器为例):10.stack 和 queue_queue 与stack-CSDN博客

正向序列:  [A][B][C][D][E]
           ↑  ↑  ↑  ↑  ↑   ↑
正向迭代器:begin           end

反向序列:      [E][D][C][B][A]
            ↑  ↑  ↑  ↑  ↑  ↑
反向迭代器:rbegin          rend
对应正向:  end            begin
  1. rbegin() 的实现

    • 内部实际上存储的是 end() 迭代器

    • 但解引用时返回的是 end()-1 位置的元素

  2. rend() 的实现

    • 内部实际上存储的是 begin() 迭代器

    • 解引用时会试图访问 begin()-1(这是未定义行为)

  3. 对称性

    • rbegin().base() == end()

    • rend().base() == begin()

3. 仿函数

什么是仿函数?

        仿函数(Functor),也称为函数对象,是C++中一种特殊的类。它通过重载operator()操作符,使得该类的对象可以像普通函数一样被调用

针对算法可以实现各种功能,最经典的就是比大小,特点就是可以控制规则

仿函数与算法和容器的关系:

算法通过仿函数参数实现灵活的行为控制:

// 排序算法使用仿函数定义比较规则
sort(vec.begin(), vec.end(), greater<int>());

某些容器依赖仿函数定义元素的组织方式

// set使用less仿函数默认升序排列
std::set<int, std::less<int>> ascendingSet;

// priority_queue使用greater仿函数实现小顶堆
std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap;

C++11后,lambda表达式成为创建仿函数的便捷方式:

sort(v.begin(), v.end(), [](auto& a, auto& b) {
    return a.value < b.value;
});

4.空间配置器

        空间配置器是STL中负责内存管理的核心组件,它为容器提供统一的内存分配和释放接口

主要职责是:

  • 内存的分配与释放

  • 对象的构造与析构

  • 提供标准化的内存管理接口

所有STL容器都使用空间配置器管理内存,默认使用std::allocator:

template <class T, class Allocator = std::allocator<T>>
class vector;

template <class T, class Allocator = std::allocator<T>>
class list;

 其他功能:

  • 解耦内存策略:将内存管理与容器实现分离

  • 灵活扩展:允许自定义内存管理策略(如内存池、共享内存等)

5.STL的优缺点是什么?设计的好的地方和不好的地方?

STL缺点:

  • 不支持线程安全(注意容器不是线程安全的)
  • 没有持续更新,增加新容器(跳表。。。)
  • 有一些冗余设计,如array,forward_list 等

STL优点:

1. 泛型编程(核心优势)

        STL通过模板实现了类型无关的通用数据结构和算法一套代码可处理任意数据类型,极大提高了代码复用率。

2. 高性能(核心竞争力)

        采用编译时多态(模板)替代运行时多态(虚函数),算法经过深度优化(如sort用内省排序),内存管理可定制(如内存池),效率接近手写C代码。

3. 组件化设计(架构优势)

        六大组件通过迭代器解耦,扩展性强且维护成本低。

  1. 空间配置器给容器提供统一的内存分配和释放接口
  2. 适配器作用于容器,通过封装现有组件并提供新的接口来满足不同的需求(stack、queue)
  3. 适配器作用于迭代器,通过封装现有组件并提供新的接口来满足不同的需求(反向迭代器)
  4. 容器提供了迭代器接口(begin、end)
  5. 迭代器用来实现了算法,算法脱离具体的存储结构
  6. 算法通过统一简单的方式:迭代器,操作各种底层不同容器
  7. 迭代器作为算法和容器之间的桥梁存在
  8. 仿函数给容器提供了元素组织方式(默认建大堆→建小堆)
  9. 仿函数给算法提供了更灵活的机制(sort默认排升序→排降序)