C++动态内存分配(new和delete)

1.静态内存,栈内存,堆内存

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

栈内存用来保存定义在函数内的非static对象。

分配在静态或栈内存中的对象由编译器自动创建和销毁。

对于栈对象,仅在其定义的程序块运行时才存在;static对象在使用之前分配,在程序结束时销毁。

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

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

  1. 栈又叫堆栈--非静态局部变量/函数参数/返回值等等,栈是向下增长的。
  2. 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共 享内存,做进程间通信。
  3. 堆用于程序运行时动态内存分配,堆是可以上增长的。
  4. 数据段--存储全局数据和静态数据。
  5.  代码段--可执行的代码/只读常量。 

2.new和delete

c++语言定义了两个运算符来分配和释放动态内存。运算符new分配内存,delete释放new分配的内存。

相对于智能指针,使用这两个运算符管理内存非常容易出错,随着我们逐步详细介绍这两个运算符,这一点会更为清楚。

而且,自己直接管理内存的类与使用智能指针的类不同,它们不能依赖类对象拷贝、赋值和销毁操作的任何默认定义。

因此,使用智能指针的程序更容易编写和调试。

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

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

int*pi = new int; // pi指向一个动态分配的、未初始化的无名对象

此new表达式在自由空间构造一个int型对象,并返回指向该对象的指针。

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

string*ps = new string;// 初始化为空 string
int*pi = new int; // pi指向一个未初始化的int

我们可以使用直接初始化方式来初始化一个动态分配的对象。

我们可以使用传统的构造方式(使用圆括号),在新标准下,也可以使用列表初始化(使用花括号):

int *pi = new int(1024); // pi指向的对象的值为1024
string *ps = new string(10,'9'); //*ps 为"9999999999"
// vector有10个元素,值依次从0到9
vector<int> *pv = new vector<int>{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

也可以对动态分配的对象进行值初始化,只需在类型名之后跟一对空括号即可;


string *psl  new string; //默认初始化为空string
string *ps = new string();// 值初始化为空 string
int *pil =new int;// 默认初始化;*pil的值未定义
int *pi2 = new int(); // 值初始化为 0;*pi2为0

对于定义了自己的构造函数的类类型来说,要求值初始化是没有意义的;不管采用什么形式,对象都会过就认构造函数来初始化.

但对于内置类型,两种形式的差别就很大了;值初始化的内置类型对象有着良好定义的值而默认初始化的对象的值则是未定义的。

类似的,对于类中那些依赖于编译器合成的默认构造函数的内置类型成员,如果它们未在类内被初始化,那么它们的值也是未定义的。

出于与变量初始化相同的原因,对动态分配的对象进行初始化通常是个好主意。

如果我们提供了一个括号包围的初始化器,就可以使用auto从此初始化器来推断我们想要分配的对象的类型。

但是,由于编译器要用初始化器的类型来推断要分配的类型,只有当括号中仅有单一初始化器时才可以使用auto:

auto pi = new auto(obj); // p指向一个与obj类型相同的对象
//该对象用obi进行初始化
auto p2 = new auto{a, b, c}; //错误:括号中只能有单个初始化器

p1的类型是一个指针,指向从obj自动推断出的类型。

若obj是一个int,那么p1就是int*;若obj是一个string,那么pl是一个string*;依此类推。新分配的对象用obj的值进行初始化。

2.2.动态分配的 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的指针。

2.3内存耗尽

虽然现代计算机通常都配备大容量内存,但是自由空间被耗尽的情况还是有可能发生。

一旦一个程序用光了它所有可用的内存,new表达式就会失败。

默认情况下,如果new不能分配所要求的内存空间,它会抛出一个类型为bad_alloc的异常。

我们可以改变使用new的方式来阻止它抛出异常:

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

我们称这种形式的new为定位new。定位new表达式允许我们向new传递额外的参数。

在此例中,我们传递给它一个由标准库定义的名为nothrow的对象。如果将 nothrow 传递给 new,我们的意图是告诉它不能抛出异常,如果这种形式的new 不能分配所需内存,它会返回一个空指t bad_alloc和nothrow都定义在头文件 new中.

2.4.释放动态内存

为了防止内存耗尽,在动态内存使用完毕后,必须将其归还给系统,我们通过delete来将动态内存归还给系统,delete表达式接受一个指针,指表达式 向我们想要释放的对象:

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

与new类型类似,delete表达式也执行两个动作:销毁给定的指针指向的对象;释放对应的内存
指针值和delete

我们传递给delete的指针必须指向动态分配的内存,或者是一个空指针。

释放一块并非new分配的内存,或者将相同的指针值释放多次,其行为是未定义的:

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

对于delete i的请求,编译器会生成一个错误信息,因为它知道i不是一个指针。

执行delete pil和pd2所产生的错误则更具潜在危害:通常情况下,编译器不能分辨一个指针指向的是静态还是动态分配的对象。类似的,编译器也不能分辨一个指针所指向的内存是否已经被释放了。

对于这些delete表达式,大多数编译器会编译通过,尽管它们是错误的。

虽然一个const对象的值不能被改变,但它本身是可以被销毁的。如同任何其他动态对象一样,想要释放一个const动态对象,只要delete指向它的指针即可:

const int *pci = new const int(1024);
delete pci; // 正确:释放一个const对象

2.5.动态对象的生存期直到被释放时为止

由shared_ptr管理的内存在最后一个shared_ptr销毁时会被自动释放。

但对于通过内置指针类型来管理的内存,就不是这样了。对于一个由内置指针管理的动态对象,直到被显式释放之前它都是存在的。

返回指向动态内存的指针(而不是智能指针)的函数给其调用者增加了一个额外负担——调用者必须记得释放内存:

// factory返回一个指针,指向一个动态分配的对象
Foo factory(T arg)
{
//视情况处理arg
return new Foo(arg);//调用者负责释放此内存
}

要是下面这种情况,就会造成内存泄漏

#include<iostream>
using namespace std;
void A()
{
int*a=new int;
}
int main()
{
A();
}

与类类型不同,内置类型的对象被销毁时什么也不会发生。特别是,当一个指针离开其作用域时,它所指向的对象什么也不会发生。如果这个指针指问的是动态内存,那么内存将不会被自动释放。

由内置指针(而不是智能指针)管理的动态内存在被显式释放前一直都会存在

在本例中,a是指向动态内存分配的唯一指针。一旦A()执行完毕,我们就再也无法释放这片内存了,为此,我们提供了两种处理方法

include<iostream>
using namespace std;
int* A()
{
int*a=new int;
return a;
}
int main()
{
int* b=A();//由b来负责释放
}
include<iostream>
using namespace std;
void A()
{
int*a=new int;
delete a;
}
int main()
{
A();
}

小心:动态内存的管理非常容易出错

使用new和delete管理动态内存存在三个常见问题:

  1. 忘记delete内存。忘记释放动态内存会导致人们常说的“内存泄漏”问题,因为这种内存永远不可能被归还给自由空间了。查找内存泄露错误是非常困难的,因为通常应用程序运行很长时间后,真正耗尽内存时,才能检测到这种错误。
  2. 使用已经释放掉的对象。通过在释放内存后将指针置为空,有时可以检测出这种错误。
  3. 同一块内存释放两次。当有两个指针指向相同的动态分配对象时,可能发生这种错误。如果对其中一个指针进行了delete操作,对象的内存就被归还给自由空间了。如果我们随后又delete第二个指针,自由空间就可能被破坏。

相对于查找和修正这些错误来说,制造出这些错误要简单得多。

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

2.6.delete 之后重置指针值……

当我们delete一个指针后,指针值就变为无效了。

虽然指针已经无效,但在很多机器上指针仍然保存着(已经释放了的)动态内存的地址。在delete之后,指针就变成了人们所说的空悬指针,即,指向一块曾经保存数据对象但现在已经无效的内存的指针。

未初始化指针的所有缺点空悬指针也都有。

有一种方法可以避免空悬指针的问题:在指针即将要离开其作用域之前释放掉它所关联的内存。这样,在指针关联的内存被释放掉之后,就没有机会继续使用指针了。如果我们需要保留指针,可以在delete之后将nullptr赋予指针,这样就清楚地指出指针不指向任何对象。

2.7……这只是提供了有限的保护

动态内存的一个基本问题是可能有多个指针指向相同的内存。

在delete内存之后重置指针的方法只对这个指针有效,对其他任何仍指向(已释放的)内存的指针是没有作用的。

例如:

int *p(new int(42));// p指向动态内存
auto q=p; //p和q指向相同的内存
//p和q均变为无效
delete p;
P =nullptr; //指出p不再绑定到任何对象

本例中p和q指向相同的动态分配的对象。我们delete此内存,然后将p置为nullptr,指出它不再指向任何对象。

但是,重置p对q没有任何作用,在我们释放p所指向的(同时也是q所指向的!)内存时,q也变为无效了。在实际系统中,查找指向相同内存的所有指针是异常困难的。

3.new和数组

为了让 new 分配一个对象数组,我们要在类型名之后跟一对方括号,在其中指明要分配的对象的数目。

在下例中,new分配要求数量的对象并(假定分配成功后)返回指向第一个对象的指针:

//调用get_size确定分配多少个int
int *pia = new int[get _size()];// pia指向第一个int

方括号中的大小必须是整型,但不必是常量。

也可以用一个表示数组类型的类型别名来分配一个数组,这样,new表达式中就不需要方括号了:

typedef int arrT[42]; // arrT表示42个int的数组类型
int *p= new arrl; // 分配一个42个int的数组;p指向第一个int

在本例中,new分配一个int数组,并返回指向第一个int的指针。即使这段代码中没有方括号,编译器执行这个表达式时还是会用new[]。

即,编译器执行如下形式:

int *p = new int[42];

分配一个数组会得到一个元素类型的指针

虽然我们通常称new T[ ] 里分配的内存为“动态数组”,但这种叫法某种程度上有些误导。

当用new 分配一个数组时,我们并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。即使我们使用类型别名定义了一个数组类型,new也不会分配一个数组类型的对象。

在上例中,我们正在分配一个数组的事实甚至都是不可见的——连[num]都没有。new返回的是一个元素类型的指针。

由于分配的内存并不是一个数组类型,因此不能对动态数组调用begin或end。这些函数使用数组维度(回忆一下,维度是数组类型的一部分)来返回指向首元素和尾后元素的指针。出于相同的原因,也不能用范围for语句来处理(所谓的)动态数组中的元素。

要记住我们所说的动态数组并不是数组类型,这是很重要的。

3.1.初始化动态分配对象的数组

默认情况下,new分配的对象,不管是单个分配的还是数组中的,都是默认初始化的。

可以对数组中的元素进行值初始化,方法是在大小之后跟一对空括号。

int*pia new int[10]; //10个未初始化的int
int*pia2 = new int[10](); //10个值初始化为0的int
string*psa = new string[10]; //10个空string
string*tpsa2=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表达式失败,不会分配任何内存。

在本例中new会抛出一个类型为bad_array_new_length的异常。类似bad al1oc,此类型定义在头文件new中。

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

3.2.动态分配一个空数组是合法的

可以用任意表达式来确定要分配的对象的数目:

size_t n = get_size(); // get_size返回需要的元素的数目
int*p = new int[n]; //分配数组保存元素
for (int* q = p; q !=p + n; ++q)
/*处理数组*/

这产生了一个有意思的问题:
如果get_size返回0,会发生什么?答案是代码仍能正常

虽然我们不能创建一个大小为0的静态数组对象,但当n等于0时,调用new(n]工作。是合法的:

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

当我们用new分配一个大小为0的数组时,new返回一个合法的非空指针。

此指针保证new 返回的其他任何指针都不相同。

对于零长度的数组来说,此指针就像尾后指针一样,我们可以像使用尾后迭代器一样使用这个指针。可以用此指针进行比较操作,就像上面循环代码中那样。可以向此指针加上(或从此指针减去)0,也可以从此指针减去自身从而得到0。

但此指针不能解引用——毕竟它不指向任何元素。

在我们假想的循环中,若get_size返回0,则n也是0,new会分配0个对象。for循环中的条件会失败(p等于q+n,因为n为0)。因此,循环体不会被执行。

3.3.释放动态数组

为了释放动态数组,我们使用一种特殊形式的delete——在指针前加上一个空方括号对: 

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

第二条语句销毁pa指向的数组中的元素,并释放对应的内存。

数组中的元素按逆序销毁,即,最后一个元素首先被销毁,然后是倒数第二个,依此类推。

当我们释放一个指向数组的指针时,空方括号对是必需的:它指示编译器此指针指向一个对象数组的第一个元素。

如果我们在delete一个指向数组的指针时忽略了方括号或者在delete一个指向单一对象的指针时使用了方括号),其行为是未定义的。

回忆一下,当我们使用一个类型别名来定义一个数组类型时,在 new 表达式中不使用。即使是这样,在释放一个数组指针时也必须使用方括号:

typedef int arrT[42]; //arrT是42个int的数组的类型别名
int *p = new arrT; // 分配一个42个int的数组;p指向第一个元素
delete [] pi ;//方括号是必需的,因为我们当初分配的是一个数组

不管外表如何,p指向一个对象数组的首元素,而不是一个类型为arrT的单一对象。因此,在释放p时我们必须使用[]。

如果我们在delete一个数组指针时忘记了方括号,或者在delete一个单一对象的指针时使用了方括号,编译器很可能不会给出警告。我们的程序可能在执行过程中在没有任何警告的情况下行为异常。

4. operator new与operator delete函数

new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的 全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局 函数来释放空间。

/*operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,
尝试执行空间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。*/
void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
	// try to allocate size bytes
	void* p;
	while ((p = malloc(size)) == 0)
		if (_callnewh(size) == 0)
		{
			// report no memory
			// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
			static const std::bad_alloc nomem;
			_RAISE(nomem);
		}
	return (p);
}

/*
operator delete: 该函数最终是通过free来释放空间的
*/
void operator delete(void* pUserData)
{
	_CrtMemBlockHeader* pHead;
	RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));

	if (pUserData == NULL)
		return;
	_mlock(_HEAP_LOCK);  /* block other threads */
	__TRY
		/* get a pointer to memory block header */
		pHead = pHdr(pUserData);
	/* verify block type */
	_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
	_free_dbg(pUserData, pHead->nBlockUse);
	__FINALLY
		_munlock(_HEAP_LOCK);  /* release other threads */
	__END_TRY_FINALLY
		return;
}

通过上述两个全局函数的实现知道,operator new 实际也是通过malloc来申请空间,如果malloc申请空间 成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异 常。operator delete 最终是通过free来释放空间的。

5. new和delete的实现原理

5.1 内置类型

如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete申请和 释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常, malloc会返回NULL。

5.2 自定义类型

new的原理

  • 1. 调用operator new函数申请空间
  • 2. 在申请的空间上执行构造函数,完成对象的构造

delete的原理

  • 1. 在空间上执行析构函数,完成对象中资源的清理工作
  • 2. 调用operator delete函数释放对象的空间

new T[N]的原理

  • 1. 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申 请
  • 2. 在申请的空间上执行N次构造函数

delete[]的原理

  • 1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
  • 2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空

6. 常见面试题

6.1 malloc/free和new/delete的区别

malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:

  • 1. malloc和free是函数,new和delete是操作符
  • 2. malloc申请的空间不会初始化,new可以初始化
  • 3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可, 如果是多个 对象,[]中指定对象个数即可
  • 4. malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
  • 5. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
  • 6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间 后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理
#include<iostream>
using namespace std;

class A
{
public:
	A(int a = 0)
		: _a(a)
	{
		cout << "A():" << this << endl;
	}
	~A()
	{
		cout << "~A():" << this << endl;
	}
	
private:
	int _a;
};

int main()
{
	// new/delete 和 malloc/free最大区别是 new/delete对于【自定义类型】除了开空间还会调用构造函数和析构函数
		
	A* p1 = (A*)malloc(sizeof(A));
	A* p2 = new A(1);
	
	free(p1);
	delete p2;

	// 内置类型是几乎是一样的
	int* p3 = (int*)malloc(sizeof(int)); // C
	int* p4 = new int;
	
	free(p3);
	delete p4;
	
	A* p5 = (A*)malloc(sizeof(A) * 10);
	A* p6 = new A[10];
	
	free(p5);
	delete[] p6;
	
	return 0;
}

注意:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会。 

 

6.2 内存泄漏

6.2.1 什么是内存泄漏,内存泄漏的危害

什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。

内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而 造成了内存的浪费。

内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会 导致响应越来越慢,最终卡死。

void MemoryLeaks() 
{ 
// 1.内存申请了忘记释放 
int* p1 = (int*)malloc(sizeof(int)); 

int* p2 = new int; // 2.异常安全问题 

int* p3 = new int[10]; 

delete[] p3; } Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.

6.2.2 内存泄漏分类(了解)

C/C++程序中一般我们关心两种方面的内存泄漏:

堆内存泄漏(Heap leak)

堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存, 用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那 么以后这部分空间将无法再被使用,就会产生Heap Leak。

系统资源泄漏

指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统 资源的浪费,严重可导致系统效能减少,系统执行不稳定。

6.2.3 如何检测内存泄漏(了解)

在vs下,可以使用windows操作系统提供的_CrtDumpMemoryLeaks() 函数进行简单检测,该函数只报出 了大概泄漏了多少个字节,没有其他更准确的位置信息。

int main()
{
	int* p = new int[10];

	// 将该函数放在main函数之后,每次程序退出的时候就会检测是否存在内存泄漏
	_CrtDumpMemoryLeaks();
	return 0;
}


 // 程序退出后,在输出窗口中可以检测到泄漏了多少字节,但是没有具体的位置
Detected memory leaks!
Dumping objects ->
{79} normal block at 0x00EC5FB8, 40 bytes long.
Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.

因此写代码时一定要小心,尤其是动态内存操作时,一定要记着释放。

但有些情况下总是防不胜防,简单的 可以采用上述方式快速定位下。

如果工程比较大,内存泄漏位置比较多,不太好查时一般都是借助第三方内 存泄漏检测工具处理的。

在linux下内存泄漏检测:linux下几款内存泄漏检测工具http://t.csdnimg.cn/mUr1V

在windows下使用第三方工具:VLD工具说明http://t.csdnimg.cn/3E5yU

其他工具:内存泄漏工具比较内存泄露检测工具比较 - 默默淡然 - 博客园 (cnblogs.com)

6.2.4如何避免内存泄漏

  • 1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状 态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保 证。
  • 2. 采用RAII思想或者智能指针来管理资源。
  • 3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
  • 4. 出问题了使用内存泄漏工具检测。

ps:不过很多工具都不够靠谱,或者收费昂贵。

总结一下: 内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工 具

  • 24
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值