allocator和容器实例构建过程

本文探讨了STL容器如在添加新对象时,如何通过allocator进行内存管理和对象创建。通过分析代码,揭示了allocator在内存分配、对象构造、复制构造以及析构过程中的作用。在测试过程中,观察到allocator的allocate和deallocate操作,以及对象构造与析构的顺序,展示了容器内部的工作机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

看到过几次,但一直对allocator的功能不甚了解。最近在看《Effective STL》的时候,看到里面提到了allocator以及示例代码,决定尝试去理解一下。

allocator本质是:在每个新的对象实例被加入到容器中时,容器所作的准备工作。如果你以为新添加对象实例时,容器只需要在insert函数中确认一下空间并new一个对象,那就大错特错了。
list为例,容器在新添加对象的时候,除了为对象保留足够的空间外,还需要创建指向上个结点与下个结点的指针。这种在每次添加新对象实例时都具备的重复工作,就由allocator完成。

我用User-Defined Allocator中的代码进行了测试。PS:这段代码太棒了,不足百行,简单清晰地演示了一般allocator需要完成的工作。

//自定义的allocator
namespace MyLib {
    template <class T>
    class MyAlloc {
    public:
        // type definitions
        typedef T        value_type;
        typedef T*       pointer;
        typedef const T* const_pointer;
        typedef T&       reference;
        typedef const T& const_reference;
        typedef std::size_t    size_type;
        typedef std::ptrdiff_t difference_type;

        // rebind allocator to type U
        template <class U>
        struct rebind {
            typedef MyAlloc<U> other;
        };

        // return address of values
        pointer address(reference value) const {
            return &value;
        }
        const_pointer address(const_reference value) const {
            return &value;
        }

        /* constructors and destructor
        * - nothing to do because the allocator has no state
        */
        MyAlloc() throw() {
        }
        MyAlloc(const MyAlloc&) throw() {
        }
        template <class U>
        MyAlloc(const MyAlloc<U>&) throw() {
        }
        ~MyAlloc() throw() {
        }

        // return maximum number of elements that can be allocated
        size_type max_size() const throw() {
            return std::numeric_limits<std::size_t>::max() / sizeof(T);
        }

        // allocate but don't initialize num elements of type T
        pointer allocate(size_type num, const void* = 0) {
            // print message and allocate memory with global new
            std::cerr << "allocate " << num << " element(s)"
                << " of size " << sizeof(T) << std::endl;
            pointer ret = (pointer)(::operator new(num*sizeof(T)));
            std::cerr << " allocated at: " << (void*)ret << std::endl;
            return ret;
        }

        // initialize elements of allocated storage p with value value
        void construct(pointer p, const T& value) {
            // initialize memory with placement new
            std::cout << "allocator construction" << std::endl;
            new((void*)p)T(value);
        }

        // destroy elements of initialized storage p
        void destroy(pointer p) {
            // destroy objects by calling their destructor
            std::cout << "allocator destruction" << std::endl;
            p->~T();
        }

        // deallocate storage p of deleted elements
        void deallocate(pointer p, size_type num) {
            // print message and deallocate memory with global delete
            std::cerr << "deallocate " << num << " element(s)"
                << " of size " << sizeof(T)
                << " at: " << (void*)p << std::endl;
            ::operator delete((void*)p);
        }
    };

    // return that all specializations of this allocator are interchangeable
    template <class T1, class T2>
    bool operator== (const MyAlloc<T1>&,
        const MyAlloc<T2>&) throw() {
        return true;
    }
    template <class T1, class T2>
    bool operator!= (const MyAlloc<T1>&,
        const MyAlloc<T2>&) throw() {
        return false;
    }
}

我在原代码的destroy()construct()函数中各加了一行输出,以便我们能在测试中观察到他们何时被调用。

同时用以下代码进行测试


class object {
    int i;
public:
    object(int i) :i(i) {
        std::cout << "object construction" << std::endl;
    };
    object(const object& t):i(t.i) {
        std::cout << "copy construction" << std::endl;
    }
    ~object() {
        std::cout << "object destruction" << std::endl;
    }
};

int main()
{
    // create a vector, using MyAlloc<> as allocator
    std::vector<object, MyLib::MyAlloc<int> > v;
    std::cout << "begin" << std::endl;
    //使用push_back()的测试结果大同小异,大家可以自己尝试
    v.emplace_back(42);
    std::cout << "exit" << std::endl;
}

结果如下:

allocate 1 element(s) of size 8
allocated at: 007421C0
allocator construction
begin………………………………………………………………….
allocate 1 element(s) of size 4
allocated at: 00741C60
object construction
allocator construction
copy construction
object destruction
exit………………………………………………………………………
allocator destruction
object destruction
deallocate 1 element(s) of size 4 at: 00741C60
allocator destruction
deallocate 1 element(s) of size 8 at: 007421C0

begin和exit后的点是我为了使结果呈现地更清晰而添加的,并非源代码输出,但这无关紧要。


结果分析

从源代码中可以得知,object对象的size只有4个字节(一个int),但在begin之前的1-3行,程序已经调用allocator分配了一个8字节的空间。说明这不是用来存放对象实例的。推测是vector用来存放容器成员变量(很可能是size)的地方。在第3行调用MyAlloc::construct()后,没有调用object的构造函数也说明了该处并没有存放任何对象实例。

begin后的第5行开始就是向容器添加对象实例的过程了。
第5,6两行都是调用MyAlloc::allocate()的结果,创造了一个4字节的空间,并返回一个指针指向该空间。
第7行,程序调用对应构造函数创造了object对象。
第8行,程序调用了MyAlloc::construct()
第9行,程序调用了object的复制构造函数,并将对象放在之前申请到的内存上。这与我们以往的认知:emplace_back()函数直接在目标内存上创建对象 不一致。它依旧是先构造了一个对象,在把它复制到容器内。为什么跟以往说明不一致,我也不知道
第10行是一个析构函数,可想而知这显然是第7行构造的对象,对容器来说,已经得到副本了,就把“原配”丢弃了。
至此,容器完成了一次添加对象的过程。
如果逐步添加对象时,会遇到需要扩展vector长度的时候,这时可能会发现,虽然可能只是添加1个对象,但输出的却是 添加了XXX个对象,随后又 删除了XXX-1个对象。如果预先reserve()足够的空间,就不会出现这个情况。大家可以自行测试。

在第11行的exit后,就是容器的析构过程。
第12行先调用MyAlloc::destroy()
第13行调用object::~object()析构了容器内的对象。
第14行调用MyAlloc::deallocate()释放了之前申请的容纳该元素的空间。对于vector来说,该空间的大小与对象大小一致。但如前文提到的,对于list或set等结构来说,这两者的空间就不一样了
第15,16行,最后的最后,容器释放了最开始(begin之前)申请的那部分空间。至此容器完全析构。


尾声:这个测试只能对allocator的大致功能有个初步的了解,具体的,rebind::other是如何发挥作用的,我还没有理解。等明白了再来补充。
顺便,不知道为什么,上述测试代码,如果采用list替换vector容器,那么`emplace_back()函数就能正常发挥作用(而不调用复制构造函数)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值