吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 2691|回复: 28
收起左侧

[C&C++ 原创] 【STL】手撕STL系列(一)STL简介及分配器(二合一)

  [复制链接]
jjjzw 发表于 2025-1-16 23:51
本帖最后由 jjjzw 于 2025-1-20 20:03 编辑

序言

工作之余,突觉自己编程水平有限,对数据结构、算法知之甚少,甚至C++基础也不甚牢固,决定仔细研究基础但相当重要的STL容器,以查漏补缺。
历时一个多月,从C++初学者水平,零STL基础完成了整个手撕STL的项目,感慨良多,决定制作一份详细的讲解,供大家交流讨论。

系列导航

项目地址:https://github.com/Icingworld/WW-STL
仓库代码已经补全,code review会随着系列帖子逐渐完成,欢迎大家关注交流!


一、初识 STL

STL(标准模板库)是 C++ 标准库的一个重要组成部分,是一套泛型编程组件。它提供了大量可复用的模板类和函数,帮助程序员快速实现高效的数据结构和算法。

STL 并非指某些特定的代码,而是一套抽象的接口概念,是一个严谨的接口标准,凡是严格按照 STL 标准实现的容器,都可以称为 STL 容器。

一、STL 的特点

STL 的特点是:

  • 通用性:STL 广泛使用模板,可以使得容器能存储不同的数据类型。
  • 高效性:时间方面,STL 规定了算法的时间复杂度,并使用哈希表、红黑树等作为高级容器的底层数据结构,提高了算法的效率;空间方面,STL 有自己的内存分配器,设计了动态的内存策略,提高了内存管理的效率。
  • 灵活性:STL 采用分层设计,六大组件各司其职,可以独立测试,也可以组合使用,为自定义、扩展提供了便捷。
  • 统一性:对不同的容器,STL 设计了类似的接口,可以方便地操作不同地容器,降低了学习和使用难度。

二、STL 的组成

STL 由六大组件组成,每个组件可以组合使用:

  1. 容器(container):STL 中最直观的部分,提供存储数据的容器,包括了vector、list、deque等常用数据结构。
  2. 算法(algorithm):提供操作数据结构的算法,通过迭代器来适配各种不同的数据结构,提供通用的算法操作。
  3. 迭代器(iterator):迭代器是一种自定义的指针,可以便捷地在容器中移动,对外提供访问容器的能力,也是容器和算法之间的桥梁。
  4. 仿函数(functor):仿函数是一种重载了()的类,以模仿函数的调用方式。
  5. 适配器(adaptor):适配器是一种修饰容器的策略,其底层一般使用某种容器,适配器在其基础上限制或扩展容器的功能,以达到构建某种数据结构的目的。比如,stack 和 queue 都是适配器,其底层是 deque 双端队列,stack 和 queue 通过限制数据的插入、删除,实现了栈和队列两种数据结构。
  6. 分配器(allocator):提供内存管理的策略。

三、STL 容器

STL 中,容器分为序列型容器、关联型容器、无序关联型容器和容器适配器四种

序列型容器有:

  • array:固定长度数组
  • vector:动态数组
  • deque:双端队列
  • forward_list:单向链表
  • list:双向链表

关联型容器有:

  • set:有序集合
  • multiset:有序多值集合
  • map:有序映射
  • multimap:有序多值映射

无序关联型容器有:

  • unordered_set:无序集合
  • unordered_multiset:无序多值集合
  • unordered_map:无序映射
  • unordered_multimap:无序多值映射

容器适配器有:

  • stack:栈
  • queue:队列
  • priority_queue:优先队列

二、分配器

分配器是 STL 容器的基石,一切的容器都离不开分配器,使用分配器,我们能方便地管理容器中的内存,提高了内存的管理效率,使得我们在实现容器时保证了代码的可读性和可维护性。

因此我们从分配器开始,一步步实现 STL。在本实现中,作者只实现了一个拥有基本分配能力的分配器,而 SGI STL 实现的 STL 中,分配器具有次级空间分配能力,能够根据分配空间的大小,选择如何分配空间,当空间大于128byte时,直接在堆中申请相应大小的空间,当小于128byte时,启动次级分配器,在预先维护的内存池中取出一块分配给用户。

一、分配器的实现

分配器的完整实现位于ww_memory.h中,文后亦会给出

1. 分配器的结构

template <class T>
class allocator
{
public:
    using value_type = T;
    using pointer = T*;
    using const_pointer = const T*;
    using reference = T&;
    using const_reference = const T&;
    using size_type = std::size_t;
    using difference_type = std::ptrdiff_t;
    using propagate_on_container_move_assignment = std::true_type;
    using is_always_equal = std::true_type;

    template <typename U>
    class rebind
    {
    public:
        using other = allocator<U>;
    };
};

allocator是一个模板类,用于管理数据类型为T的对象内存。

using value_type = T;
using pointer = T*;
using const_pointer = const T*;
using reference = T&;
using const_reference = const T&;
using size_type = std::size_t;
using difference_type = std::ptrdiff_t;
using propagate_on_container_move_assignment = std::true_type;
using is_always_equal = std::true_type;

这是allocator类的嵌套类型定义,用于描述allocator的成员类型,其中value_type的定义是必须的,其他定义可选,如果没有定义,allocator_traits会为其自动生成默认的嵌套类型定义。

在《STL源码剖析》一书及标准库实现中,可能采用typedef来定义别名,这和using的效果是一样的,由于using更符合现代 C++ 的风格,符合自然语言的直觉,且在支持模板上更胜一筹,本实现使用using来定义别名。

template <typename U>
class rebind
{
public:
    using other = allocator<U>;
};

rebind是一个嵌套类,它只有一个类型定义other,通过它可以将allocator重新绑定到新的类型U上去。

使用方法示例:

#include "ww_memory.h"

using int_allocator_type = wwstl::allocator<int>;   // 定义 int 类型的分配器类型

using double_allocator_type = typename int_allocator_type::template rebind<double>::other;     // 通过重新绑定声明 double 类型的分配器类型

为什么要这么做呢,在一些数据结构如 list 等,维护的并不是单纯的数据类型T,而是一个节点list_node,因此分配器的职责需要转为管理节点类,而我们在定义 list 的模板时发现:

template <
    class T,
    class Allocator = mystl::allocator<T>
> class list
{
};

Allocator是根据模板参数T已经确定了的,只能管理数据类型T,因此需要在使用前将其rebind转换为节点分配器。

2. 分配器的接口

分配器主要拥有申请空间、释放空间、构造对象、销毁对象四个功能。

  1. 申请空间:
pointer allocate(size_type n, const void * hint = nullptr)
{
    (void)hint;
    if (n > max_size())   // 超出最大尺寸
        throw std::bad_array_new_length();

    if (n == 0)
        return nullptr;

    return static_cast<pointer>(::operator new(n * sizeof(value_type)));
}

该接口接受两个参数,第一个参数n为需要申请的元素个数,第二个参数hint为提示指针,如果支持,则在申请空间时会优先申请靠近这个指针的空间,本 STL 实现忽略所有带提示的接口。

值得注意的是,这个接口中使用了::operator new运算符,它是全局作用域下的内存分配函数,是对malloc的封装,因此它和malloc一样,接受一个空间大小,返回一个void *,需要将其转换为目标pointer

在这个函数中,存在两种异常抛出,一是bad_array_new_length,当申请空间过大时抛出;二是bad_alloc,由::operator new抛出,这也是和malloc不一样的地方。

  1. 释放空间
void deallocate(pointer ptr, size_type n = 0)
{
    (void)n;
    if (ptr == nullptr)
        return;

    ::operator delete(ptr);
}

该函数使用::operator delete,它是对free的封装,接受一个指针作为参数,第二个参数n是申请时申请的元素数量,起到提醒的作用,可忽略。

  1. 构造对象
template <typename U, typename... Args>
void construct(U * ptr, Args&&... args)
{
    ::new(ptr) U(std::forward<Args>(args)...);
}

该接口接受两个参数,第一个是一个指针ptr,表明需要在什么地方构造对象,第二个参数是一个万能引用参数包,可以传入任何数量的任何参数,作为类U的构造函数的参数。

该函数中,使用的是 placement new 函数,它接受一个已经申请空间,但是未构造的指针,在该位置构造一个对象。

  1. 销毁对象
template <typename U>
void destroy(U * ptr)
{
    ptr->~U();
}

该接口接受一个指针参数,传入一个由 placement new 构造的对象,调用它的析构函数,注意它并不会释放被占用的空间,只是将内存恢复为申请后的状态。

  1. 思考

这四个函数涵盖了 C++ 常用的内存管理机制,我们常用 new 和 delete 来进行动态分配,实际上,当我们使用 new operator 时,首先申请了一个元素大小的空间,随后在该空间上构造对象;当我们使用 delete operator 时,首先调用了类的析构函数,随后释放该空间。

int * p = new int(7);   // 申请空间并构造

delete p;               // 析构销毁并释放

3. 其他接口

allocator的实现中,引人注目的还有这两个重载:

bool operator==(const allocator & other) const noexcept
{
    return true;
}

bool operator!=(const allocator & other) const noexcept
{
    return false;
}

实际上,分配器是一个无状态的类,无论是什么模板参数的分配器,它们总是相等的,这一点在嵌套类型定义中可见:

using is_always_equal = std::true_type;

完整代码

#ifndef __WW_MEMORY_H__
#define __WW_MEMORY_H__

#include <limits>
#include <stdexcept>

namespace wwstl
{

/**
 * @brief 分配器
 * @link https://zh.cppreference.com/w/cpp/memory/allocator
 */
template <class T>
class allocator
{
public:
    using value_type = T;
    using pointer = T*;
    using const_pointer = const T*;
    using reference = T&;
    using const_reference = const T&;
    using size_type = std::size_t;
    using difference_type = std::ptrdiff_t;
    using propagate_on_container_move_assignment = std::true_type;
    using is_always_equal = std::true_type;

    template <typename U>
    class rebind
    {
    public:
        using other = allocator<U>;
    };

public:
    allocator() noexcept = default;

    allocator(const allocator & other) noexcept = default;

    template <typename U>
    allocator(const allocator<U> & other) noexcept 
    { // template cannot be default
    };

    ~allocator() = default;

public:
    /**
     * @brief 分配n个元素的内存
     * @Param n 元素个数
     * @param hint 会在hint附近分配内存,忽略
     * @Return pointer 内存指针
     * @exception std::bad_array_new_length 超出最大尺寸
     * @exception std::bad_alloc 内存分配失败
     */
    pointer allocate(size_type n, const void * hint = nullptr)
    {
        (void)hint;
        if (n > max_size())   // 超出最大尺寸
            throw std::bad_array_new_length();

        if (n == 0)
            return nullptr;

        return static_cast<pointer>(::operator new(n * sizeof(value_type)));
    }

    /**
     * @brief 释放由allocate分配的内存
     * @param ptr 要释放的内存指针
     * @param n 元素个数,忽略
     */
    void deallocate(pointer ptr, size_type n = 0)
    {
        (void)n;
        if (ptr == nullptr)
            return;

        ::operator delete(ptr);
    }

    /**
     * @brief 构造对象
     * @param ptr 要构造的内存指针
     * @param args 构造函数参数包
     */
    template <typename U, typename... Args>
    void construct(U * ptr, Args&&... args)
    { ::new(ptr) U(std::forward<Args>(args)...); }

    /**
     * @brief 销毁对象
     * @param ptr 要销毁的内存指针
     */
    template <typename U>
    void destroy(U * ptr)
    { ptr->~U(); }

    size_type max_size() const noexcept
    { return std::numeric_limits<std::size_t>::max() / sizeof(value_type); }

    pointer address(reference r) const noexcept
    { return &r; }

    const_pointer address(const_reference r) const noexcept
    { return &r; }

    bool operator==(const allocator & other) const noexcept
    { return true; }

    bool operator!=(const allocator & other) const noexcept
    { return false; }
};

/**
 * @brief void类型特化
 * @details 为void类型的分配器提供类型支持,但不提供实现
 */
template <>
class allocator<void>
{
public:
    using value_type = void;
    using pointer = void*;
    using const_pointer = const void*;
    using propagate_on_container_move_assignment = std::true_type;
    using is_always_equal = std::true_type;
};

} // namespace wwstl

#endif // __WW_MEMORY_H__

免费评分

参与人数 13威望 +2 吾爱币 +113 热心值 +11 收起 理由
BronzeAge + 1 + 1 谢谢@Thanks!
jinyang + 1 谢谢@Thanks!
yumuchuxin + 1 + 1 谢谢@Thanks!
ColorExists + 1 + 1 谢谢@Thanks!
雷欧库珀 + 2 + 1 用心讨论,共获提升!
fault + 1 + 1 谢谢@Thanks!
jackies + 1 + 1 热心回复!
klop + 1 + 1 提个建议,讲解stl,最好从元模板,元数据讲起更好,这是stl的基础
iceSleeping + 1 + 1 我很赞同!
苏紫方璇 + 2 + 100 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
AuroraVerses + 1 + 1 我很赞同!
genius11 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
liuqm + 1 + 1 用心讨论,共获提升!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

 楼主| jjjzw 发表于 2025-1-17 13:50
bester 发表于 2025-1-17 13:13
大佬,手撕STL看谁的视频哦?

没有看过特别好的系列视频,买过代码随想录的手撕STL项目,非常一般,想看视频可以b站单独搜如 vector实现、红黑树实现 等,把算法融合到STL框架里
我是看了侯捷的《STL源码剖析》,了解STL的具体架构和基本思想,书虽然有点老了,但是学习源码还是不错的
然后看cppreference获取C++的最新接口标准,其中也会有一些关于实现的提示
在设计具体容器、算法的时候,会借鉴gcc和msvc的实现,这部分可以通过安装具体编译器或者在GitHub上找到

免费评分

参与人数 1吾爱币 +1 热心值 +1 收起 理由
bester + 1 + 1 热心回复!

查看全部评分

BrutusScipio 发表于 2025-1-21 10:27
jjjzw 发表于 2025-1-17 13:50
[md]没有看过特别好的系列视频,买过代码随想录的手撕STL项目,非常一般,想看视频可以b站单独搜如 vec ...

标准库为了兼容在性能上做了一点取舍 大型项目一般用自己特化的标准库 手写的话建议把一些设计上的考虑也写下来,为什么要用一种方案而不用另一种方案 搞明白这一点还能解释清楚就差不多了
lidaquan2188 发表于 2025-1-16 23:57
liuqm 发表于 2025-1-17 05:46
感谢分享
lypxynok 发表于 2025-1-17 08:24
一起学习,感谢楼主
kongson 发表于 2025-1-17 08:38
这个可以,备用了,谢谢
bester 发表于 2025-1-17 13:13
大佬,手撕STL看谁的视频哦?
AuroraVerses 发表于 2025-1-17 20:34
感谢分享!
adevour 发表于 2025-1-20 00:49
学到了学到了
zhaijing521 发表于 2025-1-20 20:15
感谢楼主分享,收获很大!
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2025-7-10 13:49

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表