深入理解C++空间配置器

1、简介

  其实以运用STL的角度来看,完全可以忽略空间适配器,因为每个容器都是通过默认参数指定好了allocator,通过查看vector的声明可以看出:

template<class _Ty,class _Alloc = allocator<_Ty> >
	class vector
{
//...
}

  如要使用自己的空间配置器,只需要在声明时传入这个参数即可。

标准空间配置器

STL中已经规定好了allocator的组成。
简单举例:

#ifndef __HN_ALLOC__
#define __HN_ALLOC__
#include <new>
#include <cstddef>
#include <cstdlib>
#include <climits>
#include <iostream>

namespace HN
{
	//开辟空间
    template<typename T>
    inline T *_allocate(ptrdiff_t size,T*)
    {
        set_new_handler(0);
        T *tmp = (T*)(::operator new((size_t)(size * sizeof(T))));
        if(tmp == 0)
        {
            std::cerr<<"out of memmory"<<std::endl;
            exit(1);
        }
        std::cout<<"allocate memory size:"<<size<<"  allocate memory address:"<<tmp<<std::endl;
        return tmp;
    }

    //归还先前配置的空间,直接使用delete
    template <typename T>
    inline void _deallocate(T* buffer)
    {
        std::cout<<"delete object!"<<"memory address:"<<buffer<<std::endl;
        ::operator delete(buffer);
    }
    //构造,使用定位new
    template<typename T1,typename T2>
    inline void _construct(T1* p,const T2& value)
    {
        std::cout<<"USE placement new TO CONSTRUCT OBJECT AT p "<<"memory address:"<<p<<std::endl;
        new(p) T1(value);
    }
    //析构,调用对象对应的析构函数
    template <typename T>
    inline void _destory(T* ptr)
    {
        std::cout<<"析构对象"<<std::endl;
        ptr->~T();
    }

    template <typename 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;

        //rebind allocator of type U
        // 成员模板 rebind
		// 定义了一个associated type other,other也是一个allocator的实例,但是负责管理的对象类型与T不同
		// 具体可以参考https://blog.csdn.net/qq100440110/article/details/50198789
        template <typename U>
        struct rebind
        {
            typedef allocator<U> other;
        };
        //内存申请,直接使用new
        pointer allocate(size_type n,const void * hint=0)
        {
            std::cout<<"allocate  "<<"size_type:"<<n<<std::endl;
            return _allocate((difference_type)n,(pointer)0);
        }
		//释放内存
        void deallocate(pointer p,size_type n)
        {
            std::cout<<"deallocate"<<std::endl;
            _deallocate(p);
        }
		//构造
        void construct(pointer p,const T& value)
        {
            std::cout<<"construct"<<std::endl;
            _construct(p,value);
        }
		//析构
        void destory(pointer p)
        {
            std::cout<<"destory"<<std::endl;
            _destory(p);
        }
		//返回地址
        pointer address(reference x)
        {
            std::cout<<"address"<<std::endl;
            return (pointer)&x;
        }
		//返回const对象的地址
        const_pointer const_address(const_reference x)
        {
            std::cout<<"const_address"<<std::endl;
            return (const_pointer)&x;
        }
		//可配置的最大量
        size_type max_size() const
        {
            std::cout<<"max_size"<<std::endl;
            return size_type(UINT_MAX/sizeof(T));
        }
    };
}
#endif

使用:

#include "hn_alloc.h"
#include <vector>
#include <iostream>
using namespace std;

int main()
{
    int ia[5]={0,1,2,3,4};
    unsigned int i;
    cout<<"TEST START!"<<endl;
    vector<int,HN::allocator<int>> iv(ia,ia+5);
    for ( i = 0; i < iv.size(); i++)
    {
        cout<<iv[i]<<" ";
    }
    cout<<endl;
    cout<<"TEST END!"<<endl;
}

输出日志:
在这里插入图片描述

2、具备次配置力的SGI空间配置器

在这里插入图片描述

2.1 SGI标准空间配置器std::allocator

  是SGI定义的符合部分STL标准的配置器,但由于效率不佳,不推荐使用。其实它就是对::operator new和::operator delete做了一层薄薄的封装。
在这里插入图片描述
在这里插入图片描述

2.2 SGI特殊的空间配置器 std::alloc

  由于一个内存配置与释放操作通常分两个阶段

class Foo{...};
Foo* pFoo = new Foo;// 第一阶段,干了俩事:1,配置内存 2,在配置好的内存上构造对象
delete pFoo; // 第二阶段,也干了俩事:1,析构对象 2,释放内存

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

  1,内存配置由alloc::allocate()负责,内存释放由alloc::deallocate()负责;

  2,对象构造由::construct()负责,对象析构由::destroy()负责。
在这里插入图片描述

2.3 构造和析构

在这里插入图片描述
构造,调用定位new
在这里插入图片描述
析构:分四个版本
1. 接受一个指针,直接调用该对象的析构函数即可。
在这里插入图片描述

2. 接受两个迭代器。此函数设法找出元素的数值型别,进而利用__type_traits<>求取最适当措施。就是说万一两个迭代器的范围很大,而每个对象的析构函数都是没啥用的,那么调用这些没啥用的析构函数会降低效率。所以,这里首先利用value_type()获得迭代器所指对象的类型,再利用__type_traits判断该类型的析构函数是否是没啥用的。如果是没啥用的,就啥也不做,否则调用其析构函数。
在这里插入图片描述
3&4. 针对迭代器为char 指针和wchar_t指针的特化版

在这里插入图片描述

2.4 空间的配置与释放 std::alloc

SGI是以malloc()和free()函数完成内存的配置与释放。
考虑到小型区块所可能造成的内存破碎问题,SGI设计了双层配置器:
  1.__malloc_alloc_template,第一级配置器直接使用malloc()和free()
  2.__default_alloc_template,第二级配置器视情况采用不同的策略:
当配置区块超过128bytes时,视之为“足够大”,就调用第一级配置器;
当配置区块小于128bytes时,视之为“过小”,为了降低额外负担,便采用复杂的内存池整理方式。
是否开放第二级配置器取决于是否定义__USE_MALLOC。//SGI未定义,则表示同时使用两个配置器。

无论alloc被定义为第一级或第二级配置器,SGI还为它再包装一个接口,使其能够符合STL规范:
在这里插入图片描述
这个接口使配置器的配置单位从bytes转为单个元素的大小。SGI STL容器全都使用simple_alloc接口:
在这里插入图片描述

3、一二级空间配置器剖析

其关系:
在这里插入图片描述
第一级与第二级配置器的包装接口和运用方式:
在这里插入图片描述

3.1 第一级配置器__malloc_alloc_template剖析

基本原理:

#if 0
#include<new>
#define __THROW_BAD_ALLOC throw bad_alloc
#elif !defined(__THROW_BAD_ALLOC)
//#include<iostream.h>
#define __THROW_BAD_ALLOC cout<<"Out Of Memory."<<endl; exit(1)
#endif
 
//inst完全没用
template<int inst>
class __malloc_alloc_template
{
private:
	//以下用来处理内存不足的情况;oom:out of memory
	static void * oom_malloc(size_t n); 
	static void * oom_realloc(void *p, size_t n);
	static void(*__malloc_alloc_oom_handler)();
 
public:
	static void* allocate(size_t n)
	{
		void *result = malloc(n); //< 直接调用malloc()
		if (result == 0)
			result = oom_malloc(n); //< 分配失败调用oom_malloc()
		return result;
	}
 
	static void  deallocate(void *p, size_t)
	{
		free(p); //< 直接调用free()
	}
 
	static void* reallocate(void *p, size_t old_sz, size_t new_sz)
	{
		void *result = realloc(p, new_sz); //< 直接调用C中的realloc()
		if (0 == result)
			result = oom_realloc(p, new_sz);  //< 分配失败调用oom_realloc
		return result;
	}
 
	//模拟C++中的set_new_handler(),也就是通过这个函数指针来指定自己的out-of-memory操作
	static void(* set_malloc_handler(void(*f)()))()
};
 
// 初值为0,客户端指定
template<int inst>
void(*__malloc_alloc_template<inst>::__malloc_alloc_oom_handler)() = 0;
 
//如果指定了 __malloc_alloc_oom_handler,则循环调用,直到分配到内存,否则抛异常
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 (0 == my_malloc_handler)
		{
			__THROW_BAD_ALLOC;
		}
		(*my_malloc_handler)();
		result = malloc(n);
		if (result)
			return result;
	}
}
 
//如果指定了 __malloc_alloc_oom_handler,则循环调用,直到分配到内存,否则抛异常
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 (0 == my_malloc_handler)
		{
			__THROW_BAD_ALLOC;
		}
		(*my_malloc_handler)();
		result = realloc(p, n);
		if (result)
			return result;
	}
}

第一级配置器中的allocator()直接调用C中的malloc()函数,reallocator()直接调用C中的realloc()。deallocate()直接调用C中的free()函数。
其中使用了C++ new-handler机制:

你可以要求系统在内存配置需求无法被满足时,调用一个你所指定的函数。
就是说一旦::operator new无法完成任务,在丢出std::bad_alloc之前,会先调用客端指定的处理例程。
该例程被称为new-handler。new-handler是解决内存不足的特有的做法。//参考Effective C++ 条款7

SGI不是用::operator new来配置内存,所以不能使用C++的set_new_handler,可以通过模拟C++中的set_new_handler()的set_malloc_handler()来设定。如果用户指定了,则循环调用这个handler,直到分配到内存,如果没定义,则直接抛bad_alloc异常,或者利用exit(1)直接终止程序。

3.2 第二级配置器__default_alloc_template剖析

基本过程图解:
在这里插入图片描述
当配置区块小于128bytes时,视之为“过小”,为了降低额外负担,便采用复杂的内存池整理方式。称之为次层配置:

每次配置一大块内存,并维护对应值自由链表(free-list)。下次若有相同大小的内存需求,就直接从free-list中取出,
如果客端释放小额区块,就由配置器回收到free-list中。

free-list
  free_list 维护了16个链表,每个链表my_free_list 存放的节点的大小是~~~这一类同样大小的内存(一般是20个),最小的是8个字节,然后开始以8的倍数递增,分别是 8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128,perfect !!!,刚好到128个字节,这就是为什么大于128bytes要使用第一级配置器了,因为,第二级配置器最多只能分配128bytes。
16个链表,每一个节点都是这种结构:

union obj{
        union obj *free_list_link;
        char client_data[1];
}

union的妙用
  union 修饰的这种数据类型,里面可以存放多种不同类型的数据,并且公用一块内存,也就是说 free_list_link 和client_data 用的是同一块内存,前者表示指向下一个节点的指针,后者表示一个指向实际内存的指针,它们公用同一块内存,正是因为无论在什么情况下,二者只用其一,情况无非两种,分配和不分配,不分配的时候,节点就放在空闲链表里面,节点内容是下一个节点的地址,如果被分配了,那么节点内容就是指向一块实际的内存,这样就不用在节点里开两个不同内存的变量,节省了好多空间。
free-list分配:
  如果用户要申请大小为N的内存,如果N>128,用第一级配置器,如果N<128,假设N=15,为了避免内存碎片,把N上调到8的倍数,这里是16,那么就去找到大小为节点16的那个空闲链表,my_free_list = free_list + FREELIST_INDEX(N), FREELIST_INDEX(N),的功能是把通过N上调到合适的8的倍数,然后算出相应的那一个free_list的号数,比如说:N = 15 那就调到16,16/8 = 2,从0开始算那就是第一号free_list,如果那个链表free_list 有可用的内存块,那么就把第一块拿出来,然后就把头指针往下移动一个单位 ,比如说找的了链表指针是my_free_list,取出第一个 result = * my_free_list , 链表头指针下移my_free_list = result ->free_list_link;
free-list没有了:
  如果第二号free_list的内存块都用完了,这个时候,向内存池申请,一般是20个大小为16bytes的内存,把第一个返回,其余19个加到free_list中。
memory pool 不够:
  内存池不够提供20个大小为16的块,但是足够提供几个可能是7个,那么就把这7个给分配出去1个返回,6个加到free_list,注意这个时候内存池已经空了。
memory pool没了:
  如果内存池里边只有一个,那就直接返回这个,如果内存池里边的大小不足以提供一个申请的单位,如不足16bytes,那么也不要浪费,把剩下来的一点点进行判断 是否为8的倍数,分出去加到相应的free_list里边,这个时候就要开始使用malloc来分配heap内存了,这次申请的标准也定的挺巧妙,是 (40+n)*16 ,因为free_list向内存池申请20 * 16 bytes,那么我就申请双倍一半给free_list ,一半给memory pool,至于n嘛,为1个附加值,你申请的次数越多,n就越大,这更符合实际。
基本原理:

enum { __ALIGN = 8 }; //< 小型区块的上调边界
enum { __MAX_BYTES = 128 }; //< 小型区块的上限
enum { __NFREELISTS = __MAX_BYTES / __ALIGN }; //< freelist个数:16个
 
template<bool threads, int inst>
class __default_alloc_template
{
public:
	//< 三个接口
	static void *allocate(size_t n);
	static void deallocate(void *p, size_t n);
	static void* reallocate(void *p, size_t old_sz, size_t new_sz);
 
private:
	// 将申请的size上调至__ALIGN的倍数
	static size_t ROUND_UP(size_t bytes)/
	{
		return (bytes + __ALIGN - 1) & ~(__ALIGN - 1);//
	}
	
	// freelist节点结构
	union obj
	{
		union obj * free_list_link;
		char client_data[1];
	};
 
	// 根据要申请的区块大小,决定使用第n号freelist,n从0算起
	static size_t FREELIST_INDEX(size_t bytes)
	{
		return (bytes + __ALIGN - 1) / __ALIGN - 1;
	}
 
	// 返回一个大小为n的区块对象,并可能(通常)加入大小为n的其他区块到freelist
	static void* refill(size_t n);
 
	// 配置一大块空间,可容纳nobjs个大小为size的区块
	// 注意此处nobjs是引用,如果配置有所不便(内存不够),nobjs会降低
	static char* chunk_alloc(size_t size, int &nobjs);
 
	static obj * free_list[__NFREELISTS]; //< 16个freelist 
	static char *start_free;//< 内存池其实位置
	static char *end_free; //< 内存池结束位置
	static size_t heap_size; //< 配置内存的附加量
};
 
// 赋初值
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>
size_t __default_alloc_template<threads, inst>::heap_size = 0;
 
template<bool threads, int inst>
__default_alloc_template<threads, inst>::obj*
__default_alloc_template<threads, inst>::free_list[__NFREELISTS] =
{ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 };

3.2.1空间配置函数allocate()

以下代码描述了如何利用二级配置器的allocator()配置空间,以及free-list空间如何与内存池之间通信:

static void* allocate(size_t n)
{
	obj* volatile* my_free_list;
	void* result = 0;
 
	//如果大于128B, 直接调用一级配置器
	if (n > (size_t)_MAX_BYTES) 
	{
		return (malloc_alloc::allocate(n));
	}
	//寻找 16个free-list 中的一个
	my_free_list = free_list + FREELIST_INDEX(n);
	result = *__my_free_list;
	if (result == 0)
	{
		//如果freelist上没有可用空间,则将空间调整至8的倍数
        //调用refill,向mempool申请内存,重新填充该freelist
		result = refill(ROUND_UP(n));
		return result;
	}
	else 
	{
		*my_free_list = result->_M_free_list_link;
	}
 
	return result;
};

若free-list中没有可用区块时,调用refill()
其中freelist和内存池通信函数refill()的源码如下:

template<bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n)
{
	//默认取20个新节点连接到freelist上(其实是19个,第一个返回给用户)
	int nobjs = 20;
 
	//调用chunk_alloc(),尝试取得nobjs个区块作为freelist的新节点
	//注意此处参数nobjs是通过引用传入,有可能变小
	char* chunk = chunk_alloc(n, nobjs);
	obj* volatile* my_free_list;
	obj * result;
	obj * current_obj, *next_obj;
	int i;
 
	//如果只获得一个区块,则将这个区块直接反馈,freelist无新节点
	if (1 == nobjs)
	{
		return chunk;
	}
 
	//找到需要填充的链表的位置
	my_free_list = freeList + FREELIST_INDEX(n);
	result = (obj*)chunk;//第一块返回给客户端
	//引导freelist指向新的空间
	*my_free_list = next_obj = (obj*)(chunk + n);//这里把第二块先挂到指针数组对应位置下  //注意这里的n在传参数时已经调整到8的倍数
	for (i = 1;; i++) {//从1开始,0返回给客户端
		cur_obj = next_obj;
		next_obj = (obj*)((chat*)next_obj + n);
		if (nobjs - 1 == i) {                   //因为第一次从内存池取下的空间在物理上是连续的 尾插方便用 以后用完还回自由链表的就不是了
			cur_obj->free_list_link = NULL;//这里没有添加节点
			break;
		}
		else {
			cur_obj->free_list_link = next_obj;//nobjs - 2是最后一次添加节点
		}
	}
	return result;
}

3.2.2 空间释放函数 deallocate()

源码:

static void deallocate(void* p, size_t n)
{
	obj* volatile*  my_free_list;
	obj* q = (obj*)p;
 
	//如果大于128,调用第一级配置器
	if (n > (size_t)_MAX_BYTES)
	{
		malloc_alloc::deallocate(p, n);
		return;
	}	
		
	//寻找对应的freelist
	my_free_list = _S_free_list + _S_freelist_index(n);
	//回收该区块
	q->free_list_link = *my_free_list;
	*my_free_list = q;
}

  如果要释放内存的话,很简单,若释放的大小size>128bytes,直接调用第一级配置器的释放函数dealloc(),其实就是内部调用free(),如果释放的大小<128那么判断大小从而确定 属于哪一号free_list,比如说是64,那就是64/8 -1,那就是第7号free_list,把这块要回收的内存插到第7号free的开头,然后指针my_free_list指针向前移动一个节点,ok!!!

3.2.3 二级配置器总流程框图:

在这里插入图片描述

3.3 内存池

chunk_alloc()是负责内存池与系统内存打交道的:

template<bool threads, int inst>
void* __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 * total_bytes + ROUND_UP(heap_size >> 4);
		// 以下试着让内存池中的残余零头还有利用价值
		if (bytes_left > 0) 
		{
			// 内存池内还有一些零头,先配给适当的free list
			// 首先寻找适当的free list
			obj * volatile * my_free_list = free_list + FREELIST_INDEX(bytes_left);
			// 调整free list,将内存池中的残余空间编入
			((obj *)start_free)->free_list_link = *my_free_list;
			*my_free_list = (obj *)start_free;
		}
 
		// 配置heap空间,用来补充内存池
		start_free = (char *)malloc(bytes_to_get);
		if (0 == start_free) 
		{
			// heap空间不足,malloc失败
			int i;
			obj * volatile * my_free_list, *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 (0 != p) 
				{ // free list内尚有未用块
							  // 调整free list以释放未用区块
					*my_free_list = p->free_list_link;
					start_free = (char *)p;
					end_free = start_free + i;
					// 递归调用自己,为了修正nobjs
					return chunk_alloc(size, nobjs);
					// 注意,任何残余零头终将被编入适当的free list中备用
				}
			}
			end_free = 0; // 如果出现意外,调用第一级配置器,看看oom机制能否尽力
			start_free = (char *)malloc_alloc::allocate(bytes_to_get);
			// 这会抛出异常 或 内存不足的情况得到改善
		}
		heap_size += bytes_to_get;
		end_free = start_free + bytes_to_get;
		// 递归调用自己,为了修正nobjs
		return chunk_alloc(size, nobjs);
	}
}

引用书中解释:
在这里插入图片描述

4、内存基本处理工具

提供三个工具uninitialized_copy()、uninitialized_fill()、uninitialized_fill_n(),用于将内存的配置与对象的构造分别开来。
预备知识:

POD:意指Plain Old Data,也就是标量型别或传统的C struct型别。POD必然拥有trivial ctor/dtor/copy/assignment函数)
指的是没啥用的 construct/destory/copy construct/assignment construct函数//真烦人,写全不好吗!
//什么叫有用的或无用的这些函数,在深度探索C++中有详细说明。
对POD型别采取最有效的初值填写法,如:

    int a;
    a = 5;

而对non-POD型别采取最保险的安全做法:

    char* p = new char;
    new(p) char(5);

4.1 uninitialized_copy()

在这里插入图片描述
它会使用复制构造函数产生相应的复制品,放置于输出范围的相对位置上(迭代器指向的未初始化内存区域)。
容器的全区间构造函数通常以两个步骤完成:
  1.配置内存块,足以包含范围内的所有元素。
  2.使用uninitialized_copy(),在该内存块上构造元素。

该函数具有原子性,要么构造所有必要元素,要么不构造任何东西。
实现技术:
首先萃取出result的value_type,然后判断是否为POD类型。
在这里插入图片描述
如果是POD类型,就转到以下函数:
在这里插入图片描述
如果不是POD类型,就转到以下函数:
  在这里插入图片描述
针对char 指针和wchar_t指针两种类型,可以采用最具效率的做法memmove(直接移动内存内容)来执行复制行为。
在这里插入图片描述

4.2 uninitialized_fill()

在这里插入图片描述
该函数会在迭代器指向的未初始化的内存块中产生x的复制品。
该函数也具有原子性,如果有任何一个复制构造丢出异常,必须将已产生的所有元素析构掉。
实现技术:
首先萃取出first的value_type,然后判断该类型是否是POD类型:
在这里插入图片描述
如果是POD类型,转到以下函数:
在这里插入图片描述
如果不是POD类型,转到以下函数:
在这里插入图片描述

4.3 uninitialized_fill_n()

在这里插入图片描述
同上,也具有原子性,并且可以指定欲初始化的空间的大小。
实现技术:
首先萃取出迭代器first的value_type,然后判断是否为POD类型。
在这里插入图片描述
如果是POD类型,就转到以下函数:
在这里插入图片描述
如果不是POD类型,就转到以下函数:
在这里插入图片描述

三个内存基本函数的泛型或特化版本总框图

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值