C++ STL全面解析:六大核心组件之一----分配器(STL进阶学习)

目录

分配器(Allocators)

[new 和 delete](#new 和 delete)

构造和析构

内存的配置和释放

构造和析构过程

空间配置与释放的过程

第一配置器源码分析

第二配置器源码分析

[空间配置函数 allocate](#空间配置函数 allocate)

内存池


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

分配器(Allocators)

分配器负责管理内存的分配和释放。STL 提供了 std::allocator 类模板,用于为容器分配和释放内存。分配器的设计是为了提高内存分配的效率,并且可以被重载以适应特定的应用需求。在STL的角度来看,分配器是一个非常隐秘的组件,它默默地在后台工作,精确地分配和控制内存空间。当我们深入了解STL时,分配器是我们首先应该关注的,因为它掌管着最重要的数据空间。

注:以下内容部分参考自《STL源码剖析》。

newdelete

一般来说,我们通常使用 newdelete 来分配和释放内存,例如:

cpp 复制代码
class F {};
F *f = new F;
delete f;

这里的 new 操作包含了两个阶段:

  1. 调用 ::operator new 分配内存
  2. 调用 class F 的构造函数构造内容

delete 操作则是相反的过程,即 new 变为 delete,构造变为析构。

为了更精细地分工,allocator 将这两个阶段的操作区分开来:

  • 内存分配操作由 alloc::allocate() 负责
  • 内存释放操作由 alloc::deallocate() 负责
  • 对象构造由 ::construct() 负责
  • 对象析构操作由 ::destroy() 负责

构造和析构

stl_construct.h 定义了两个基本函数 constructdestroy,它们分别用于对象的构造和析构。具体来说:

  • construct:用于在分配的内存中构造对象。
  • destroy:用于在分配的内存中析构对象。

内存的配置和释放

对象构造前的空间配置和对象析构后的空间释放由 stl_alloc.h 负责。设计时考虑了以下几点:

  1. 向系统堆请求空间
  2. 考虑多线程状态
  3. 考虑内存不足时的应变措施
  4. 考虑过多小型区块可能造成的内存碎片

构造和析构过程

空间配置与释放的过程

C++的内存配置基本操作是 ::operator new(),内存释放的基本操作是 ::operator delete()。这两个全局函数相当于C语言中的malloc() 和 free()函数。

考虑到小型区块所可能造成的内存破碎的问题,C++设计了双层的配置器,第一层则是由malloc和free来完成。第二层则较为复杂以下,会根据不同的情况采取不同的策略。

当配置区块大小超过128bytes时,视为大,调用一级配置器,也就是malloc和free。

当配置区块小于128bytes时,则视为小,为了降低负担则使用memory pool整理方式,而不再求助于一级配置器。其中还配置了一级配置器和二级配置器的开放权限。关键在于__USE_MALLOC. (这名字还真是朴实无华-.-)

其中 __malloc_alloc_template就是第一级的配置器,__default开头的就是第二季的配置器。

他们的关系如下图:

第一配置器源码分析

第一配置器以malloc,free,realloc等C函数执行实际的内存配置,释放等操作。

cpp 复制代码
#include <new>
#include <iostream>

// 定义内存不足时的异常处理
#ifndef THROW_BAD_ALLOC
#define THROW_BAD_ALLOC std::cerr << "Out of memory" << std::endl; exit(1)
#endif

// malloc-based allocator,通常比稍后介绍的 default allocator 速度慢
// 通常线程安全,并且对于空间的运用比较高效
template <int inst>
class malloc_alloc_template {
private:
    // 以下都是函数指针,所代表的函数将用来处理内存不足的情况
    static void* (*oom_malloc)(size_t);
    static void* (*oom_realloc)(void*, size_t);
    static void (*oom_free)(void*);

public:
    // 第一级配置器直接使用 malloc()
    static void* allocate(size_t n) {
        void* result = ::malloc(n);
        if (0 == result) {
            result = oom_malloc(n);
        }
        return result;
    }

    // 第一级配置器直接使用 free()
    static void deallocate(void* p, size_t /* n */) {
        ::free(p);
    }

    // 第一级配置器直接使用 realloc()
    static void* reallocate(void* p, size_t /* old_sz */, size_t new_sz) {
        void* result = ::realloc(p, new_sz);
        if (0 == result) {
            result = oom_realloc(p, new_sz);
        }
        return result;
    }

    // 以下仿真 C++ 的 set_new_handler。换句话说,你可以通过它指定你自己的 out-of-memory handler
    static void (*set_malloc_handler(void (*f)())) {
        void (*old)(void) = malloc_alloc_oom_handler;
        malloc_alloc_oom_handler = f;
        return old;
    }

    // malloc_alloc out-of-memory handling 初值为0,有待客户端设定
    static void (*malloc_alloc_oom_handler)();
};

// 定义内存不足时的处理函数
template <int inst>
void* malloc_alloc_template<inst>::oom_malloc(size_t n) {
    void (*my_malloc_handler)();
    void* result = nullptr;

    while (true) {
        my_malloc_handler = malloc_alloc_oom_handler;
        if (0 == my_malloc_handler) {
            THROW_BAD_ALLOC;
        }

        // 调用处理例程,企图释放内存
        (*my_malloc_handler)();

        // 再次尝试配置内存
        result = ::malloc(n);
        if (result) {
            return result;
        }
    }
}

template <int inst>
void* malloc_alloc_template<inst>::oom_realloc(void* p, size_t n) {
    void (*my_malloc_handler)();
    void* result = nullptr;

    while (true) {
        my_malloc_handler = malloc_alloc_oom_handler;
        if (0 == my_malloc_handler) {
            THROW_BAD_ALLOC;
        }

        // 调用处理例程,企图释放内存
        (*my_malloc_handler)();

        // 再次尝试配置内存
        result = ::realloc(p, n);
        if (result) {
            return result;
        }
    }
}

// 注意,以下直接将参数 inst 指定为 0
typedef malloc_alloc_template<0> malloc_alloc;
第二配置器源码分析

第二级配置器多了一些机制,主要是为了避免太多小额区块造成的内存碎片,小额区块带来的其实不仅是内存碎片,配置时的额外负担也是一个大问题。额外的空间无法避免,毕竟系统要靠这多出来的进行管理内存。所以当区域块越小是,这块空间就越来越显得浪费。

SGI第二级配置器的做法是,如果区块够大,超过128byies时,就移交第一级配置器处理。当区块小于128bytes时,则以内存池(memorypool)管理,此法又称为次层配置(sub-allocation):每次配置一大块内存,并维护对应之自由链表(free-lisr)。下次若再有相同大小的内存需求,就直接从fee-lists中拨出。如果客端释还小额区块,就由配置器回收到free-lists中--是的,别忘了,配置器除了负责配置,也负责回收,为了方便管理,SGI第二级配置器会主动将任何小额区块的内存需求量上调至8的倍数(例如客端要求30bytes,就自动调整为32bytes),并维护 16个/ree-lisrs,各自管理大小分别为8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128 bytes 的小额区块。

大佬的想法当然不止于此,为了节省内存空姐到极致,大佬也对指针下了手。每个指针都要指向一个链表那岂不是很浪费!所以为了节省,采用了union。

这样的好处就是最大限度的去节省内存,由于Uinon中可以允许不通类型的元素存在,所以它正好可以维护整个链表,而不会局限于类型。当内存块被分配给客户端时,只需要将 free_list_link 清零或设置为无效指针,就可以将其转换为客户数据存储区。反之亦然,当内存块被释放时,可以将其转换回链表节点。

空间配置函数 allocate

作为一个配置器,__default_alloc_template拥有配置器的标准接口函数allocate,此函数会先去判断区块大小,大于128就调第一级配置器,否则就检查对应的free-list,如果free-list有可用区块就会直接拿来用,否则九八区块大小上调的8的倍数。然后使用refill味free list填充空间。

空间释放函数 deallocate()

这个函数则是比较简单一些,老样子还是先检查区块大小,如果超过128那就直接去找一级配置器进行回收,小于128则找出对应free-list进行回收。

内存池

这也是内存分配中的一个很重要的点,它的职责就是从储存内存。而chunk_alloc是负责调用内存池的一个函数,主要是负责给free-list进行空间配置。

cpp 复制代码
// 分配内存
    char* chunk_alloc(size_t size, int& nobjs) {
        size_t total_bytes = size * nobjs;
        size_t bytes_left = end_free - start_free;

        if (bytes_left >= total_bytes) {
            // 内存池剩余空间完全满足需求量
            char* result = start_free;
            start_free += total_bytes;
            return result;
        } else if (bytes_left >= size) {
            // 内存池剩余空间不能完全满足需求量,但足够供应一个(含)以上的区块
            nobjs = bytes_left / size;
            total_bytes = size * nobjs;
            char* result = start_free;
            start_free += total_bytes;
            return result;
        } else {
            // 内存池剩余空间连一个区块的大小都无法提供
            size_t bytes_to_get = 2 * total_bytes + (heap_size >> 4);

            if (bytes_left > 0) {
                // 内存池内还有一些零头,先配给适当的 free list
                int index = free_list_index(bytes_left);
                Obj* volatile* my_free_list = &free_lists[index];
                Obj* obj = reinterpret_cast<Obj*>(start_free);
                obj->free_list_link = *my_free_list;
                *my_free_list = obj;
            }

            // 配置 heap 空间,用来补充内存池
            start_free = reinterpret_cast<char*>(::malloc(bytes_to_get));
            if (start_free == nullptr) {
                // heap 空间不足,malloc() 失败
                // 尝试从自由链表中获取内存
                for (size_t i = size; i <= BLOCK_SIZE; i += ALIGN) {
                    int index = free_list_index(i);
                    Obj* volatile* my_free_list = &free_lists[index];
                    Obj* p = *my_free_list;
                    if (p != nullptr) {
                        *my_free_list = p->free_list_link;
                        start_free = reinterpret_cast<char*>(p);
                        end_free = start_free + i;
                        return chunk_alloc(size, nobjs);
                    }
                }

                // 调用第一级配置器,看看 out-of-memory 机制能否尽点力
                start_free = reinterpret_cast<char*>(malloc_alloc::allocate(bytes_to_get));
                if (start_free == nullptr) {
                    throw std::bad_alloc();
                }
            }

            heap_size += bytes_to_get;
            end_free = start_free + bytes_to_get;
            return chunk_alloc(size, nobjs);
        }
    }

chunk_alloc函数通过end_free - start_free去判断内存池剩余的内存数量,如果数量充足,那么则直接20个区块返回的free-list。如果不足的话但是还足够供应一个以上的区块,就返回剩余的进行。如果连一个以上的都不够了那就就需要去malloc去申请内存了。

总体来说,如果满足20个,那就返回,如果不满足,但是够用,仍然返回,如果不够,那就返回剩下的,剩下的回去再申请。当然可能申请的会多一些。但是如果malloc也申请不来,那就真的是山穷水尽了,它就会四处寻找是否有可用的内存。如果实在没有那就会发出bad_alloc这个异常了。

内存处理工具:

STL 中定义了五个全局函数,用于操作未初始化的空间。这些函数对于容器的实现非常有帮助,因为它们允许在不初始化内存的情况下直接构造或填充对象。这些函数分别是 constructdestroyuninitialized_copyuninitialized_filluninitialized_fill_n。这些函数在容器的实现中起着至关重要的作用。但是后三个我也没看明白说实话,留给有缘人探索了就。

以上均为笔者在阅读《STL源码剖析》时的学习笔记,并非自己探索。但是读书嘛,确实时有些时候时难以理解,这篇笔记中有笔者的一些独自理解,希望可以帮到大家,希望大家给点个关注吧~

相关推荐
yuyanjingtao12 分钟前
CCF-GESP 等级考试 2023年12月认证C++三级真题解析
c++·青少年编程·gesp·csp-j/s·编程等级考试
雨颜纸伞(hzs)14 分钟前
C语言介绍
c语言·开发语言·软件工程
J总裁的小芒果16 分钟前
THREE.js 入门(六) 纹理、uv坐标
开发语言·javascript·uv
坊钰44 分钟前
【Java 数据结构】移除链表元素
java·开发语言·数据结构·学习·链表
chenziang11 小时前
leetcode hot100 LRU缓存
java·开发语言
时雨h1 小时前
RuoYi-ue前端分离版部署流程
java·开发语言·前端
云计算DevOps-韩老师1 小时前
【网络云计算】2024第52周-每日【2024/12/25】小测-理论&实操-自己构造场景,写5个系统管理的脚本-解析
开发语言·网络·云计算·bash·perl
暮色尽染1 小时前
Python 正则表达式
开发语言·python
IT猿手1 小时前
最新高性能多目标优化算法:多目标麋鹿优化算法(MOEHO)求解GLSMOP1-GLSMOP9及工程应用---盘式制动器设计,提供完整MATLAB代码
开发语言·算法·机器学习·matlab·强化学习
小爬虫程序猿1 小时前
利用Java爬虫获取速卖通(AliExpress)商品详情的详细指南
java·开发语言·爬虫