stl源码剖析_STL源码剖析——allocator分配器(上)

0757c7e60790c6452a915c6c8a219208.png

写在前面

最近在阅读候捷老师的《STL源码剖析》,阅读到空间配置器(allocator)这一章,觉得非常受用。这篇文章就当作自己的学习笔记,

下面我会从头到尾梳理一遍 allocator 的重点难点,希望对这方面不了解的读者看完后有所收获。

什么是 allocator?allocator 有什么用?

我们需要对 C++ 的 allocator 的堆内存接口调用顺序有个清晰的认识,如下图所示。

1cfd9af53fe9f24a12dcc0d6e89f2409.png
allocator 堆内存管理接口

allocator 堆内存管理接口 STL 的容器(eg: vector、stack、deque等)有一个共同特征,就是它们的大小可以在程序运行时改变。通俗点说就是当我们想要往容器中加东西的时候,容器的内存就会自动扩充,不需要提前设定好内存的大小。这种内存分配方式称为动态内存分配,而 allocator 正是用于动态内存的分配释放

有必要自己实现 allocator 吗?

在我们使用容器的时候,一般不需要我们自己去实现 allocator,程序会调用默认的 std::allocator 来动态分配内存。但是有些情况需要我们自定义 allocator,比如:

  1. 有些嵌入式平台没有提供默认的 malloc/free 等底层内存管理函数,你需要继承 std::allocator,并封装自定义版本的 malloc/free 等更底层的堆内存管理函数。

2. 使用 C++ 实现自己的数据结构,有时我们需要扩展(继承) std::allocator

3. 大部分用 C++ 写的游戏程序都有自己重新实现的 allocator。

自定义 allocator 的难点

代码就是《STL源码剖析》上的代码,自己添加了一些注释。另外代码中有几个难点值得我们探讨。

难点一:

template <class T>
inline T* _allocate(ptrdiff_t size, T*) {
    std::set_new_handler(0);  // 分配失败,抛出std::bad_alloc

    // 空间的分配实现,调用 ::operator new() 全局函数
    T* tmp = (T*)(::operator new((size_t)(size * sizeof(T))));

    if (tmp == 0) {
        std::cerr << "out of memory" << std::endl;
        exit(1);
    }

    return tmp;
}

我们来看看这段代码的第 3 行:

std::set_new_handler(0);

在解释 set_new_handler 函数之前,我们先来看一段标准库函数声明。

namespace std{
    typedef void (*new_handler)();
    new_handler set_new_handler(new_handler p) throw();
}

从声明中可以看出,new_handler 是个 typedef,定义出一个指针指向函数,该函数没有参数也没有返回值。

而 set_new_handler 是“获得一个 new_handler 并返回一个 new_handler”的函数。

这样就很清晰了,在 operator new 分配内存失败抛出一个异常之前,会先调用一个错误处理函数(即 new_handler),但是 new_handler 无参数无返回,所以需要调用 set_new_handler 函数来获取和返回 new_handler。

难点二:

template <class T1, class T2>
inline void _construct(T1* p, const T2& value)
{
    new(p) T1(value); // placement new
}

代码里的第 4 行比较少见,它是一个重载函数,叫作 placement new。

再解释之前,我们先来区分一下 new、operator new、placement 究竟有什么区别?

new operator 是一个我们熟悉的 new,不可以重载,作用是调用 operator new 申请内存,并初始化一般用户调用。

operator new 是重载函数,一般在类中进行重载。如果类中没有重载 operator new,那么调用的就是全局的 ::operator new 来完成堆的分配。

placement new 是 operator new 的一个重载版本,只是我们很少用到它。如果你想在已经分配的内存中创建一个对象,使用 new 是不行的。也就是说 placement new 允许你在一个已经分配好的内存中构造一个新的对象

我们知道使用 new 操作符分配内存需要在堆中查找足够大的剩余空间,这个操作速度是很慢的,而且有可能出现无法分配内存的异常(空间不够)。

placement new 就可以解决这个问题。我们构造对象都是在一个预先准备好了的内存缓冲区中进行,不需要查找内存,内存分配的时间是常数;而且不会出现在程序运行中途出现内存不足的异常。

所以,placement new 非常适合那些对时间要求比较高,长时间运行不希望被打断的应用程序。

难点三:

template <class U>
struct rebind {
    typedef allocator<U> other;
};

rebind 的意义就在于实现两个不同但两者互相有关的类型(比如类型 T 和 Node类型),使用同一种内存分配方法。如果抛开 rebind 不提,想要实现上述的意义,容器必须要让 allocator 是同一个模板,问题就出在容器并不关心你的 allocator 是怎么写的。

它唯一有关的就是在声明时在 template 中写 alloc=allocator<T>,只知道模板参数名 allocator,而不知道其具体实现,导致没有办法让 T 与 U 的 allocator 是同一个。于是在 allocator<T> 中创建一个 U 的 allocator,标准中有这样的规定:

对于 allocator<T> 与一个类型 U,allocator<U> 与 allocator<T>::rebind<U>::other 是等价的。在想使用 allocator<U> 的时候就需要使用 allocator<T>::rebind<U>::other,否则就是用了一个别的 allocator了。

源代码

myalloc.h 头文件

#ifndef MY_ALLOCATOR_H
#define MY_ALLOCATOR_H

#include <new>      // placement new
#include <cstddef>  // ptrdiff_t, size_t
#include <cstdlib>  // exit()
#include <climits>  // UINT_MAX
#include <iostream> // cerr
// 一个简单的空间配置器
namespace myAllocator
{
    // 空间的分配,可以存储 size 个 T 对象
    template <class T>
    inline T* _allocate(ptrdiff_t size, T*) {
        std::set_new_handler(0);  // 分配失败,抛出std::bad_alloc

        // 空间的分配实现,调用 ::operator new() 全局函数
        T* tmp = (T*)(::operator new((size_t)(size * sizeof(T))));

        if (tmp == 0) {
            std::cerr << "out of memory" << std::endl;
            exit(1);
        }

        return tmp;
    }
    // 空间的释放,调用 ::operator delete() 全局函数
    template <class T>
    inline void _deallocate(T* buffer) {
        ::operator delete(buffer);
    }
    // 对象的构造
    template <class T1, class T2>
    inline void _construct(T1* p, const T2& value)
    {
        new(p) T1(value); // placement new
    }   
    // 对象的析构
    template <class T>
    inline void _destroy(T* ptr) {
        ptr->~T();
    }

    // 提供外部使用 allocator
    template <class T>
    class allocator {
    public:
        // 对象的类型
        typedef T value_type;
        typedef T* pointer;
        typedef const T* const_pointer;
        typedef T& reference;
        typedef const T& const_reference;
        typedef size_t size_type;
        typedef ptrdiff_t difference_type; // ptrdiff_t 是两个指针相减结果的有符号整数类型
        // 嵌套 allocator
        template <class U>
        struct rebind {
            typedef allocator<U> other;
        };      
        /***
         * 以下四个函数提供外部使用,空间的分配和释放,对象的构造和析构
         */
        pointer allocate(size_type n, const void* hint = 0) {
            return _allocate((difference_type)n, (pointer)0);
        }
        void deallocate(pointer p, size_type n) {
            _deallocate(p);
        }
        void construct(pointer p, const T& value) {
            _construct(p, value);
        }
        void destroy(pointer p) {
            _destroy(p);
        }
        // 返回某个对象的地址
        pointer address(reference x) {
            return (pointer)&x;
        }
        // 返回某个 const 对象的地址
        const_pointer const_address(const_reference x) {
            return (const_pointer)&x;
        }       
        // 返回可成功分配的最大量  
        size_type max_size() const {  
            return size_type(UINT_MAX/sizeof(T)); // UINT_MAX 是 unsigned long 及 unsigned long long 的最大值
        }
    };
}
#endif

main 函数

#include "myalloc.h"
#include <vector>
#include <iostream>

using namespace std;

int main(int argc, char* argv[]){
    cout << "allocator test" << endl;
    vector<int,myAllocator::allocator<int> >v;
    v.push_back(1);
    v.push_back(2);

    for (int i = 0; i < v.size(); ++i)
    {
        cout << v[i] << ' ';
    }
    cout<<endl;
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值