STL分析

一. 配置器

配置器(Allocator) 负责为容器(如 vector、list 等)分配和释放内存,以及对象的构造和析构。
这也是为什么vector等容器不用显式的回收内存,采用的是通过类对象来创建回收资源的RALL思想

1. 类模板的使用

类模板是一种可以在声明时不指定具体类型的方式,使得类可以在多个不同类型的场景下复用。

在代码中,allocator使用了模板参数 T 来表示类型。例如:

template <class T>
class allocator
{
  // 类中的代码
};

通过template <class T>allocator可以处理不同的数据类型,具体类型在实例化时才决定,比如 allocator<int> 会针对 int 类型分配内存。

此外,在类外定义成员函数时,仍需使用 template <class T> 再次声明,这是模板类成员函数的标准写法:

template <class T>
T* allocator<T>::allocate()
{
  return static_cast<T*>(::operator new(sizeof(T)));  // 为 T 类型分配内存
}

这样,在实例化时,可以为 T 类型的对象分配内存,类型 T 是灵活变化的。

2. 命名空间的使用

命名空间用于防止不同代码块之间的命名冲突。在你的代码中,mystl命名空间包含了allocator类,避免了与其他库中的allocator命名冲突:

namespace mystl
{
  template <class T>
  class allocator
  {
    // 内存分配器的定义
  };
}

mystl命名空间表明该分配器是属于用户自定义的,并且可以在不同的代码库中独立存在。使用时可以通过mystl::allocator来访问该类,避免与其他库中的 allocator 类混淆。

3. 可变参数模板与完美转发

可变参数模板是C++11引入的一项功能,允许模板接受不定数量的模板参数和函数参数。与传统的C语言可变参数不同,可变参数模板支持类型安全。

在代码中,allocator类使用了可变参数模板来处理构造函数的不同调用方式:

template <class... Args>
static void construct(T* ptr, Args&& ...args)
{
  mystl::construct(ptr, mystl::forward<Args>(args)...);  // 完美转发
}

template <class... Args> 定义了可以接受任意类型和数量参数的模板函数。Args&& 表示参数包,这里使用了 万能引用 来接收所有参数,包括左值和右值。

mystl::forward<Args>(args)... 通过 完美转发 保留了参数的原始类型性质(左值、右值),并将它们传递给下游的构造函数。这种方式确保了高效的参数传递,避免不必要的拷贝。

4. 四种类型的重载

allocator中的construct函数展示了四种不同的重载方式,每种重载都适用于不同的场景:

// 1. 默认构造
static void construct(T* ptr)
{
  mystl::construct(ptr);  // 调用默认构造函数
}

// 2. 拷贝构造
static void construct(T* ptr, const T& value)
{
  mystl::construct(ptr, value);  // 拷贝构造
}

// 3. 移动构造
static void construct(T* ptr, T&& value)
{
  mystl::construct(ptr, mystl::move(value));  // 移动构造
}

// 4. 可变参数构造
template <class... Args>
static void construct(T* ptr, Args&& ...args)
{
  mystl::construct(ptr, mystl::forward<Args>(args)...);  // 完美转发构造
}

5. 内存分配和释放

allocator使用 ::operator new::operator delete 来分配和释放内存。值得注意的是,前面的 :: 是作用域解析运算符,用来明确调用的是全局的 operator newoperator delete

// 分配单个对象的内存
static T* allocate()
{
  return static_cast<T*>(::operator new(sizeof(T)));  // 使用全局 new 分配
}

// 分配多个对象的内存
static T* allocate(size_type n)
{
  if (n == 0) return nullptr;
  return static_cast<T*>(::operator new(n * sizeof(T)));  // 分配 n 个对象的内存
}

// 释放单个对象的内存
static void deallocate(T* ptr)
{
  if (ptr == nullptr) return;
  ::operator delete(ptr);  // 使用全局 delete 释放内存
}

这里的 ::operator new::operator delete 用于手动分配和释放原始内存(没有调用构造函数和析构函数),这与C++标准库 newdelete 不同,它们同时会调用构造函数和析构函数。

6. 构造与内存分配的分离:灵活与高效

在C++中,内存分配和对象构造的分离有其明确的优势,特别是在复杂的内存管理场景下:

  • 延迟构造:通过先分配内存,再在需要时构造对象,提供了更灵活的内存和对象管理方式。例如,你可以分配一块内存但等到特定时刻才开始构造对象。
  • 批量管理:内存分配的开销通常比构造和销毁对象大得多,将分配与构造分离允许在分配一次内存后,在该内存中多次构造和销毁对象,优化了性能。
  • 优化资源回收:你可以销毁对象后保留分配好的内存,等待下次构造操作使用,这在内存池或容器中非常常见。它减少了频繁分配和释放内存的开销,提升了资源管理效率。

7. 析构与内存释放的分离

与构造和内存分配类似,析构对象和释放内存的分离也有重要意义:

  • 灵活销毁:有时你可能只想销毁对象,而暂时不释放内存。例如,预先分配好一块大内存块,并在其上反复构造和销毁对象,这在内存池中非常常见。
  • 批量释放:在某些场景中,你可能不需要一个一个地销毁对象(特别是当对象类型为 平凡析构 时),而是直接释放整块内存。通过将 destroydeallocate 分开,可以选择是否显式调用析构函数。
  • 异常安全性:析构与释放的分离允许在异常发生时,能够选择性地销毁对象,而确保内存能够被安全地回收,避免内存泄漏。

8. 定位 new(Placement new):内存控制的关键工具

construct 函数中,你看到了 placement new 的使用:

::new ((void*)ptr) Ty(mystl::forward<Args>(args)...);
  • 定位 newplacement new 是一种特殊的 new 运算符,它不分配新的内存,而是在已有的内存位置(通过指针 ptr 提供)上构造对象。
    这个技术是分离内存分配与对象构造的基础,允许开发者在预先分配的内存中手动调用构造函数。

使用 placement new 的原因是它赋予了开发者精确的内存控制能力,可以在同一块内存中反复构造和销毁对象,而不涉及频繁的内存分配和释放操作。它广泛用于内存池、STL容器和自定义分配器中。

9. 平凡与非平凡析构对象的差异化处理

destroy 函数中,类型特征被用来区分 平凡析构非平凡析构 对象:

template <class Ty>
void destroy(Ty* pointer)
{
  destroy_one(pointer, std::is_trivially_destructible<Ty>{});
}
  • 平凡析构trivially destructible)对象是指那些不需要显式调用析构函数的对象,例如基本类型(intfloat)或者不含有资源管理的类。这类对象的内存可以直接释放,而无需调用析构函数。
  • 非平凡析构对象则需要显式调用析构函数来释放资源(如指针、文件句柄等)。

通过区分这两类对象,代码在处理平凡类型时可以跳过析构步骤,提升效率。这种优化广泛应用于标准库的实现中,比如 std::vector 在处理基础类型时,就会避免不必要的析构调用。

  • std::is_trivially_destructible<T> 利用 模板特化 来判断类型 T 是否是平凡析构类型,返回 std::true_typestd::false_type,通过类型推导和模板实例化在 编译期 选择适当的函数重载,这体现了 静态多态 的思想。

  • 泛型编程允许代码对任意类型进行操作,提升了代码复用性;模板特化则提供了一种机制,根据具体类型在编译期生成特定实现;类型特征(如 std::is_trivially_destructible)通过类型萃取技术,在编译期获取类型的特性,从而实现高效、类型安全的程序设计。

10. 批量销毁中的迭代器支持

在批量销毁时,代码提供了对迭代器的支持,允许你销毁一段范围内的对象:

template <class ForwardIter>
void destroy(ForwardIter first, ForwardIter last)
{
  destroy_cat(first, last, std::is_trivially_destructible<
              typename iterator_traits<ForwardIter>::value_type>{});
}
  • 迭代器:通过接受两个迭代器 firstlast,可以销毁一段内存区域内的所有对象。
  • 优化:同样地,使用 std::is_trivially_destructible 来判断迭代器指向的对象是否需要销毁,可以进一步优化性能。

这种设计灵活性使得 destroy 函数能够轻松处理大多数标准容器(如 std::vectorstd::list)中的对象销毁操作,同时保证高效的内存管理。

二. 迭代器

迭代器的总结

在 C++ 中,迭代器是一个非常重要的概念,它们是容器与算法之间的桥梁,用来遍历容器中的元素。迭代器的设计使得同一套算法可以适用于不同类型的容器。迭代器的基本功能包括:

  • 遍历容器中的元素:通过迭代器可以逐个访问容器中的元素。
  • 支持前进、后退、随机访问等操作:不同类型的迭代器支持不同的操作,如输入迭代器支持读取,双向迭代器支持前进和后退,随机访问迭代器则可以直接通过索引访问元素。
  • 兼容性:C++ 标准库中设计的各种算法(如 std::sortstd::find)都是基于迭代器实现的,支持多种容器(如 vectorlistmap)的遍历。

迭代器可以分为五种类型:

  1. 输入迭代器(Input Iterator):只能读取元素,常见于输入流。
  2. 输出迭代器(Output Iterator):只能写入元素,常见于输出流。
  3. 前向迭代器(Forward Iterator):可以读取、写入,并且只能前向遍历。
  4. 双向迭代器(Bidirectional Iterator):可以前向和后退遍历。
  5. 随机访问迭代器(Random Access Iterator):支持常数时间内的随机访问,类似于数组的指针。

实现迭代器的理解

实现迭代器的核心思想是通过类型萃取和模板技术,使得不同容器的迭代器能够提供统一的接口,便于在泛型算法中进行操作。迭代器实现的核心功能包括:

  • 遍历容器:通过 ++-- 操作符前进和后退,或者通过 +=-= 在容器中移动。
  • 访问元素:通过 *-> 操作符访问迭代器当前指向的元素。
  • 计算迭代器之间的距离:不同类型的迭代器支持不同的距离计算方式。
  • 迭代器分类:通过 iterator_category,使算法能够区分不同的迭代器类型,并根据迭代器的能力选择最优的操作方式。

迭代器的基本接口:

  • iterator_traits:用于萃取迭代器的类型信息,如迭代器的类型、指针类型、引用类型、差值类型等。
  • advance 函数:让迭代器前进或后退特定的步数。
  • distance 函数:计算两个迭代器之间的距离。
  • reverse_iterator:实现反向迭代器,使得前进操作变为后退,后退变为前进,通常用于从后向前遍历容器。

串联用到的知识与功能实现

  1. 模板编程与泛型设计
    迭代器的实现大量依赖模板技术。通过模板编程,我们可以实现一个支持多种类型的迭代器,而无需为每种类型单独定义迭代器。模板允许迭代器对容器中的任意类型进行操作,并且在编译期确定具体的类型和操作方式。

  2. 类型萃取(Type Traits)
    iterator_traits 是类型萃取的核心,它用于从迭代器中提取类型信息,如 value_typepointerreference 等。这样可以让算法和迭代器分离,算法只需要知道如何通过类型萃取器获取迭代器的信息,而不需要关心迭代器的具体实现。

    例如

    typedef typename iterator_traits<Iterator>::iterator_category iterator_category;
    

    这行代码提取了 Iterator 迭代器的类型标签,让算法可以根据 iterator_category 选择合适的操作。

  3. 模板特化与静态多态
    迭代器的设计中使用了大量的模板特化,以实现针对不同迭代器类型的优化。例如,针对 random_access_iterator_tag,我们可以直接通过算术运算符计算迭代器的距离,而对其他类型的迭代器则需要逐个遍历计算。

    例如,对于 random_access_iterator_tag

    template <class RandomIter>
    typename iterator_traits<RandomIter>::difference_type
    distance_dispatch(RandomIter first, RandomIter last, random_access_iterator_tag) {
      return last - first;
    }
    
  4. 静态多态与编译期优化
    C++ 的模板和类型萃取机制使得迭代器的多态行为可以在 编译期 确定,而不是像传统的虚函数那样在运行时进行多态操作。这大大提升了性能,因为所有的类型判断和函数选择都在编译期间完成。

  5. 反向迭代器
    reverse_iterator 是 STL 中非常常见的迭代器类型,它通过包装正向迭代器,将前进变为后退,后退变为前进,从而实现反向遍历的功能。

    例如

    template <class Iterator>
    class reverse_iterator {
    private:
      Iterator current;
    public:
      reverse_iterator& operator++() {
        --current;
        return *this;
      }
      reverse_iterator& operator--() {
        ++current;
        return *this;
      }
    };
    

    reverse_iterator 通过重载 ++-- 操作符改变正向迭代器的遍历方向。

结论

实现迭代器的复杂性在于如何提供一个泛型、灵活且高效的遍历接口。通过模板编程、类型萃取和模板特化,C++ 标准库成功实现了一个强大且通用的迭代器机制。每个迭代器可以根据不同容器的需求提供不同的功能,同时通过 iterator_traits 进行类型萃取,保证算法和迭代器的解耦,并且利用静态多态和编译期优化提升性能。

三. 算法

通过迭代器以及模板进行简单操作即可
也就是通过迭代器内容里面的val值,进行操作
包括常用的max_element、min_elememt、count、find、upper_bound、lower_bound、accumulate、iota、max等

四. 容器(基本数据结构)

1. vector

首先是三个迭代器,分别是头部、尾部、储存空间尾部
四种构造方式

  • 默认构造16格大小,同时初始化三个指针,头部指针和尾部指针在一起,指向使用配置器新建的空间,储存空间尾部后移16;
  • 指定空间的大小,使用配置器初始化,也就是分配空间,同时初始化指针,储存空间尾部指针为指定和16中的较大值
  • 指定空间大小,同时指定值,这里会根据类型萃取决定是否调用对应构造函数,平凡析构对象直接使用memset
  • 指定另一个迭代器的两个指针,根据距离指定空间大小,根据类型萃取决定是memove还是默认拷贝构造函数复制

进行扩容:首先根据添加元素数量获取需要扩容的大小,一般是扩张1.5倍
擦除元素:析构掉对应区域元素,然后使用迭代器双指针移动接管资源,同时改变尾指针
指定空间大小赋值:如果容量不足,直接新建交换,容量足够但超过尾指针,填充并移动指针,没超过尾指针擦除
插入元素:移动语义
扩容:移动语义
交换元素:交换头指针、尾指针、存储空间尾部指针
重置容器大小(逻辑上):resize根据大小,如果比原来小,直接擦除erase,比原来大,大的部分insert
重置内存大小(物理上):reserve,只有实际容量比指定大小小的时候才会操作,新建内存,移动语义,析构,改变指针
push_back和emplace_back:前者只能添加一个元素,后者可以添加变长参数列表

2. map

红黑树

3. unordered_map

哈希表

五. 配接器(特殊接口)

配接器是一种特殊的包装器(wrapper),它们并不提供新的数据结构,而是基于现有的容器提供了一个不同的接口,适应特定的使用场景,配接器的核心思想是将现有容器进行功能包装,使其适应特定的数据访问方式。

1. stack

使用双向队列deque作为底层容器,提供接口

2. queue

缺省使用deque

3. priority_queue

缺省使用vector

posted @ 2024-09-26 00:15  失控D大白兔  阅读(5)  评论(0编辑  收藏  举报