Chapter 2:空间配置器 allocator

一:空间配置器的标准接口

STL 规范规定了空间配置器 allocator 一定要有的一些接口,其中最重要的四个接口如下:

//配置空间,足以存储 n 个 T 对象;
T* allocator::allocate(size_t n, const void* = 0)

//归还先前配置的空间;
void allocator::deallocate(T* p, size_t n)

//在配置的指定空间 p 上构造对象 T
void allocator::construct(T* p, const T& x)

//将存储在内存空间 p 上的对象 T 析构
void allocator::destroy(T* p)

因此当在内存中构造一个对象时,内存的分配 allocate 与对象的构造 costruct 是分开进行的;同样撤销存储对象的内存时,对象的析构 destroy 和 内存的撤销 deallocate 也是分开进行的。比如我们会经常用操作符 new 分配内存,然后用操作符 delete 释放内存,例子代码如下:

class Foo{ ... };
Foo* pf = new Foo;
delete pf;

其中 new 操作符内就分为配置内存和构造对象两阶段操作:(1):调用 ::operator new(类似于 allocate) 配置内存;(2):调用 Foo::Foo() 构造对象内存(相当于 construct);同样 delete 操作符也分为对象析构和内存撤销两阶段:(1):调用Foo::~Foo()将对象析构;(2):调用::operator delete释放内存。

二:SGI 中的空间配置器

1:SGI 中配置了一个符合 STL 规范的标准空间配置器 std::allocator,但由于该配置器主要是将 c++ 中的::operator new::operator delete做一层薄薄的包装而已,因此使用效率不佳,SGI 从未使用过它;SGI 拥有一个特殊的空间配置器 std::alloc,SGI 中的容器就是基于该空间配置器配置空间的;

2:在 SGI <memory> 头文件中,包括了三个头文件<stl_construct.h>,<stl_alloc.h> 和 <stl_uninitialized.h>

  1. <stl_construct.h> 定义了全局函数 construct()destroy(),负责对象的构造与析构,符合 STL 规范;

  2. <stl_alloc.h> 定义了配置器 alloc,负责内存的分配与撤销;

  3. <stl_uninitialized.h> 中定义了三个全局函数,用来填充(fill)或复制(copy)大块内存数据。

注明:STL 规定空间配置器必须拥有名为construct()destroy()这两个成员函数,但真正在SGI STL 中大显身手的 std::alloc配置器并未遵守这一规则,其只负责内存的分配与撤销,对象的构造与析构由全局函数construct()destroy()负责

三:构造与析构工具: construct()destroy()

1:construct()函数接受一个指针 p 和一个初值 value,此函数用途就是将初值设定到指针所指的空间上,c++ 中的 placement new 操作符可完成这一任务。construct()函数代码如下:

#include <new.h>  // for placement new operator
using namespace std;

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

2:destroy()函数有两个版本:

(1):第一版本接受一个指针,将该指针所指之物析构掉,代码如下:

template<class T>
inline void destroy(T* pointer)
{  pointer->~T(); }

(2):第二版本接受 first 和 last 两个迭代器,准备将 [first, last) 范围内的所有对象析构掉。我们先调用value_type()函数,获得迭代器所指对象的型别,再利用 __type_traits<T>判断该型别的析构函数是否无关痛痒,若是(__true_type)的话,则什么也不做结束,若不是(__false_type),则以循环方式访问整个范围,并在循环中每经历一个对象就调用第一版本的destroy()函数。代码如下:

//元素的数值型别(value type)有 non-trivial destructor
template<class ForwardIterator>
inline void __destroy_aux(ForwardIterator first, ForwardIterator last, __false_type)
{
        for( ; first != last; ++first)
                destroy(&*first);
}

//元素的数值型别(value type)有 trivial destructor
template<class ForwardIterator>
inline void __destroy_aux(ForwardIterator first, ForwardIterator last, __true_type)
{
}

//判断元素的数值型别(value type)是否有 trivial destructor
template<class ForwardIterator, class T>
inline void __destroy(ForwardIterator first, ForwardIterator last, T*)
{
        typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;       
         __destroy_aux(first, last, trivial_destructor());
}

template<class Iterator>
inline typename iterator_traits<iterator>::value_type* value_type(const iterator&)
{
        return static_cast<typename iterator_traits<iterator>::value_type*>(nullptr);
}

//这是destroy()函数第二版本,此函数设法找出元素的数值型别,
//进而利用 __type_traits<T> 求取最适当措施
template<class ForwardIterator>
inline void destroy(ForwardIterator first, ForwardIterator last)
{  __destroy(first, last, value_type(first)); }

//以下是 destroy() 第二版本针对 char* 和 wchar_t* 的特化版;
inline void destroy(char*, char*){}
inline void destroy(wchar_t*,wchar_t*){}

关于value_type()函数以及__type_traits<T>的相关内容见第三章。

四:空间的配置与释放, std::alloc

1:C++ 的内存配置基本操作是 ::operator new(),内存释放基本操作是::operator delete();这两个全局函数相当于 C 语言的 malloc()free()函数,SGI 正是以 C 语言的这两个函数完成内存的配置与释放;无论是 C++ 还是 C 的内存配置操作,配置的内存都是以字节(byte)为单位;

2:考虑到小型内存区块造成的内存破碎问题,SGI 设计了双层级配置器,第一级配置器直接使用malloc()free(),第二级配置器则视情况采用不同的策略:当配置区块超过 128 bytes 时,视之为“足够大”,便调用第一级配置器;当配置区块小于 128 bytes 时,视之为“过小”,此时便采用复杂的 memory pool 整理方式,而不再求助于第一级配置器。alloc 既可以为第一级配置器,也可以为第二级配置器,不过 SGI STL 已经把 alloc 设为第二级配置器,并且 alloc 并不接受任何 template 型别参数;

3:SGI 为 alloc 包装了一个如下的接口,使得 SGI 中配置器的接口能够符合 STL 规格:

template<class T, class Alloc>
class simple_alloc{
public:
        static T* allocate(size_t n) { return n==0? 0: static_cast<T*>(Alloc::allocate(n*sizeof(T))); }
        static T* allocate(void) { return static_cast<T*>(Alloc::allocate(sizeof(T))); }
        static void deallocate(T* p, size_t n) { if(n != 0) Alloc::deallocate(p, n*sizeof(T)); }
        static void deallocate(T* p) { Alloc::deallocate(p, sizeof(T)); }
};

这个接口 simple_alloc 使得配置器的配置单位从 bytes 转为个别元素大小(sizeof(T)),SGI STL 容器全都是以这个 simple_alloc为接口,例如:

template<class T, class Alloc = alloc> //默认使用 alloc 为配置器
class vector{
public:
        typedef T value_type;
        ...
protected:
        typedef simple_alloc<value_type, Alloc> data_allocator;
        ...
};

4:第一级配置器 __malloc_alloc_template

1):SGI STL 中的第一级配置器 __malloc_alloc_templateallocate()deallocate()函数分别直接使用 malloc()free()函数;

2):模拟 C++ 中的 C++ new-handler 机制set_new_handler()以处理内存不足的状况。所谓 C++ new handler 机制是,我们可以要求系统在内存配置需求无法满足时,调用一个我们指定的函数取释放内存,但该函数的设计是我们自己的责任。

第一级配置器的代码如下:

//注意:无 template 型别参数,至于非型别参数 inst,则完全无用
template<int inst>
class __malloc_alloc_template{
public:
        static void* allocate(size_t n)
        {
                void* result = malloc(n);
                //无法满足需求时,改用oom_malloc();
                if( result == nullptr) result = oom_malloc(n);

                return result;
        }

        static void deallocate(void* p, size_t /* n */) { free(p); }

        static void* reallocate(void* p, size_t /* old_size */, size_t new_sz)
        {
                void* result = realloc(p, new_sz);
                //无法满足需求时,改用oom_realloc();
                if(result == nullptr) result = oom_realloc(p, new_sz);

                return result;
        }

        //仿真 C++ 的 set_new_handler,
        //可以通过它指定自己的 out-of-memory handler.
        typedef void (*malloc_handler)();
        static malloc_handler set_malloc_handler(malloc_handler f)
        {
                malloc_handler old = __malloc_allc_oom_handler;
                __malloc_alloc_oom_handler = f;

                return old;
        }

private:
        //以下函数用来处理内存不足的情况
        //oom: out of memory.
        static void* oom_malloc(size_t);
        static void* oom_realloc(void*, size_t);
        //将__malloc_alloc_oom_handler声明为指向函数的指针,
        //它指向的函数,其形参为空,返回值也为空
        static void (*__malloc_alloc_oom_handler)();
 };

template <int inst>
void (*__malloc_alloc_template<inst>::__malloc_alloc_oom_handler)() = 0;

template <int inst>
void* __malloc_alloc_template<inst>::oom_malloc(size_t n)
{
        void (*my_malloc_handler)();
        void* result;

        for(;;){             //不断尝试释放,配置,再释放,再配置...
                my_malloc_handler = __malloc_alloc_oom_handler;
                if (my_malloc_handler == nullptr) { throw bad_alloc; }
                my_malloc_handler();   //调用处理例程,企图释放内存
                result = malloc(n);
                if (result != nullptr) return result;
        }
}

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

        for(;;){             //不断尝试释放,配置,再释放,再配置...
                my_malloc_handler = __malloc_alloc_oom_handler;
                if (my_malloc_handler == nullptr) { throw bad_alloc; }
                my_malloc_handler();   //调用处理例程,企图释放内存
                result = realloc(p,n);
                if (result != nullptr) return result;
        }
}

//以下直接将参数 inst 指定为0
typedef __malloc_alloc_template<0> malloc_alloc;        

5:第二级配置器:__default_alloc_template

1):第二级配置器主要负责内存区块小于 128 bytes 的分配。SGI 专门设计第二级配置器,而不是直接采取通过malloc()函数分配内存的第一级配置器来分配小额内存区块的原因在于以下两点:

  1. 如果直接采用malloc()函数分配小额内存区块,会造成内存碎片杂乱分布的问题,然而第二级配置器能够系统性地管理这些小额区块,其不仅负责小额区块的分配也负责其回收;

  2. 通过malloc()分配的内存要比实际分配的内存要大,因为要通过多出来的内存管理分配的内存,这称之为 overhaed(额外负担),区块越小,额外负担所占的比例就愈大,就俞显得浪费。第二级配置器能够避免在小额区块中非常突出的 overhead 问题。

2):第二级配置器管理小额内存的分配思路:

  1. 每次配置一大块内存,称之为内存池(memory pool)。并且维护 16 个 free lists,各自管理大小分别为 8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128 bytes 的小额内存区块;

  2. 当客端要求小额内存时,SGI 第二级配置器会主动将任何小额区块的内存需求量上调至 8 的倍数,并且从 free list 中获得对应的内存分配给客端。如果 free lists 没有对应的内存可用,就从内存池中取内存空间给 free lists;

  3. 当客端释放小额区块时,就由配置器将该小额内存区块回收到 free lists 中。

3):free-lists 的节点结构代码如下:

union obj {            
                union obj* free_list_link;
                char client_data[1];   /* The client sees this. */
};

free_list_link指向下一个相同形式的 union;

4):第二级配置器的部分实现代码如下:

enum { __ALIGN = 8 };
enum { __MAX_BYTES = 128 };
enum { __NFREELISTS = __MAX_BYTES/__ALIGN };

template <bool threads, int inst>
class __default_alloc_template{
public:
        static void* allocate (size_t);
        static void deallocate (void*, size_t);
        static void* reallocate (void*, size_t, size_t);

private:
        // ROUND_UP() 将 bytes 上调至 8 的倍数
        static size_t ROUND_UP (size_t bytes)
        { return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1)); }

private:
        union obj {             // free-lists 的节点构造
                union obj* free_list_link;
                char client_data[1];   /* The client sees this. */
        };

private:
        // 16 个 free-lists
        static obj* volatile free_list[__NFREELISTS];
        //以下函数根据区块大小,决定使用第 n 号 free-list,n 从 0 算起
        static size_t FREELIST_INDEX (size_t bytes)
        { return (((bytes) + __ALIGN - 1)/__ALIGN - 1); }

        //返回一个大小为 n 的对象,并可能加入大小为 n 的其它区块到 free list
        static void* refill (size_t n);
        //配置一大块空间,可容纳 nobjs 个大小为 "size" 的区块
        //如果配置 nobjs 个区块有所不便,nobjs 可能会降低
        static char* chunk_alloc (size_t size, int& nobjs);

        //Chunk allocation state
        static char* start_free; //内存池起始位置;
        static char* end_free;   //内存池结束位置;
        static size_t heap_size;
};

//以下是 static data member 的定义以及初值设定
template <bool threads, int inst>
char* __default_alloc_template<threads, inst>::start_free = 0;

template <bool threads, int inst>
char* __default_alloc_template<threads, inst>::end_free = 0;

template <bool threads, int inst>
__default_alloc_template<threads, inst>::obj* volatile __default_alloc_template<threads, inst>::free_list[__NFREELISTS]
 = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};

(1):空间配置函数allocate()

首先判断区块大小,大于 128 bytes 就调用第一级配置器,小于 128 bytes 就检查对应的 free list,如果 free list 有可用的区块,就直接拿来用,如果没有可用区块,就将区块大小上调至 8 倍数边界,然后调用refill()函数,为 free list 重新填充空间。

// n must be > 0
template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::allocate (size_t n)
{
        obj* volatile* my_free_list;
        obj* result;

        //大于 128 bytes就调用第一级配置器
        if (n > static_cast<size_t>(__MAX_BYTES)) {
                return malloc_alloc::allocate(n);
        }

        //寻找 16 个 free lists 中适当的一个
        my_free_list = free_list + FREELIST_INDEX(n);
        result = *my_free_list;
        if (result == 0){
                //没有找到可用的 free list,准备重新填充 free list
                void* r = refill(ROUND_UP(n));
                return r;
        }

        //调整 free list
        *my_free_list = result -> free_list_link;
        return result;
}

(2):空间释放函数deallocate()

该函数首先判断区块大小,大于 128 bytes 就调用第一级配置器,小于 128 bytes 就找出对应的 free list,将区块回收;

template <bool threads, int inst>
void __default_alloc_template<threads, inst>::deallocate(void* p, size_t n)
{
        obj* q = static_cast<obj*>(p);
        obj* volatile* my_free_list;

        //大于 128 bytes 就调用第一级配置器
        if (n > static_cast<size_t>(__MAX_BYTES)) {
                malloc_alloc::deallocate(p, n)l
                return;
        }

        //寻找对应的 free list
        my_free_list = free_list + FREELIST_INDEX(n);
        //调整 free list,回收区块
        q -> free_list_link = *my_free_list;
        *my_free_list = q;
}

(3):重新填充 free lists 函数refill()

当 free list 中没有可用区块了时,就调用refill()函数,为 free list 重新填充空间,新的空间取自内存池,默认取得 20 个新节点,但如果内存池空间不足,获得的节点数可能小于20;

//返回一个大小为 n 的对象,并且有时候为适当的 free list 增加节点
//假设 n 已经上调至 8 的倍数
template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n)
{
        int nobjs = 20;

        //调用 chunk_alloc(),尝试取得 nobjs 个区块作为 free list 的新节点
        char* chunk = chunk_alloc(n, nobjs);
        obj* volatile* my_free_list;
        obj* result;
        obj *current_obj, *next_obj;
        int i;

        //如果只获得一个区块,这个区块就分配给调用者用,free list 无新节点
        if (nobjs == 1) return chunk;
        //否则准备调整 free list,纳入新节点
        my_free_list = free_list + FREELIST_INDEX(n);

        //以下在 chunk 空间内建立 free list
        result = static_cast<obj*>(chunk); //这一块准备返回给客端
        //以下引导 free list 指向新配置的空间(取自 memory pool)
        *my_free_list = next_obj = static_cast<obj*>(chunk+n);
        //以下将 free list 的各节点串接起来
        for (i = 1; ;++i) { //从 1 开始,因为第 0 个将返回给客端
                current_obj = next_obj;
                next_obj = static_cast<obj*>((static_cast<char*>(next_obj) + n));
                if (nobjs -1 == i) {
                        current_obj -> free_list_link = 0;
                        break;
                }
                else {
                        current_obj -> free_list_link = next_obj;
                }
        }

        return result;
}

(4):chunk_alloc()函数

该函数负责给内存池分配空间以及从内存池中取空间给 free list 使用:

//假设 size 已经上调至 8 的倍数
template <bool threads, int inst>
char* __default_alloc_template<threads, inst>::chunk_alloc(size_t size, int& nobjs)
{
        char* result;
        size_t total_bytes = size * nobjs;
        size_t bytes_left = end_free - start_free; //内存池剩余空间

        if (bytes_left >= total_bytes) {
                //内存池剩余空间完全满足需求量
                result = start_free;
                start_free += total_bytes;

                return result;
        }
        else if (bytes_left >= size){
                //内存池剩余空间不能完全满足需求量,但足够供应一个(含)以上的区块
                nobjs = bytes_left/size;
                total_bytes = size * nobjs;
                result = start_free;
                start_free += total_bytes;

                return result;
        }
        else {
                //内存池剩余空间连一个区块大小都无法提供
                size_t bytes_to_get = 2 * toatl_bytes + ROUND_UP(heap-size >> 4);
                //以下试着让内存池中的残余零头还有利用价值
                if (bytes_left > 0){
                        //内存池中还有一些零头,先配给适当的 free list
                        obj* volatile* my_free_list = free_list + FREELIST_INDEX(bytes_left)
                        //调整 free list,将内存池中的残余空间编入
                        static_cast<obj*>(start_free) -> free_list_link = *my_free_list;
                        my_free_list = static_cast<obj*>(start_free);
        }

        //配置 heap 空间,用来弥补内存池
        start_free = static_cast<char*>(malloc(bytes_to_get));
        if (start_free == 0){
                // heap 空间不足,malloc() 失败
                int i;
                obj* volatile* my_free_list;
                obj* p;

                //以下搜寻适当的 free list
                //所谓适当是指“尚有未用区块,且区块足够大”之 free list
                for (i = size; i <= __MAX_BYTES; i += __ALIGN){
                        my_free_list = free_list + FREELIST_INDEX(i);
                        p = *my_free_list;
                        if (p != nullptr){ //free list 尚有未用区块
                                //调整 free list 以释放出未用区块
                                *my_free_list = p -> free_list_link;
                                start_free = static_cast<char*>(p);
                                end_free = start_free + i;
                                //递归调用自己,为了修正 nobjs
                                return chunk(size, nobjs);
                                //注意,任何残余零头终将被编入适当的free list 中备用
                        }
                }
        end_free = 0; //如果实在没有内存了
        //调用第一级配置器,看看 out-of-memory 机制能否起作用
        start_free = static_cast<char*>(malloc::allocate(bytes_to_get));
        //这会导致异常(exception),或内存不足情况得以改善
        }

        heap_size += bytes_to_get;
        end_free = start_free + bytes_to_get;
        //递归调用自己,为了修正 nobjs
        return chunk_alloc(size,nobjs);
}

六:内存基本处理工具

1:STL 定义有五个全局函数,作用于未初始化空间,之前介绍了用于构造的 construct()和用于析构的destroy()函数,另外三个函数是uninitialized_copy()uninitialized_fill()uninitialized_fill_n(),下面将会予以介绍;

2:POD 是指 Plain Old Data, 也就是标量型别(scalar types)或传统的 C struct 型别,POD 型别必须要拥有 trivial constructor/destructor/copy/assignment 函数;

3:函数uninitialized_copy()

(1):该函数的函数形式如下:

template <class InputIterator, class ForwardIterator>
inline ForwardIterator uninitialized_coyp(InputIterator first, InputIterator last,
ForwardIterator result)

该函数表示的意思是将迭代器 [first, last)范围内的每一个对象复制到从迭代器result开始指向的未构造内存中

(2):该函数源代码如下所示:


template <class InputIterator, class ForwardIterator>
ForwardIterator __uninitialized_coyp_aux(InputIterator first, InputIterator last, ForwardIterator result, __false_type)
{
        ForwardIterator cur = result;
        for ( ; first != last ; ++ first, ++cur)
                construct(&*cur,*first);

        return cur;
}
template <class InputIterator, class ForwardIterator>
inline ForwardIterator __uninitialized_coyp_aux(InputIterator first, InputIterator last, ForwardIterator result, __true_type)
{
        return copy(first, last, result);   //调用 STL 算法 copy()
}

template <class InputIterator, class ForwardIterator, class T>
inline ForwardIterator __uninitialized_coyp(InputIterator first, InputIterator last, ForwardIterator result, T*)
{
        typedef typename __type_traits<T>::is_POD_type is_POD;
        return __uninitialized_copy_aux(first, last, result, is_POD());
}

template <class InputIterator, class ForwardIterator>
inline ForwardIterator uninitialized_coyp(InputIterator first, InputIterator last, ForwardIterator result)
{
        return __uninitialized_copy(first, last, result, value_type(result));
}

inline char* uninitialized_copy(const char* first, const char* last, char* result)
{
        memmove(result, last, last - first);
        return result + (last - first);
}

inline wchar_t* uninitialized_copy(const wchar_t* first, const wchar_t* last, wchar_t* result)
{
        memmove(result, first, sizeof(wchar_t) * (last - first));
        return result + (last - first);
}

4:函数uninitialized_fill()

(1):该函数的函数形似如下:

template <class ForwardIterator, class T>
inline void uninitialized_fill(ForwardIterator first, ForwardIterator last,
const T& x)

表达的形式是如果 [first, last) 范围内的每个迭代器都指向未初始化的内存,那么该函数会在该范围内产生 x 的复制品

(2):该函数的源代码如下:

template <class ForwardIterator, class T>
void __uninitialized_fill_aux(ForwardIterator first, ForwardIterator last, const T& x, __false_type)
{
        ForwardIterator cur = first;
        for ( ; cur != last; ++cur)
                construct(&*cur, x);
}

template <class ForwardIterator, class T>
inline void __uninitialized_fill_aux(ForwardIterator first, ForwardIterator last, const T& x, __true_type)
{
        fill(first, last, x); //调用 STL 算法 fill()
}

template <class ForwardIterator, class T, class T1>
inline void __uninitialized_fill(ForwardIterator first, ForwardIterator last, const T& x, T1*)
{
        typedef typename __type_traits<T1>::is_POD_type is_POD;
        __uninitialized_fill_aux(first, last, x, is_POD());
}

template <class ForwardIterator, class T>
inline void uninitialized_fill(ForwardIterator first, ForwardIterator last, const T& x)
{
        __uninitialized_fill(first, last, x, value_type(first));
}

5:函数uninitialized_fill()_n

(1):该函数的形式为:

template <class ForwardIterator, class Size, class T>
inline ForwardIterator uninitialized_fill_n(ForwardIterator first, 
Size n, const T& x)

该函数的表达意思为:如果 [first, first+n) 范围内的每一个迭代器都指向未初始化内存,那么该函数会在该范围内产生 x 的复制品

(2):该函数的源代码如下:

template <class ForwardIterator, class Size, class T>
ForwardIterator __uninitialized_fill_n_aux(ForwardIterator first, Size n, const T& x, __false_type)
{
        ForwardIterator cur = first;
        for ( ; n > 0; --n, ++cur)
                construct(&*cur, x);

        return cur;
}

template <class ForwardIterator, class Size, class T>
inline ForwardIterator __uninitialized_fill_n_aux(ForwardIterator first, Size n, const T& x, __true_type)
{
        return fill_n(first, n, x); // 调用 STL 中的 fill_n 函数
}

template <class ForwardIterator, class Size, class T, class T1>
inline ForwardIterator __uninitialized_fill_n(ForwardIterator first, Size n, const T& x, T1*)
{
        typedef typename __type_traits<T1>::is_POD_type is_POD;
        return __uninitialized_fill_n_aux(first, n, x, is_POD());
}

template <class ForwardIterator, class Size, class T>
inline ForwardIterator uninitialized_fill_n(ForwardIterator first, Size n, const T& x)
{
        return __uninitialized_fill_n(first, n, x, value_type(first));
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值