关闭

stl allocator源码学习

标签: stl源码
4432人阅读 评论(0) 收藏 举报
分类:

概述

介绍几个allocator的源码实现:简单的对operator new和operator delete进行封装的实现,vs2015中的实现,STLport中的实现,仿造STLport实现内存池。

1. 参考

http://www.cplusplus.com/reference/memory/allocator/
《STL源码剖析》
《C++ Primer 第五版》
《Generic Programming and the STL》(《泛型编程和STL》)
MSDN

2. 介绍

std::allocator是STL容器使用的内存配置器,也是标准库唯一预定义的内存配置器。

3. 实现一:最简单的实现

3.1 程序实现

template<class T>
class allocator
{
public:
    // 1、为什么需要下面这些成员,有什么作用呢?
    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;       // size_t是无符号整数
    // ptrdiff_t是有符号整数,代表指针相减结果的类型
    typedef ptrdiff_t  difference_type;

    // 2、这是做什么用的,为何template是U,而不是与allocator的T一致?
    template<class U>
    struct rebind
    {
        typedef allocator<U> other;
    };

    // 默认构造函数,什么都不做
    allocator() noexcept
    {
    }

    // 泛化的构造函数,也什么都不做
    // 3、为什么需要这个泛化的构造函数,不同类型的allocator复制合适吗?
    template<class U>
    allocator(const allocator<U>&) noexcept
    {
    }

    // 析构函数,什么都不做
    ~allocator() noexcept
    {
    }

    // 返回对象地址
    pointer address(reference val) const noexcept
    {
        //non-const版调用const版,参见《Effective C++》条款3
        return const_cast<reference>(address(static_cast<const_reference>(val)));
    }

    // 返回对象地址
    const_pointer address(const_reference val) const noexcept
    {
        return &val;
    }

    // 申请内存,count是指对象个数,不是字节数。
    // 4、hint是做什么的?
    pointer allocate(size_type count, allocator<void>::const_pointer hint = nullptr)
    {
        return static_cast<pointer>(::operator new(count * sizeof(value_type)));
    }

    // 释放内存
    void deallocate(pointer ptr, size_type count)
    {
        ::operator delete(ptr);
    }

    // 可配置的最大量(指对象个数,不是字节数)
    size_type max_size() const noexcept
    {
        return (static_cast<size_type>(-1) / sizeof(value_type));
    }

    // 构造对象,Args是模板参数包,见《C++ Primer》第5版16.4节
    template <class U, class... Args>
    void construct(U* p, Args&&... args)
    {
        ::new ((void *)p) U(::std::forward<Args>(args)...);
    }

    // 析构对象
    template <class U>
    void destroy(U* p)
    {
        p->~U(); // 原来模板还可以这样用
    }
};

// 5、为什么要对void进行特化?
template<>
class allocator<void>
{
public:
    typedef void value_type;
    typedef void *pointer;
    typedef const void *const_pointer;
    template <class U> struct rebind
    {
        typedef allocator<U> other;
    };
};

3.2 问题解答

1、STL的规范,同时这些type在迭代器和traits技术中有用。

2、摘自MSDN:A structure that enables an allocator for objects of one type to allocate storage for objects of another type.
This structure is useful for allocating memory for type that differs from the element type of the container being implemented.
The member template class defines the type other. Its sole purpose is to provide the type name allocator<_Other>, given the type name allocator<Type>.

For example, given an allocator object al of type A, you can allocate an object of type _Other with the expression:
A::rebind<Other>::other(al).allocate(1, (Other *)0)
Or, you can name its pointer type by writing the type:
A::rebind<Other>::other::pointer

具体例子:一个保存int的列表list<int>,列表存储的对象并不是int本身,而是一个数据结构,它保存了int并且还包含指向前后元素的指针。那么,list<int, allocator<int>>如何知道分配这个内部数据结构呢?毕竟allocator<int>只知道分配int类型的空间。这就是rebind要解决的问题。通过allocator<int>::rebind<_Node>()你就可以创建出用于分配_Node类型空间的分配器了。

3、allocator类的模板参数只有一个,代表分配的元素类型,如果allocator封装的仅是内存的分配策略而与元素类型无关,定义泛型复制构造好像没什么不合理,同时如果不定义成泛型rebind将无法使用。construct成员函数和destroy成员函数也是泛型,allocator的使用条件还是特别宽松的。

4、hint
Either 0 or a value previously obtained by another call to allocate and not yet freed with deallocate.
When it is not 0, this value may be used as a hint to improve performance by allocating the new block near the one specified. The address of an adjacent element is often a good choice.

5、只有void *变量,没有void变量,没有void&变量,不能typedef void value_type等等。

4. 实现二:vs2015中的实现

4.1 程序实现(部分和实现一类似的内容省略)

template<class _Ty>
	class allocator
	{	// generic allocator for objects of class _Ty
public:
	static_assert(!is_const<_Ty>::value,
		"The C++ Standard forbids containers of const elements "
		"because allocator<const T> is ill-formed.");

……

	template<class _Other>
		allocator<_Ty>& operator=(const allocator<_Other>&)
		{	// assign from a related allocator (do nothing)
		return (*this);
		}

	void deallocate(pointer _Ptr, size_type _Count)
		{	// deallocate object at _Ptr
		_Deallocate(_Ptr, _Count, sizeof (_Ty));
		}

	__declspec pointer allocate(size_type _Count)
		{	// allocate array of _Count elements
		return (static_cast<pointer>(_Allocate(_Count, sizeof (_Ty))));
		}

	__declspec pointer allocate(size_type _Count, const void *)
		{	// allocate array of _Count elements, ignore hint
		return (allocate(_Count));
		}

……
	};

4.2 问题解释

1、static_assert :Tests a software assertion at compile time. If the specified constant expression is false, the compiler displays the specified message and the compilation fails with error C2338; otherwise, the declaration has no effect.

2、__declspec :Microsoft Specific. Tells the compiler not to insert buffer overrun security checks for a function.

3、这里的实现也比较简单,只是对内存分配函数(_Allocate)和释放函数(_Deallocate)进行简单的封装。

4.3 _Allocate和_Deallocate的实现

直接复制过来的实现如下

inline
	_DECLSPEC_ALLOCATOR void *_Allocate(size_t _Count, size_t _Sz,
		bool _Try_aligned_allocation = true)
	{	// allocate storage for _Count elements of size _Sz
	void *_Ptr = 0;

	if (_Count == 0)
		return (_Ptr);

	// check overflow of multiply
	if ((size_t)(-1) / _Sz < _Count)
		_Xbad_alloc();	// report no memory
	const size_t _User_size = _Count * _Sz;

 #if defined(_M_IX86) || defined(_M_X64)
	if (_Try_aligned_allocation
		&& _BIG_ALLOCATION_THRESHOLD <= _User_size)
		{	// allocate large block
		static_assert(sizeof (void *) < _BIG_ALLOCATION_ALIGNMENT,
			"Big allocations should at least match vector register size");
		const size_t _Block_size = _NON_USER_SIZE + _User_size;
		if (_Block_size <= _User_size)
			_Xbad_alloc();	// report no memory
		const uintptr_t _Ptr_container =
			reinterpret_cast<uintptr_t>(::operator new(_Block_size));
		_SCL_SECURE_ALWAYS_VALIDATE(_Ptr_container != 0);
		_Ptr = reinterpret_cast<void *>((_Ptr_container + _NON_USER_SIZE)
			& ~(_BIG_ALLOCATION_ALIGNMENT - 1));
		static_cast<uintptr_t *>(_Ptr)[-1] = _Ptr_container;

 #ifdef _DEBUG
		static_cast<uintptr_t *>(_Ptr)[-2] = _BIG_ALLOCATION_SENTINEL;
 #endif /* _DEBUG */
		}
	else
 #endif /* defined(_M_IX86) || defined(_M_X64) */

		{	// allocate normal block
		_Ptr = ::operator new(_User_size);
		_SCL_SECURE_ALWAYS_VALIDATE(_Ptr != 0);
		}
	return (_Ptr);
	}

		// FUNCTION _Deallocate
inline
	void _Deallocate(void * _Ptr, size_t _Count, size_t _Sz)
	{	// deallocate storage for _Count elements of size _Sz
 #if defined(_M_IX86) || defined(_M_X64)
	_SCL_SECURE_ALWAYS_VALIDATE(_Count <= (size_t)(-1) / _Sz);
	const size_t _User_size = _Count * _Sz;
	if (_BIG_ALLOCATION_THRESHOLD <= _User_size)
		{	// deallocate large block
		const uintptr_t _Ptr_user = reinterpret_cast<uintptr_t>(_Ptr);
		_SCL_SECURE_ALWAYS_VALIDATE(
			(_Ptr_user & (_BIG_ALLOCATION_ALIGNMENT - 1)) == 0);
		const uintptr_t _Ptr_ptr = _Ptr_user - sizeof(void *);
		const uintptr_t _Ptr_container =
			*reinterpret_cast<uintptr_t *>(_Ptr_ptr);

 #ifdef _DEBUG
		// If the following asserts, it likely means that we are performing
		// an aligned delete on memory coming from an unaligned allocation.
		_SCL_SECURE_ALWAYS_VALIDATE(
			reinterpret_cast<uintptr_t *>(_Ptr_ptr)[-1] ==
				_BIG_ALLOCATION_SENTINEL);
 #endif /* _DEBUG */

		// Extra paranoia on aligned allocation/deallocation
		_SCL_SECURE_ALWAYS_VALIDATE(_Ptr_container < _Ptr_user);

 #ifdef _DEBUG
		_SCL_SECURE_ALWAYS_VALIDATE(2 * sizeof(void *)
			<= _Ptr_user - _Ptr_container);

 #else /* _DEBUG */
		_SCL_SECURE_ALWAYS_VALIDATE(sizeof(void *)
			<= _Ptr_user - _Ptr_container);
 #endif /* _DEBUG */

		_SCL_SECURE_ALWAYS_VALIDATE(_Ptr_user - _Ptr_container
			<= _NON_USER_SIZE);

		_Ptr = reinterpret_cast<void *>(_Ptr_container);
		}
 #endif /* defined(_M_IX86) || defined(_M_X64) */

	::operator delete(_Ptr);
	}
上面的代码有很多typedef和宏,还有一些判断和assert,看起来比较复杂,下面是精简之后的实现(只保留比较关键的部分)

void *_Allocate(size_t _Count, size_t _Sz, bool _Try_aligned_allocation = true)
{	// allocate storage for _Count elements of size _Sz
    void *_Ptr = 0;

    // 计算需要内存的字节数
    const size_t _User_size = _Count * _Sz;

// _BIG_ALLOCATION_THRESHOLD 为4096 大于这个大小的内存块需要对齐。
//1、为什么以4096为界?
    if (_Try_aligned_allocation && _BIG_ALLOCATION_THRESHOLD <= _User_size)
    {	// 分配大内存块
        // _BIG_ALLOCATION_ALIGNMENT 大内存对齐 32
        // _NON_USER_SIZE 为 (2 * sizeof(void *) + _BIG_ALLOCATION_ALIGNMENT - 1) 即两个指针大小再加31
        const size_t _Block_size = _NON_USER_SIZE + _User_size;
        // 这里将地址转换成整型是为了进行位运算,uintptr_t可能是unsigned int(32位),或者是unsigned long long(64位)
        const uintptr_t _Ptr_container =
            reinterpret_cast<uintptr_t>(::operator new(_Block_size));
        // 获取对齐地址,低5位清零。
        // 2、为什么是按32个字节对齐?
        _Ptr = reinterpret_cast<void *>((_Ptr_container + _NON_USER_SIZE)
            & ~(_BIG_ALLOCATION_ALIGNMENT - 1));
        // 用_NON_USER_SIZE中的位置存放真正的内存块起始地址
        static_cast<uintptr_t *>(_Ptr)[-1] = _Ptr_container;
    }
    else
    {	// 分配一般内存块
        _Ptr = ::operator new(_User_size);
    }
    return (_Ptr);
}

void _Deallocate(void * _Ptr, size_t _Count, size_t _Sz)
{	// deallocate storage for _Count elements of size _Sz
    const size_t _User_size = _Count * _Sz;
    if (_BIG_ALLOCATION_THRESHOLD <= _User_size)
    {	// 释放大内存块
        // 将地址转换为整数类型,以便做减法运算
        const uintptr_t _Ptr_user = reinterpret_cast<uintptr_t>(_Ptr);
        const uintptr_t _Ptr_ptr = _Ptr_user - sizeof(void *);
        // 获取_NON_USER_SIZE中存放的真正的内存块起始地址
        const uintptr_t _Ptr_container =
            *reinterpret_cast<uintptr_t *>(_Ptr_ptr);
        _Ptr = reinterpret_cast<void *>(_Ptr_container);
    }

    // 真正释放内存
    ::operator delete(_Ptr);
}

问题解释

1、

2、

5. 实现三:STLport中的实现

实现一太简单,只是对operator new和operator delete做了简单的封装;实现二也比较简单,微软似乎把内存分配策略实现在底层。如果需要了解比较细腻内存分配策略(内存池),参考STLport中的实现。

STLport中的实现分析参见:《STL源码剖析》—侯捷 2.2.6-2.2.10

6. 实现四:带内存池的实现

6.1 内存池

按照《STL源码剖析》—侯捷 2.2.6-2.2.10的思路,下面自己实现一个内存池,不考虑线程安全。

内存池分配内存,当所需的内存大小大于128时直接调用operator new分配内存,否则从空闲链表中分配,这样就避免太多小内存块造成的内存碎片和管理内存的额外负担造成内存利用率不高的问题。

首先是管理内存池的数据结构,内存池维护16个空闲链表,为了方便管理(分配和回收),各自管理的内存块大小都是8的倍数,分别为8,16,24,…,128。分配内存时,如果大小不是8的倍数,则将需求上调至8的倍数,然后从相应的空闲链表中分配。为了维护空闲链表中的链表结构,空闲块的前sizeof(void *)个字节存储下一个空闲块节点的地址,当空闲块被分配后,空闲块也将从空闲链表中摘除,所以这种做法不影响用户使用,同时不需要额外内存作为节点的next指针。

class MemoryPool
{
private:
    // 小型内存块的大小都是ms_align的倍数
    constexpr static size_t ms_align = 8;
    // 小型内存块大小的上限
    constexpr static size_t ms_maxBytes = 128;
    // 空闲内存块链表的个数
    constexpr static size_t ms_nFreeLists = ms_maxBytes / ms_align;
private:
    // 内存块空闲链表,m_freeLists[0]是大小为8的内存块链表,m_freeLists[1]是大小为16的...
    void *m_freeLists[ms_nFreeLists];
private:
    // 内存池分配块
    void *m_pFreeStart;
    void *m_pFreeEnd;
};
完整的内存池定义和实现:

class MemoryPool
{
public:
    MemoryPool() :m_freeLists{ nullptr }, m_pFreeStart(nullptr), m_pFreeEnd(nullptr), m_heapSize(0) {}
    // 分配内存
    void *allocate(size_t nBytes);
    // 回收内存
    void deallocate(void *ptr, size_t nBytes);
private:
    // 小型内存块的大小都是ms_align的倍数
    constexpr static size_t ms_align = 8;
    // 小型内存块大小的上限
    constexpr static size_t ms_maxBytes = 128;
    // 空闲内存块链表的个数
    constexpr static size_t ms_nFreeLists = ms_maxBytes / ms_align;
private:
    // 内存块空闲链表,m_freeLists[0]是大小为8的内存块链表,m_freeLists[1]是大小为16的...
    void *m_freeLists[ms_nFreeLists];
private:
    // 内存池
    char *m_pFreeStart;
    char *m_pFreeEnd;
    // 本以为m_heapSize没有用,原来为内存池分配内存时可以作为计算增量的因子
    size_t m_heapSize;
private:
    // 将bytes上调至ms_align的倍数
    size_t roundUp(size_t nBytes)
    {
        return (nBytes + ms_align - 1)&~(ms_align - 1);
    }

    // 根据内存块的大小求得应使用第几个freeList,从0开始
    size_t freeListIndex(size_t nBytes)
    {
        return (nBytes + ms_align - 1) / ms_align - 1;
    }
private:
    // 重新为nBytes所属的空闲链表分配空闲节点,nBytes是ms_align的倍数
    void *refill(size_t nBytes);
    // 从内存池获取内存,nBytes是ms_align的倍数
    void *chunkAlloc(size_t nBytes, int &nObjs);
};

void * MemoryPool::allocate(size_t nBytes)
{
    if (nBytes > ms_maxBytes)
    {   // 当内存块较大,直接从系统分配
        return ::operator new(nBytes);
    }

    // 获取nBytes大小所属的空闲链表
    void **pFreeList = m_freeLists + freeListIndex(nBytes);
    void *result = *pFreeList;

    if (result == nullptr)
    {   // 空闲链表中没有空闲块,需要重新分配
        return refill(roundUp(nBytes));
    }

    // 有空闲块,将该空闲块从空闲链表中摘除
    *pFreeList = *reinterpret_cast<void **>(result);

    return result;
}

// 从这个函数可以看出回收内存时并不会将内存交还系统,空闲链表的内存只增不减
// 考虑到分配的内存都是碎片级别的,非极端情况下闲占的内存不会太多,所以不考虑将内存交还系统
void MemoryPool::deallocate(void *ptr, size_t nBytes)
{
    if (nBytes > ms_maxBytes)
    {   // 和allocate对应,直接给系统回收
        ::operator delete(ptr);
    }

    // 获取nBytes大小所属的空闲链表
    void **pFreeList = m_freeLists + freeListIndex(nBytes);

    // 将内存块重新加入空闲链表
    *reinterpret_cast<void **>(ptr) = *pFreeList;
    *pFreeList = ptr;
}

void * MemoryPool::refill(size_t nBytes)
{
    int nObjs = 20;
    // 尝试从内存池获取nObjs个内存块,可能结果小于nObjs个
    void *pChunk = chunkAlloc(nBytes, nObjs);

    // 不止一块,除了第一块之外都加入空闲链表
    if (nObjs > 1)
    {
        // 获取nBytes大小所属的空闲链表
        void **pFreeList = m_freeLists + freeListIndex(nBytes);

        void *pNext = reinterpret_cast<char *>(pChunk) + nBytes;
        // 将第二块连接到表头
        *pFreeList = pNext;
        for (int i = 2;i < nObjs;++i)
        {
            void **pCurrent = reinterpret_cast<void **>(pNext);
            pNext = reinterpret_cast<char *>(pChunk) + nBytes;
            // 将pNext连接到空闲链表
            *pCurrent = pNext;
        }
        // 空闲链表最后一个next指针为空
        *reinterpret_cast<void **>(pNext) = nullptr;
    }
    // 返回第一块给用户
    return pChunk;
}

void * MemoryPool::chunkAlloc(size_t nBytes, int &nObjs)
{
    size_t nNeedBytes = nBytes * nObjs;
    size_t nFreeBytes = m_pFreeEnd - m_pFreeStart;
    void *result;
    if (nNeedBytes <= nFreeBytes)
    {   // 内存池中还有足够的内存
        result = m_pFreeStart;
        m_pFreeStart += nNeedBytes;
    }
    else if (nBytes <= nFreeBytes)
    {   // 内存池内存还够一个或以上的内存块
        nObjs = nFreeBytes / nBytes;
        result = m_pFreeStart;
        m_pFreeStart += nBytes * nObjs;
    }
    else
    {   // 内存池内存连一个内存块也不够了
        if (nFreeBytes > 0)
        {   // 内存池还有零头,将零头加入合适的空闲链表
            // 从这里可以看出将内存块的上调至ms_align的倍数这个设计真是太精巧了
            void **pFreeList = m_freeLists + freeListIndex(nFreeBytes);
            *reinterpret_cast<void **>(m_pFreeStart) = *pFreeList;
            *pFreeList = *pFreeList;
            // 当下面operator new内存分配失败时,m_pFreeStart状态保证合法
            m_pFreeStart += nFreeBytes;
        }

        // 向系统申请的内存大小,2倍所需再加上附加增量
        // STLport中是这样计算的,可能这样会比较高效
        size_t nBytesToGet = 2 * nNeedBytes + roundUp(m_heapSize >> 4);
        m_pFreeStart = reinterpret_cast<char *>(operator new(nBytesToGet));
        m_heapSize += nBytesToGet;
        m_pFreeEnd = m_pFreeStart + nBytesToGet;
        // 递归调用,非常可以的做法
        return chunkAlloc(nBytes, nObjs);
    }
    return result;
}

6.2 带内存池的allocator实现

这里和实现一相同的部分省略,仅给出分配和释放内存的几个操作:

template<class T>
class allocator
{
private:
    static MemoryPool pool;
public:
…… // 同实现一
    pointer allocate(size_type count, allocator<void>::const_pointer hint = nullptr)
    {
        return static_cast<pointer>(pool.allocate(count * sizeof(value_type)));
    }

    // 释放内存
    void deallocate(pointer ptr, size_type count)
    {
        pool.deallocate(ptr, count);
    }
…… // 同实现一
};




1
0
查看评论

C++ Grammar

变量 标准库类型 string 构造函数 string读写 string 操作变量 1> 左值: 可以出现在赋值语句的左边或者右边。 2> 右值: 只能出现在赋值语句的右边。标准库类型 意义: 语言组成部分中更基本的哪些数据类型的抽象 string构造函数:string s1; ...
  • lxhuster_csdn
  • lxhuster_csdn
  • 2017-01-15 22:34
  • 227

The C++ Standard Libary 学习笔记

<br /><br />Why do I spend my spare time to read this book?<br />I want to maximize the usage of STL in the real world projects an...
  • Taylor05
  • Taylor05
  • 2011-03-08 20:45
  • 1145

stl allocator源码学习

概述 介绍几个allocator的源码实现:简单的对operator new和operator delete进行封装的实现,vs2015中的实现,STLport中的实现,仿造STLport实现内存池。 1. 参考 http://www.cplusplus.com/reference/memory/a...
  • xiyanggudao
  • xiyanggudao
  • 2016-05-31 10:39
  • 4432

[c++学习]关于const使用的疑惑

[c++学习]关于const使用的疑惑  如代码行所标注的地方的const和&的用法和其含义不是很明白,希望已经掌握此用法的同学帮我解答一下:-P,对了书中还提到了保留setgolf()的交互版本,但要用构造函数来实现它(例如,setgolf()的代码应该获得数据,将数据传递给构...
  • Mr_Yaoxiansen
  • Mr_Yaoxiansen
  • 2018-02-06 04:00
  • 21

vector源码剖析

vector前导准备源码位置 * C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\include源码版本 Copyright (c) 1992-2009 by P.J. Plauger. ALL RIGHTS RESERVED. ...
  • feiniao251314
  • feiniao251314
  • 2016-07-21 15:01
  • 1471

STL源码剖析读书笔记2

空间适配器allocator 为什么不说allocator是内存适配器而是空间适配器,原因就是你可以写 allocat直接向硬盘取空间。 allocator标准接口 设计一个简单的空间配置器JJ::allocator_STD_BEGIN // 这里是内存分配 ...
  • qq_33823833
  • qq_33823833
  • 2017-06-09 15:34
  • 83

C/C++内存管理

void * realloc(void * p,size_t new_size) 如果p为NULL,则当malloc使用。 如果newsize为0,则释放p所指向的空间。返回NULL 如果内存不足,则原先p指向的空间不变,返回NULL 如果系统第一次分配的空间其后的空间大小可以满足newsi...
  • sdoyuxuan
  • sdoyuxuan
  • 2017-07-22 23:03
  • 193

【OpenCV】"(_Ptr_user&(_BIG_ALLOCATION_ALIGNMENT - 1)) ==0" && 0

在使用VS2015+OpenCV2410时,会报如下错误,而用VS2013时不会,网上说是兼容性问题。 void extractfeatures::extractKeypoints(cv::Mat _frame, cv::Mat _object) { //Extract keypoin...
  • henryheheng
  • henryheheng
  • 2017-08-17 14:54
  • 391

深入理解 STL allocator

由于std::set,std::multiset,std::map,std::multimap四种容器的插入删除操作性能高并且自动排序,在很多时候比如需要动态操作时往往会使用它们,然后由于容器内部使用的是节点,每次的插入或删除都要调用new或delete,往往容易造成碎片和性能下降,于是自定义的al...
  • lifesider
  • lifesider
  • 2011-06-06 12:00
  • 9909

标准库 STL :Allocator能做什么?

Allocator是C++语言标准库中最神秘的部分之一。它们很少被显式使用,标准也没有明确出它们应该在什么时候被使用。今天的allocator与最初的STL建议非常不同,在此过程中还存在着另外两个设计--这两个都依赖于语言的一些特性,而直到最近才在很少的几个编译器上可用。对 allocator的功能...
  • wallwind
  • wallwind
  • 2014-03-26 14:46
  • 2670
    个人资料
    • 访问:7496次
    • 积分:134
    • 等级:
    • 排名:千里之外
    • 原创:6篇
    • 转载:0篇
    • 译文:0篇
    • 评论:0条
    文章分类