C++(12):动态内存

除了自动和static对象外,C+还支持动态分配对象。动态分配的对象的生存期与它们在哪里创建是无关的,只有当显式地被释放时,这些对象才会销毁。

静态内存 用来保存局部static对象、类static数据成员以及定义在任何函数之外的变量。
栈内存 用来保存定义在函数内的非static对象。

分配在静态或栈内存中的对象由编译器自动创建和销毁。
对于栈对象,仅在其定义的程序块运行时才存在:static对象在使用之前分配,在程序结束时销毁。

除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称作 自由空间(free store)堆(heap)

程序用堆来存储动态分配的对象——即,那些在程序运行时分配的对象。动态对象的生存期由程序来控制,也就是说,当动态对象不再使用时,代码必须显式地销毁它们

忘记释放内存会产生内存泄漏,在尚有指针引用内存的情况下释放它会产生非法内存的指针。

动态内存和智能指针

c++ 使用 newdelete 管理动态内存
new:在动态内存中为对象分配空间并返回一个指向该对象的指针
delete:接受一个动态对象的指针,销毁该对象,并释放与之有关的内存。

新的标准库提供了两种智能指针类型来管理动态对象,都定义在 memory 头文件中。其类似于常规指针,主要区别在于管理底层指针的方式:
1.shared_ptr 允许多个指针指向同一个对象;
2.unique_ptr 则“独占”所指向的对象。
3.weak_ptr 伴随类,是一种弱引用,指向 shared_ptr 所管理的对象。

shared_ptr 类

类似 vector,智能指针也是模板。因此,创建一个智能指针时,必须提供额外的信息——指针可以指向的类型。

shared_ptr<string> pl;// shared_ptr,可以指向string
shared_ptr<list<int>> p2;// shared_ptr,可以指向int的list

默认初始化的智能指针中保存着一个空指针。

智能指针的使用方式与普通指针类似。解引用一个智能指针返回它指向的对象。如果在一个条件判断中使用智能指针,效果就是检测它是否为空:

//如果p1不为空,检查它是否指向一个空
string if(p1 && p1->empty())
	*pl = "hi";//如果pl指向一个空string,解引用p1,将一个新值赋予string

shared_ptrunique_ptr 都支持的操作
在这里插入图片描述
shared_ptr 独有的操作
在这里插入图片描述

make_shared 函数

最安全的分配和使用动态内存的方法是调用一个名为 make_shared 的标准库函数,在 memory 头文件中。
在动态内存中分配一个对象并初始化它,返回指向此对象的 shared_ptr

//指向一个值为42的int的shared_ptr
shared_ptr<int> p3 = make_shared<int>(42);
// p4指向一个值为"9999999999"的string
shared_ptr<string> p4 = make_shared<string>(10,'9');
// p5指向一个值初始化的int,即,值为0
shared_ptr<int> p5 = make_shared<int>();

类似于顺序容器的 emplace 成员,make_shared 是函数模板,用其参数来构造给定类型的对象。

shared_ptr 的拷贝和赋值

当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象:

auto p = make_shared<int>(42);// p 指向的对象只有 p 一个引用者
auto q(p) ; // p 和 q 指向相同对象,此对象有两个引用者

可以认为每个 shared_ptr 都有一个关联的计数器,通常称其为引用计数。无论何时拷贝一个 shared_ptr,计数器都会递增。
比如当用一个 shared_ptr 初始化另一个 shared_ptr,或将它作为参数传递给一个函数以及作为函数的返回值时,它所关联的计数器就会递增。
如果 shared_ptr 的计数器变为 0,就会自动释放管理的对象。给 shared_ptr 赋予一个新值或是shared_ptr 被销毁时,计数器会递减。

auto r = make_shared<int>(42);//r 指向的 int 只有一个引用者
r =q;//给 r 赋值,令它指向另一个地址
//递增 q 指向的对象的引用计数
//递减 r 原来指向的对象的引用计数
//r 原来指向的对象已没有引用者,会自动释放

shared_ptr 自动销毁所管理的对象……

shared_ptr 通过析构函数来完成销毁。

shared_ptr 的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0shared_ptr的析构函数就会销毁对象,并释放它占用的内存。

……shared_ptr 还会自动释放相关联的内存

当动态对象不再被使用时,shared_ptr 类会自动释放动态对象。

最后一个 shared_ptr 销毁前内存都不会释放,所以要保证 shared_ptr 无用之后就不要再保留了。

如果将shared_ptr存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用erase删除不再需要的那些元素,否则会浪费内存。

程序使用动态内存的三种原因

1.程序不知道自己需要使用多少对象;
2.程序不知道所需对象的准确类型;
3.程序需要在多个对象间共享数据。

直接内存管理

运算符 new 分配内存,delete 释放 new 分配的内存。
相比于智能指针,这两个运算符管理内存非常容易出错,而且自己直接管理内存的类与使用智能指针的类不同,它们不依赖于类对象拷贝、赋值和销毁操作的任何默认定义。

使用 new 动态分配和初始化对象

在自由空间分配的内存是无名的,因此 new 无法为其分配的对象命名,而是返回一个指向该对象的指针:

int *pi = new int;//pi指向一个动态分配的、未初始化的无名对象
// 此 new 表达式在自由空间构造一个 int 型对象,并返回指向该对象的指针

默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的。而类类型对象将用默认构造函数进行初始化:

int* p = new int;   //默认初始化 10
string* sp = new string(10,'9');//直接初始化“9999999999”
vector<int>* vp = new vector<int>{0,1,2,3};//列表初始化

对于定义了自己的构造函数的类类型,不管采用什么形式,对象都会通过默认构造函数来初始化。
对于内置类型,值初始化的内置类型对象有着良好定义的值,而默认初始化的对象的值则是未定义的。
类似的,对于类中那些依赖于编译器合成的默认构造函数的内置类型成员,如果它们未在类内被初始化,那么它们的值也是未定义的。

动态分配 const 对象

new 分配 const 对象是合法的:

//分配并初始化一个const int
const int *pci = new const int (1024);
//分配并默认初始化一个const的空 string 
const string *pcs = new const string;

类似其他任何const对象,一个动态分配的const对象必须进行初始化。对于一个定义了默认构造函数的类类型,其const动态对象可以隐式初始化,而其他类型的对象就必须显式初始化。由于分配的对象是const的,new返回的指针是一个指向const的指针。

内存耗尽

一旦一个程序用光了它所有可用的内存,new表达式就会失败。
默认情况下,如果 new 不能分配所要求的内存空间,会抛出一个类型为 bad_alloc 的异常。
可以改变使用 new 的方式来阻止它抛出异常:

//如果分配失败,new返回一个空指针
int *pl = new int; //如果分配失败,new抛出std::bad_alloc
int *p2 = new (nothrow) int;//如果分配失败,new返回一个空指针

释放动态内存

delete 执行两个动作:销毁给定的指针指向的对象;释放对应的内存。

delete p;// p必须指向一个动态分配的对象或是一个空指针

指针值和 delete

传递给 delete 的指针必须指向动态分配的内存,或者是一个空指针。释放一块并非 new 分配的内存,或者将相同的指针释放多次,其行为是未定义的:

int i, *pi1 = &i, *pi2=nullptr;
double *pd = new double(33),*pd2 = pd;
delete i;//错误:i不是一个指针
delete pi1;//未定义:pi1指向一个局部变量
delete pd;//正确
delete pd2;//未定义:pd2指向的内存已经被释放了
delete pi2;//正确:释放一个空指针总是没有错误的

const 动态对象的值不可改变,但是可以通过 delete 指向它的指针进行销毁。

动态对象的生存期知道释放时为止

shared_ptr 管理的内存在最后一个 shared_ptr 销毁时会被自动释放;
对于通过内置指针类型来管理的内存(动态对象),需要显式释放。
通常编译器不能分辨 delete 的对象是动态还是静态分配的对象,也不能分辨一个指针所指的内存是否已被释放。

动态内存的管理容易出现的问题

1.忘记 delete 内存。产生“内存泄漏”:内存无法归还给自由空间;
2.使用已释放掉的对象;
3.同一块内存释放两次:自由空间可能会被破坏。

使用智能指针可以避免这些问题。对于一块内存,只有在由任何智能指针指向它的情况下,智能指针才会自动释放它。

delete 之后重置指针值

delete 一个指针后,其指针值变为无效,但仍然保存着(已经释放掉的)动态内存的地址。
delete 之后,指针就变成了空悬指针,即指向一块曾经保存数据对象但现在已经无效的内存的指针。
未初始化指针的所有缺点空悬指针也都有。
避免空悬指针的问题: 在指针即将要离开其作用域之前释放掉它所关联的内存。这样,在指针关联的内存被释放掉之后,就没有机会继续使用指针了。如果我们需要保留指针,可以在 delete之后将nullptr赋予指针,这样就清楚地指出指针不指向任何对象。

shared_ptr 和 new 结合使用

如果不初始化一个智能指针,它就会被初始化为一个空指针。
还可以用 new 返回的指针来初始化智能指针:

shared_ptr<double> pl;// shared_ptr 可以指向一个double
shared_ptr<int> p2(new int (42));// p2指向一个值为42的int

接受指针参数的智能指针构造函数是explicit的。explicit 一般用来防止隐式转换。
必须使用直接初始化形式来初始化一个智能指针:

shared_ptr<int> pl = new int(1024);//错误:必须使用直接初始化形式
shared_ptr<int> p2(new int(1024));//正确:使用了直接初始化形式

定义和改变 shared_ptr 的其他方法:

shared_ptr<T> p(q);// p 管理内置指针 q 所指向的对象;q必须指向new分配的内存,且能够转换为T*类型
shared_ptr<T> p(u)// p从 unique_ptr u那里接管了对象的所有权;将u置为空
shared_ptr<T> p(a,d)//p接管了内置指针q所指向的对象的所有权。q必须能转换为T*类型。p将使用可调用对象d来代替delete
shared ptr<T> p(p2,d)//p是shared ptrp2的拷贝,唯一的区别是p将用可调用对象d来代替delete
p.reset()
p.reset (q)
p.reset(q,d)//若p是唯一指向其对象的shared_ptr,reset会释放此对象。若传递了可选的参数内置指针q,会令p指向q,否则会将p置为空。若还传递了参数d,将会调用d而不是delete来释放q

不要混合使用普通指针和智能指针

shared_ptr可以协调对象的析构,但这仅限于其自身的据贝(也是shared_ptr)之间。这也是为什么推荐使用make_ shared而不是new的原因。

make_ shared 能在分配对象的同时就将 shared_ptr 与之绑定,从而避免将同一块内存绑定到多个独立创建的 shared_ptr 上。

使用一个内置指针来访问一个智能指针非常危险,因为无法知道对象何时会被销毁。

也不要使用 get 初始化另一个智能指针或为智能指针赋值

智能指针类型定义了一个名为 get 的函数,它返回一个内置指针指向智能指针管理的对象。此函数设计目的:需要向不能使用智能指针的代码传递一个内置指针。
使用get返回的指针的代码不能delete此指针。

sharedptr<int>p(newint(42));//引用计数为1
int*q=p.get();//正确:但使用q时要注意,不要让它管理的指针被释放
{//新程序块
//未定义:两个独立的sharedptr指向相同的内存
shared ptr<int>(q);
)//程序块结束,q被销毁,它指向的内存被释放
int foo=*p;//未定义:p指向的内存已经被释放了

get 用来将指针的访问权限传递给代码,只有在确定代码不会 delete 指针的情况下,才能使用 get。
特别是,永远不要用 get 初始化另一个智能指针或者为另一个智能指针赋值。

智能指针和异常

使用异常处理的程序能在异常发生后令程序流程继续,需要确保在异常发生后资源能被正确地释放,一种简单的确保资源被释放的方法是使用智能指针。

如果使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放。内置指针管理内存时,在 new 之后在对应的 delete 之前发生了异常,则内存不会被释放。

智能指针和哑类

C++ 中很多类都定义了析构函数,负责清理对象使用的资源,但是那些为 C 和 C++ 两种语言设计的类,通常要求用户显式地释放所使用地任何资源。

那些分配了资源,而又没有定义析构函数来释放这些资源的类,可能会遇到与使用动态内存相同的错误——忘记释放资源。类似的,如果在资源分配和释放之间发生了异常,程序也会发生资源泄漏。

使用自己的释放操作

默认情况下,shared_ptr 假定它们指向的是动态内存。因此,当一个shared_ptr 被销毁时,它默认地对它管理的指针进行 delete操作。可以使用 shared_ptr 管理其他对象,但需要定义一个相应的删除器函数来代替 delete,完成对 shared_ptr 中保存地指针进行释放地操作。

//删除器接受单个类型位 connection* 的参数:
//void end_connection (connection *p){disconnect(*p);}

void f(destination &d/*其他参数*/){
	connection c = connect(&d);
	shared_ptr<connection> p(&c,end_connection);
	//使用连接
	//当f退出时(即使是由于异常而退出),connection会被正确关闭
}

p 被销毁时,它不会对自己保存的指针执行delete,而是调用end_connection。接下来,end_connection 会调用disconnect,从而确保连接被关闭。如果f正常退出,那么 p 的销毁会作为结束处理的一部分。如果发生了异常,p 同样会被销毁,从而连接被关闭。

正确使用智能指针的一些基本规范:

  • 不使用相同的内置指针值初始化(或reset)多个智能指针。
  • 不delete get()返回的指针。
  • 不使用get()初始化或reset另一个智能指针。
  • 如果使用get()返回的指针,记住当最后一个对应的智能指针销毁后,指针就变为无效了。
  • 如果使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。

unqie_ptr

一个 unique_ptr "拥有"它所指向的对象。与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。
unique_ptr 独有它指向的对象,所有它不支持拷贝和赋值操作。但可以"拷贝"或"赋值"一个将要被销毁的unique_ptr

//从函数返回一个`unique_ptr`
unique_ptr<int> clone(int p){
//正确:从int*创建一个unique_ptr<int>
	return unique_ptr<int>(new int (p));
}

//返回一个局部对象的拷贝
unique ptr<int> clone(int p){
	unique_ptr<int> ret(new int (P))// ...
	return ret;
}

在这里插入图片描述

auto_ptr

标准库的较早版本包含了一个名为auto_ptr的类,它具有unique_ptr的部分特性,但不是全部。特别是,不能在容器中保存auto_ptr,也不能从函数中返回auto_ptr
虽然auto_ptr仍是标准库的一部分,但编写程序时应该使用unique_ptr.

weak_ptr

weak_ptr 是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr 管理的对象。
将一个 weak_ptr 绑定到一个 shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也还是会被释放。

在这里插入图片描述

weak_ptr 定义和初始化

]创建一个weak_ptr时,要用一个shared_ptr来初始化它:

auto p= make shared<int>(42);
weak_ptr<int> wp(p); // wp弱共享p;p的引用计数未改变

//wp和p指向相同的对象。由于是弱共享,创建wp不会改变p的引用计数;wp指向的对象可能被释放掉。

由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock。此函数检查weak_ptr指向的对象是否仍存在。如果存在,lock返回一个指向共享对象的shared_ptr。与任何其他shared_ptr类似,只要此shared_ptr存在,它所指向的底层对象也就会一直存在。

if (shared_ptr<int> np - wp.1ock()){ //如果np 不为空则条件成立
//在if中,np与p共享对象
}

动态数组

newdelete运算符一次分配/释放一个对象,但某些应用需要一次为很多对象分配内存的功能。例如,vector 和 string 都是在连续内存中保存它们的元素,因此,当容器需要重新分配内存时,必须一次性为很多元素分配内存

C++ 语言和标准库提供了两种一次分配一个对象数组的方法:1.表达式语法new ;2. 类allocator,允许将分配和初始化分离

new 和数组

new 分配要求数量的对象并(假定分配成功后)返回指向第一个对象的指针。

// 调用 get_size 确定分配多少个 int
int *pia = new int [get_size()];// pia指向第一个int
//方括号中的大小必须是整型,但不必是常量。

也可以用一个表示数组类型的类型别名来分配一个数组:

// new 表达式不再需要方括号
typedef int arrT [42]; // arrT表示42个int的数组类型
int *p = new arrT; //分配一个42个int的数组;p指向第一个int

//new分配一个int数组,并返回指向第一个int的指针。
//即使代码中没有方括号,编译器执行这个表达式时还是会用 new[]。
//等价于执行:
int *P = new int[42];

分配一个数组会得到一个元素类型的指针
当用 new 分配一个数组时,并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。
即使使用类型别名定义了一个数组类型,new 也不会分配一个数组类型的对象。
由于分配的内存并不是一个数组类型,因此不能对动态数组调用beginend。也不能用范围 for 语句来处理(所谓的)动态数组中的元素。
通常所说的动态数组并不是一种数组类型。

初始化动态分配对象的数组
默认情况下,new分配的对象,不管是单个分配的还是数组中的,都是默认初始化的。可以对数组中的元素进行值初始化,方法是在大小之后跟一对空括号。也可以进行列表初始化。

int *pia = new int [10];// 10 个未初始化的 int
int *pia2 = new int[10]();// 10个值初始化为0的int
string *psa = new string[10];// 10个空string
string *psa2 = new string[10]();// 10个空string

// 10个int分别用列表中对应的初始化器初始化
int *pia3 = new int [10] {0,1,2,3,4,5,6,7,8,9};
// 10个string,前4个用给定的初始化器初始化,剩余的进行值初始化
string *psa3 = new string[10] {"a", "an""the",string(3, 'x')};

与内置数组对象的列表初始化一样,初始化器会用来初始化动态数组中开始部分的元素。如果初始化器数目小于元素数目,剩余元素将进行值初始化。
如果初始化器数目大于元素数目,则new表达式失败,不会分配任何内存。

虽然用空括号对数组中元素进行值初始化,但不能在括号中给出初始化器,这意味着不能用auto分配数组。

动态分配一个空数组是合法的
虽然不能创建一个大小为 0 的静态数组对象,但当 n 等于 0 时,调用 new[n] 是合法的。

char arr[0];//错误:不能定义长度为0的数组
char *cp = new char[0]; //正确:但cp不能解引用

new分配一个大小为0的数组时,new返回一个合法的非空指针。此指针保证与new返回的其他任何指针都不相同。对于零长度的数组来说,此指针就像尾后指针一样,可以像使用尾后迭代器一样使用这个指针。
可以用此指针进行比较操作,可以向此指针加上(或从此指针减去)0,也可以从此指针减去自身从而得到0。但此指针不能解引用——它不指向任何元素

释放动态数组
使用 delete [] 来释放动态数组

delete p;//p必须指向一个动态分配的对象或为空
delete [] pa;// pa必须指向一个动态分配的数组或为空

数组中的元素按逆序销毁即,最后一个元素首先被销毁,然后是倒数第二个,依此类推。
delete[][] 是必需的:如果在 delete 一个指向动态数组的指针时忽略了方括号,行为是未定义的。

智能指针和动态数组
标准库提供了一个可以管理 new 分配的数组的 unique_ptr 版本。

// up指向一个包含10个未初始化int的数组
unique_ptr<int[]> up (new int [10]);
up.release();//自动用 delete[]销毁其指针

类型说明符中的方括号(<int[]>)指出up指向一个int 数组而不是一个 int。由于up指向一个数组,当up销毁它管理的指针时,会自动使用delete[]

指向数组的 unique_ptr:

//指向数组的unique ptr不支持成员访问运算符(点和箭头运算符)。
//其他unique ptr操作不变。

unique_ptr<T[]> u      // u 可以指向一个动态分配的数组,数组元素类型为 T
unique_ptr<T[]> u(p)   // u 指向内置指针 p 所指向的动态分配的数组,p 必须能转换为类型 T*
u[i]                   // 返回 u 拥有的数组中位置 i 处的对象

shared_ptr 不直接支持管理动态数组。如果希望使用 shared_ptr 管理一个动态数组 ,必须提供自己定义的删除器。

//为了使用shared_ptr,必须提供一个删除器
shared ptr<int> sp(new int[10],[](int*p){ delete[] p;});
sp.reset();//使用我们提供的 lambda释放数组,它使用delete[]

如果未提供删除器,shared_ptr 默认使用 delete 来销毁动态数组,是未定义的。
shared_ptr 访问数组中的元素需要使用 get()。

allocator 类

灵活性上的局限,new 将内存分配和对象构造组合在了一起。类似的,delete将对象析构和内存释放组合在了一起
分配单个对象时,通常希望将内存分配和对象初始化组合在一起。因为在这种情况下,几乎肯定知道对象应有什么值。

当分配一大块内存时,通常计划在这块内存上按需构造对象。这种情况下,希望将内存分配和对象构造分离。意味着可以分配大块内存,但只在真正需要时才真正执行对象创建操作(同时付出一定开销)。

allocator

标准库allocator类定义在头文件memory中,它帮助我们将内存分配和对象构造分离开来。它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的

类似vectorallocator是一个模板。为了定义一个allocator对象,必须指明这个allocator可以分配的对象类型。
当一个allocator对象分配内存时,它会根据给定的对象类型来确定恰当的内存大小和对齐位置:

allocator<string> alloc;//可以分配string 的allocator对象
auto const p = alloc.allocate(n);//分配n个未初始化的string
// 这个 allocate 调用为 n 个 string 分配了内存

标准库 allocator 类及其算法
在这里插入图片描述

allocator 分配未构造的内存

allocator分配的内存是未构造的。按需要在此内存中构造对象。
在新标准库中,construct成员函数接受一个指针和零个或多个额外参数,在给定位置构造一个元素。额外参数用来初始化构造的对象。类似make_shared的参数,这些额外参数必须是与构造的对象的类型相匹配的合法的初始化器:

auto q = p; //q指向最后构造的元素之后的位置
alloc.construct(q++);//*q为空字符串
alloc.construct(q++,10,'c');//*q为ccccccccc
calloc.construct(q++, "hi");//*q为hi!

为了使用allocate返回的内存,必须用 construct构造对。使用未构造的内存,其行为是未定义的

用完对象后,必须对每个构造的元素调用destroy来销毁它们:

while (q != p)
	alloc.destroy(--q);//释放真正构造的string

只能对真正构造了的元素进行destroy操作。
元素被销毁后可以重新在这块内存构造对象也可以释放掉内存。
constructdestory 一次都只能构造或销毁一个对象,要想完成对所有元素的操作,需要通过指针来遍历对每个元素进行操作。

拷贝和填充未初始化内存的算法

定义在头文件 memory 中,两个伴随算法:uninitialized_copyuninitialized_fill,可以在未初始化内存中创建对象。
在这里插入图片描述将分配一块比vector中元素所占用空间大一倍的动态内存,然后将原vector中的元素拷贝到前一半空间,对后一半空间用一个给定值进行填充:

//分配比vi中元素所占用空间大一倍的动态内存
auto p = alloc.allocate(vi.size()*2);
//通过拷贝vi中的元素来构造从p开始的元素
auto q = uninitialized_copy(vi.begin(), vi.end(),p);
//将剩余元素初始化为42
uninitialized_fill_n(g, vi.size(),42);

重要术语

allocator :标准库类,用来分配未构造的内存。
空悬指针:一个指针,指向曾经保存一个对象但现在已释放的内存。众所周知,空悬指针引起的程序错误非常难以调试。
释放器:传递给智能指针的函数,用来代替delete释放指针绑定的对象。
动态分配的:在自由空间中分配的对象。在自由空间中分配的对象直到被显式释放或程序结束才会销毁。
自由空间:程序可用的内存池,保存动态分配的对象。
:自由空间的同义词。
定位new:一种new表达式形式,接受一些额外的参数,在 new关键字后面的括号中给出。例如,new(nothrow) int告诉new不要抛出异常。
引用计数:一个计数器,记录有多少用户共享一个对象。智能指针用它来判断什么时候释放所指向的对象是安全的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

飞大圣

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

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

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

打赏作者

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

抵扣说明:

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

余额充值