[UE C++] TArray

[UE C++] TArray

概念

  • TArray 是UE4中最常用的容器类。其速度快、内存消耗小、安全性高。
  • TArray 类型由两大属性定义:元素类型内存分配器
  • 元素类型 是存储在数组中的对象类型。TArray 被称为同质容器。换言之,其所有元素均完全为相同类型。单个 TArray 中不能存储不同类型的元素。
  • 分配器 常被省略,默认为最常用的分配器。其定义对象在内存中的排列方式;以及数组如何进行扩展,以容纳更多的元素。若默认行为不符合要求,可选取多种不同的分配器,或自行编写。
  • TArray数值类型。意味其与其他内置类型(如 int32float)的处理方式相同。其设计时未考虑扩展问题,因此建议在实际操作中 勿使用 newdelete 创建或销毁 TArray 实例。元素也为数值类型,为容器所拥有
  • TArray 被销毁时其中的元素也将被销毁。若在另一TArray中创建TArray变量,其元素将复制到新变量中,且不会共享状态。

TArray与std::vector类似,为可动态扩充内存容器。内部主要有三个成员变量:

ElementAllocatorType AllocatorInstance;
SizeType             ArrayNum;
SizeType             ArrayMax;

其中AllocatorInstance为分配器,ArrayNum为元素实际个数,ArrayMax为最大可容纳元素个数。内存扩容方式和std::vector相似,当容量满了之后,会额外分配一个更大的内存,并将整个数组的数据拷到新内存上,之后再释放旧的内存。

1. 创建和初始化

默认的Allocator为对分配器,关于分配器的用法,本文不会涉及。

//IntArray == [0,1]

// 原始指针 + 数量
int32 nums[3]{ 0,1,2 };
TArray<int32> IntArray{ nums ,2 };

// 初始化列表
TArray<int32> IntArray = {0,1};

// 拷贝构造
TArray<int32> IntArray = TArray<int32>{0,1};

// 移动拷贝构造
TArray<int32> tem{0,1};
TArray<int32> IntArray{MoveTemp(tem)};

// 拷贝构造 + 额外空间,主要是为了性能优化
TArray<int32> tem{0,1};
TArray<int32> IntArray{tem,3}; // ArrayMax=5, ArrayNum=2

// 移动拷贝构造 + 额外空间,主要是为了性能优化
TArray<int32> tem{0,1};
TArray<int32> IntArray{MoveTemp(tem),3}; // ArrayMax=5, ArrayNum=2

// TArrayView
TArray<int32> tem{ 0,1 };
TArrayView<int32> TestView{ tem };
TArray<int32> InArray{ TestView };

关于TArrayView: 相当于一个拥有生命周期的TArray(类似于std::span — C++20),没有提供直接改变容器元素的API,类似一个视图。元素的生命周期取决于为它进行赋值的容器,使用时请格外注意生命周期。

2. 增

2.1 Init

首先清空TArray,然后向TArray中填充Number个Element

void Init(const ElementType& Element, SizeType Number);
// IntArray == [10,10,10,10,10]
IntArray.Init(10, 5);

2.2 Emplace

使用给定参数构建元素类型的新实例,添加到TArray末尾。返回添加的位置Index

SizeType Emplace(ArgsType&&... Args);
// IntArray == [0,1,5],index == 2
int32 index = IntArray.Emplace(5);

2.3 Add/Push

将元素类型的实例复制(或移动)到数组中,并返回添加的位置Index。内部会调用Emplace,不可避免的会涉及到临时变量的复制,Emplace 的效率始终高于 Add,但 Add 的可读性可能更好。

//用于移动语义,需要对象类实现移动构造函数
SizeType Add(ElementType&& Item);
SizeType Add(const ElementType& Item);
IntArray.Add(3);
// IntArray == [0,1,3]

2.4 Append

在TArray末尾添加多个元素(来自另一个TArray、原始数组、初始化列表)

//添加TArray
void Append(const TArray<OtherElementType, OtherAllocator>& Source);
//添加TArray,用于移动语义
void Append(TArray<OtherElementType, OtherAllocator>&& Source);
//添加原始数组
void Append(const ElementType* Ptr, SizeType Count);
//使用初始化列表构造并添加
void Append(std::initializer_list<ElementType> InitList);
IntArray.Append({10,20});

int32* nums = new int32[2]{ 30,40 };
IntArray.Append(nums, 2);

TArray<int32> TemIntArray{ 50,60 };
IntArray.Append(TemIntArray);
// IntArray == [0,1,10,20,30,40,50,60]

2.5 AddUnique

仅在尚不存在等值元素时,AddUnique 才会向容器添加新元素,使用元素运算符==来判断相等性。若存在需要添加的元素,则会返回它的index,若不存在则会返回新添加元素的index

//用于移动语义
SizeType AddUnique(ElementType&& Item);
SizeType AddUnique(const ElementType& Item);
IntArray.AddUnique(1);
IntArray.AddUnique(3);
// IntArray == [0,1,3]

2.6 Insert

在给定的Index中插入一个或者多个元素(来自另一个TArray、原始数组、初始化列表),返回插入的index

//插入一个元素
SizeType Insert(const ElementType& Item, SizeType Index);
//插入一个元素,用于移动语义
SizeType Insert(ElementType&& Item, SizeType Index);
//插入原生数组
SizeType Insert(const ElementType* Ptr, SizeType Count, SizeType Index);
//插入另一个TArray
SizeType Insert(const TArray<ElementType, OtherAllocator>& Items, const SizeType InIndex);
//插入另一个TArray,用于移动语义
SizeType Insert(TArray<ElementType, OtherAllocator>&& Items, const SizeType InIndex);
//初始化列表构造并插入
SizeType Insert(std::initializer_list<ElementType> InitList, const SizeType InIndex);
IntArray.Insert(2, 1);

IntArray.Insert({ 10,20 },1);

int32* nums = new int32[2]{ 30,40 };
IntArray.Insert(nums, 2,1);

TArray<int32> TemIntArray{ 50,60 };
IntArray.Insert(TemIntArray,1);
// IntArray == [0,50,60,30,40,10,20,2,1]

2.7 SetNum

SetNum 函数可直接设置数组元素的数量。

  • 如新数量大于当前数量,则使用元素类型的默认构造函数新建元素。
  • 如新数量小于当前数量,SetNum 将移除元素
  • bAllowShrinking 代表是否可以在合适的情况下收缩正在使用的内存,改变ArrayMax
SetNum(SizeType NewNum, bool bAllowShrinking = true);
// IntArray == [0,1,0]
IntArray.SetNum(3);
// IntArray == [0]
IntArray.SetNum(1);

2.8 AddDefaulted

采用默认构造函数,向TArray增加Count个元素

SizeType AddDefaulted(SizeType Count = 1);

2.9 更多

TArray存在很多变式的增加元素的函数,比如AddDefaulted_GetRefInsertDefaultedAdd_GetRef等等。一般增加之后返回的是元素的index,XXX_GetRef返回的就是增加元素的引用; XXXDefaulted代表以默认构造函数增加元素,一般都有一个输入值Count代表增加多少个元素,也有_GetRef变式,但只能增加一个元素了。

大家根据自己的需求进行关键词组合找到合适的API进行代码逻辑编写,这里就不行一一列举了(实在太多了😭)

3. 删

3.1 Remove

移除所有与提供元素等值的元素,返回删除元素的个数

SizeType Remove(const ElementType& Item);
IntArray = {0,0,1,1};
IntArray.Remove(1);
// IntArray == [0,0],return == 2

3.2 RemoveSingle

擦除数组中的首个匹配元素。以下情况尤为实用——此函数在数组中可能存在重复,而只希望删除一个时;或作为优化,数组只能包含一个匹配元素时。删除成功返回1,失败返回0。

SizeType RemoveSingle(const ElementType& Item);
IntArray = {0,1,0,1};
IntArray.RemoveSingle(1);
// IntArray == [0,0,1],return == 1

3.3 RemoveAt

按照从零开始的index移除元素。可使用 IsValidIndex 确定数组中的元素是否使用计划提供的索引,将无效索引传递给此函数会导致运行时错误:

// 删除指定Index的元素
void RemoveAt(SizeType Index);

// 删除指定Index开始的Count个元素, bAllowShrinking是否可以在合适的情况下在remove之后收缩数组,默认为true。不一定会减少Slack
void RemoveAt(SizeType Index, CountType Count, bool bAllowShrinking = true);
IntArray = {0,1,0,1};
IntArray.RemoveAt(2);
// IntArray == [0,1,1]
IntArray.RemoveAt(99);
// fatal error,无效的index

3.4 RemoveAll

数移除与谓词函数匹配的元素,下面的例子会移除0和1

SizeType RemoveAll(const PREDICATE_CLASS& Predicate);
IntArray = {0,1,0,1,2,3,4};
IntArray.RemoveAll([](int32 val) -> bool { return (val == 1 || val == 0); });
// IntArray == [2,3,4]

3.5 Empty

移除数组中所有元素,输入Slack代表移除元素后TArray的ArrayMax大小,默认为0。

void Empty(SizeType Slack = 0);
IntArray.Empty(2);
// IntArray == []

3.6 RemoveXXXSwap

移动过程存在开销。如不需要剩余元素排序,可使用 RemoveSwapRemoveSingleSwapRemoveAtSwapRemoveAllSwap 函数减少此开销。此类函数的工作方式与其非交换变种相似,不同之处在于其不保证剩余元素的排序,因此可更快地完成任务:

IntArray.Empty(10);
for (int32 i = 0; i != 10; ++i)
IntArray.Add(i % 5);
// IntArray == [0,1,2,3,4,0,1,2,3,4]

IntArray.RemoveSwap(2);
// IntArray == [0,1,4,3,4,0,1,3]

IntArray.RemoveAtSwap(1);
// IntArray == [0,3,4,3,4,0,1]

IntArray.RemoveAllSwap([](int32 Val) {return Val % 3 == 0;});
// IntArray == [1,4,4]

RemoveXXXSwap可以输入bAllowShrinking参数

4. 迭代

4.1 基于索引

for (int32 Index = 0; Index != IntArray.Num(); ++Index)
{
    IntArray[Index];
}

4.2 ranged-for

for (auto& val : IntArray)
{
    val;
}

4.3 迭代器

TArray.CreateIterator()可读可写与TArray.CreateConstIterator()只读迭代器

for (auto It = IntArray.CreateIterator(); It; ++It)
{
    *It;
}

5. 查

5.1 Num

返回ArrayNum,即已经存储的元素个数

FORCEINLINE SizeType Num() const
{
    return ArrayNum;
}

5.2 GetData

返回指向数组内存的原生指针,若容器为常量,则返回的指针也为常量。

int32* Ptr = IntArray.GetData();
// *Ptr == 0
// Ptr[0] == 0
// Ptr[1] == 1

5.3 GetTypeSize

返回单个元素所占大小

uint32 ElementSize = IntArray.GetTypeSize();
// ElementSize == sizeof(int32)

5.4 IsValidIndex

判断指定index是否有效

// true
IntArray.IsValidIndex(0);
// false
IntArray.IsValidIndex(2);

5.5 Last

返回从数组末端反向索引的元素引用。索引默认为零

ElementType& Last(SizeType IndexFromTheEnd = 0);
// return == 1
IntArray.Last();

// return == 1
IntArray.Last(0);

// return == 0
IntArray.Last(1);

5.6 Top

返回末端元素,相当于IntArray.Last()。不接受索引输入

// return == 1
IntArray.Top();

5.7 Contains

判断是否存在特定元素,返回bool。

template <typename ComparisonType>
bool Contains(const ComparisonType& Item) const;
// return == true
IntArray.Contains(1);

// return == false
IntArray.Contains(3);

请注意这里通过==进行判断是否相等,同时 Contains 是一个模板函数。代表若是不同的对象,但是重载了==运算符也是可以比较的。

class TestClass
{
public:
    int32 a;
    TestClass(int x)
    {
        a = x;
    }
};

bool operator==(int32 a, TestClass x)
{
    return a == x.a;
}

bool operator==(TestClass x,int32 a)
{
    return a == x.a;
}
TestClass tem(1);
// res == true
bool res = IntArray.Contains(tem);

5.8 ContainsByPredicate

通过谓词函数判断是否存在特定元素

bool ContainsByPredicate(Predicate Pred) const;
// return == true
IntArray.ContainsByPredicate([](const int32 item) { return (item == 0 || item == 3); });

5.9 Find

从头开始查找特定元素。若想从末尾向前查找则使用FindLast,使用方法相同

// 通过给定元素查找,若存在则会返回首个匹配的index,若不存在则会返回 INDEX_NONE(-1)
SizeType Find(const ElementType& Item) const;
// 通过给定元素查找,并将传入的Index进行设置,若查找成功返回true,失败则返回false
bool Find(const ElementType& Item, SizeType& Index) const;
// return == 1
IntArray.Find(1);
// return == -1
IntArray.Find(3);
// return == true , Index == 1
int32 Index;
IntArray.Find(1,Index);
// return == false , Index == -1
int32 Index;
IntArray.Find(3,Index);

5.10 FindByPredicate

从头开始通过谓词函数查找特定元素,返回元素 指针 而不是index。若想从末尾开始查找,则使用FindLastByPredicate

ElementType* FindByPredicate(Predicate Pred);
// *res == 0
auto* res = IntArray.FindByPredicate([](const int32 item) { return (item == 0 || item == 3); });

FindLastByPredicate有一点区别在于可以指定从第几个开始向前查找。如FindLastByPredicate(Predicate,3)代表从第三个元素开始向前查找(会查找0,1,2)

FORCEINLINE SizeType FindLastByPredicate(Predicate Pred) const
{
    return FindLastByPredicate(Pred, ArrayNum);
}

SizeType FindLastByPredicate(Predicate Pred, SizeType Count) const;

5.11 FindByKey

从头开始查找特定元素,通过==元素符。与Contains类似,支持不同对象之间的查找。返回对应元素的 指针

template <typename KeyType>
ElementType* FindByKey(const KeyType& Key);
TestClass tem(1);
// *re == 1
auto* re = IntArray.FindByKey(tem);

5.12 IndexOfByKey

FindByKey效果相同,返回的是元素的index而非指针

template <typename KeyType>
SizeType IndexOfByKey(const KeyType& Key) const;
TestClass tem(1);
// return == 1
IntArray.IndexOfByKey(tem)

5.13 IndexOfByPredicate

查找与特定谓词匹配的首个元素的index;若未找到,返回 INDEX_NONE 值。FindByPredicate 返回的是指针

template <typename Predicate>
SizeType IndexOfByPredicate(Predicate Pred) const
//return == 0
IntArray.IndexOfByPredicate([](const int32 item) { return (item == 0 || item == 3); });

5.14 FilterByPredicate

获取与特定谓词匹配的元素数组

template <typename Predicate>
TArray<ElementType> FilterByPredicate(Predicate Pred) const;
IntArray.Emplace(3);
IntArray.Emplace(1);
TArray<int32> ResArray = IntArray.FilterByPredicate([](const int32 item) { return (item == 0 || item == 3); });
// IntArray == [0,1,3,1]
// ResArray == [0,3]

6. 排序

6.1 Sort

底层使用 快速排序不稳定。默认排序规则:运算符<,支持输入谓词函数。

IntArray = { 5,6,3,2,1,9,8,7,0,4 };

// IntArray = [0,1,2,3,4,5,6,7,8,9]
IntArray.Sort();

// IntArray = [9,8,7,6,5,4,3,2,1,0]
IntArray.Sort([](const int32& A, const int32& B) {
    return A > B;
});

6.2 HeapSort

底层使用 堆排序不稳定。默认排序规则:运算符<,支持输入谓词函数。

IntArray = { 5,6,3,2,1,9,8,7,0,4 };

// IntArray = [0,1,2,3,4,5,6,7,8,9]
IntArray.HeapSort();

// IntArray = [9,8,7,6,5,4,3,2,1,0]
IntArray.HeapSort([](const int32& A, const int32& B) {
    return A > B;
});

6.3 StableSort

底层使用 归并排序稳定。默认排序规则:运算符<,支持输入谓词函数。

IntArray = { 5,6,3,2,1,9,8,7,0,4 };

// IntArray = [0,1,2,3,4,5,6,7,8,9]
IntArray.StableSort();

// IntArray = [9,8,7,6,5,4,3,2,1,0]
IntArray.StableSort([](const int32& A, const int32& B) {
    return A > B;
});

7. 运算符

7.1 []

返回指定index元素的引用

IntArray[0] = 10;
IntArray[1] = 11;
// IntArray == [10,11]

7.2 == !=

判断是否相等,所含元素进行比较(元素的顺序和数量相同时,两个数组才被视为相同)

// res == true
bool res = (IntArray == TArray<int32>{0, 1});
// res == false
bool res = (IntArray != TArray<int32>{0, 1});

7.3 +=

作为Append的替代,内部其实调用了Append

TArray& operator+=(const TArray& Other);
// 用于移动语义
TArray& operator+=(TArray&& Other);
// 初始化列表
TArray& operator+=(std::initializer_list<ElementType> InitList);
IntArray += TArray<int32>{0, 1};
IntArray += {3, 4};

7.4 =

赋值运算符,若赋值对象不是自己,则会销毁原来保存的数据。和构造函数大致相同,例子就不举了

// 另一个TArray
TArray& operator=(const TArray& Other);
// 用于移动语义
TArray& operator=(TArray&& Other);
// 初始化列表
TArray& operator=(std::initializer_list<InElementType> InitList);
// 另一个TArray,用于Allocator发生改变时
TArray& operator=(const TArray<ElementType, OtherAllocator>& Other);
// TArrayView
TArray& operator=(const TArrayView<OtherElementType, OtherSizeType>& Other);

8. Slack

当前未使用的有效预分配元素储存槽(ArrayMax - ArrayNum)。为避免每次添加元素时重新分配内存,分配器提供的内存通常会超过必要内存,使之后调用 Add 时不会因重新分配内存而降低性能。同样,删除元素通常不会释放内存,此操作会使数组拥有Slack元素。默认构建的数组不分配内存,Slack初始为零。

8.1 GetSlack()

返回Slack数量

TArray<int32> tem{0,1};
TArray<int32> IntArray{tem,3};
// return == 3
IntArray.GetSlack();

8.2 Max

返回ArrayMax

TArray<int32> tem{0,1};
TArray<int32> IntArray{tem,3};
// return == 5
IntArray.Max();

8.3 Reset

Empty类似,但不改变内存分配,除非新大小大于当前数组。如果需要,它调用保存项的析构函数,然后将ArrayNum归零。

void Reset(SizeType NewSize = 0);
IntArray.Reset();
// return == 2
IntArray.Max();

8.4 Shrink

将数组使用的内存缩小到最小,以存储当前在其中的元素。Slack == 0

IntArray.Reset();
IntArray.Shrink();
// return == 0
IntArray.Max();

8.5 Reserve

保留内存,使数组至少可以包含Number个元素。

void Reserve(SizeType Number);
IntArray.Reserve(10);
// return == 10
IntArray.Max();

9. 堆

TArray 拥有支持二叉堆数据结构的函数。堆是一种二叉树,其中父节点的排序等于或高于其子节点。作为数组实现时,树的根节点位于元素0,索引N处节点的左右子节点的指数分别为2N+1和2N+2。子节点彼此间不存在特定排序。

9.1 Heapify

调用 Heapify 函数可将现有数组转换为堆。此会重载为是否接受谓词,无谓词的版本将使用元素类型的 运算符< 确定排序。同一组元素可存在多个有效堆。

void Heapify(const PREDICATE_CLASS& Predicate);
void Heapify();
TArray<int32> HeapArr;
for (int32 Val = 10; Val != 0; --Val)
{
    HeapArr.Add(Val);
}
// HeapArr == [10,9,8,7,6,5,4,3,2,1]
HeapArr.Heapify();
// HeapArr == [1,2,4,3,6,5,8,10,7,9]
HeapArr.Heapify([](const int32& A, const int32& B) {
    return A > B;
});
// HeapArr == [10,9,8,7,6,5,4,3,2,1]

9.2 HeapPush

将新元素添加到堆,会对其他节点进行重新排序,以对堆进行维护。若在创建堆时指定了谓词函数,这时也需要将谓词函数输入,不然会采用默认运算符< 确定排序,造成堆顺序混乱。

// 默认 < 
SizeType HeapPush(const ElementType& InItem);
// 默认 <,用于移动语义
SizeType HeapPush(ElementType&& InItem);

// 自定义谓词
template <class PREDICATE_CLASS>
SizeType HeapPush(const ElementType& InItem, const PREDICATE_CLASS& Predicate);
// 自定义谓词,用于移动语义
template <class PREDICATE_CLASS>
SizeType HeapPush(ElementType&& InItem, const PREDICATE_CLASS& Predicate);

返回值为push元素在数组中的index

HeapArr = {10,9,8,7,6,5,4,3,2,1};
HeapArr.HeapPush(11);
// HeapArr == [10,9,8,7,6,5,4,3,2,1,11]

HeapArr = {10,9,8,7,6,5,4,3,2,1};
HeapArr.HeapPush(11,[](const int32& A, const int32& B) {
    return A > B;
});
// HeapArr == [11,10,8,7,9,5,4,3,2,1,6]

9.3 HeapPop

移除堆顶元素(index = 0),输入OutItem会被设置为堆顶元素的引用。注意自定义谓词需要被输入

// 自定义谓词
void HeapPop(ElementType& OutItem, const PREDICATE_CLASS& Predicate, bool bAllowShrinking = true);
// 默认 <
void HeapPop(ElementType& OutItem, bool bAllowShrinking = true);
HeapArr = {1,2,4,3,4,5,8,10,7,9,6};
int32 TopNode;
HeapArr.HeapPop(TopNode);
// TopNode == 1
// HeapArr == [2,3,4,6,4,5,8,10,7,9]

9.4 HeapPopDiscard

移除堆顶元素(index = 0),不做任何形式的返回

// 默认 <
void HeapPopDiscard(bool bAllowShrinking = true);
// 自定义谓词
void HeapPopDiscard(const PREDICATE_CLASS& Predicate, bool bAllowShrinking = true);

9.5 HeapRemoveAt

删除数组中给定索引处的元素,然后重新排列元素,对堆进行维护:

// 默认 <
void HeapRemoveAt(SizeType Index, bool bAllowShrinking = true);
// 自定义谓词
void HeapRemoveAt(SizeType Index, const PREDICATE_CLASS& Predicate, bool bAllowShrinking = true);
HeapArr = {2,3,4,6,4,5,8,10,7,9};
HeapArr.HeapRemoveAt(1);
// HeapArr == [2,4,4,6,9,5,8,10,7]

9.6 HeapTop

返回堆顶元素引用,不改变堆结构

int32 Item = HeapArr.HeapTop();

9.7 VerifyHeap

根据输入的谓词确保TArray为一个堆

template <class PREDICATE_CLASS>
void VerifyHeap(const PREDICATE_CLASS& Predicate)
{
    check(Algo::IsHeap(*this, Predicate));
}

注意事项: 所有关于堆的操作都建立在TArray本身为一个堆的基础上(Heapify)才能正常运行,堆操作默认使用元素类型的 运算符< 确定排序。如使用自定义谓词,须在所有堆操作中使用相同谓词。

10. 其它

10.1 Swap

交换两个index的元素,会对index进行合法性检测

void Swap(SizeType FirstIndexToSwap, SizeType SecondIndexToSwap);
IntArray.Swap(1,0);
// IntArray == [1,0];

存在SwapMemory,功能相同,但不会对index进行合法性检测

10.2 BulkSerialize

序列化函数,可用作替代 运算符<<,将数组作为原始字节块进行序列化,而非执行逐元素序列化。如使用内置类型或纯数据结构体等浅显元素,可改善性能。

10.3 GetAllocatedSize

返回此容器分配的内存量,只返回容器直接分配的大小,而不返回元素本身。

10.4 CountBytes

计数序列化此数组所需的字节数。输入为FArchive对象

最后

本文结合 官方文档 总结了TArray的常见API,最后还剩"Uninitialized""Zeroed"函数族没有讲解,这两个函数族用于原始数据的bit操作,并不会调用元素的构造函数,感兴趣的可以参考官方文档进行查阅,或者进源码查看注解。

本文较为基础,并没有涉及内部实现原理,可参考:

关于TArray的性能优化,可参考:

  • 23
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MustardJim

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值