[C++]---智能指针介绍简单模拟实现

本文详细探讨了智能指针的概念,包括RAII原则的应用、不同类型的智能指针(auto_ptr、unique_ptr、shared_ptr)的工作原理及其实现方式,特别关注于它们在资源管理、线程安全、循环引用等问题上的解决方案。
摘要由CSDN通过智能技术生成

目录

 

RAII

模拟实现SmartPtr

模拟实现SmartPtr:

智能指针的原理:

常见的三种智能指针

auto_ptr

模拟测试 C++98版本的库提供的auto_ptr

auto_pt模拟实现

unique_ptr

使用库里面的unique_ptr

模拟实现unique_ptr

shared_ptr

使用库里面的shared_ptr

模拟实现shared_ptr

在多线程条件下的shared_ptr

shared_ptr的循环引用问题

shared_ptr小结

仿函数定制删除器

守卫锁


RAII

RAII(Resource Acquisition Is Initialization)根据对象的生命周期控制,初始化构造对象时管理资源,销毁对象在对象析构时释放资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。这么做的好处:

  1. 不需要显式地释放资源。
  2. 采用这种方式,对象所需的资源在其生命期内始终保持有效。
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:
    SmartPtr(T* ptr = nullptr)
    : _ptr(ptr)
    {}
    ~SmartPtr()
    {
       if(_ptr)
       delete _ptr;
    }
private:
    T* _ptr;
};
void MergeSort(int* a, int n)
{
    int* tmp = (int*)malloc(sizeof(int)*n);
    // 讲tmp指针委托给了sp对象,用时老师的话说给tmp指针找了一个可怕的女朋友!天天管着你,直到你go die
   SmartPtr<int> sp(tmp);
   // _MergeSort(a, 0, n - 1, tmp);
   // 这里假设处理了一些其他逻辑
   vector<int> v(1000000000, 10);
   // ...
}
int main()
{
   try {
       int a[5] = { 4, 5, 2, 3, 1 };
       MergeSort(a, 5);
   }
   catch(const exception& e)
   {
       cout<<e.what()<<endl;
   }
   return 0;
}

注意:并没有实现智能指针,只是实现了一个可以管理资源的类。

模拟实现SmartPtr

模拟实现SmartPtr:

智能指针:1.实现RAII思想,2.实现指针的操作(->   和   *)

//智能指针:1.实现RAII思想,2.实现指针的操作
template<class T>
class SmartPtr {
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	~SmartPtr()
	{
		if (_ptr)
		{
			delete _ptr;
			cout << "delete" << endl;
			_ptr = nullptr;
		}
	}

	//模拟实现指针功能:  *  和  ->

	T& operator*()
	{
		return *_ptr;
	}
		
	T* operator->()
	{
		return _ptr;
	}

private:
	T* _ptr;
};

int main()
{
	
	//常规的指针
	int* pi = new int(2);
	cout << *pi << endl;
	*pi = 3;
	cout << *pi << endl;
	//普通指针malloc后需要手动释放
	delete pi;
	//智能指针
	cout << "SmartPtr:" << endl;
	SmartPtr<int> sp(new int(10));
	cout << *sp << endl;
	*sp = 20;
	cout << *sp << endl;
	//智能指针编译器会在指针对象使用完成后,进行自动调用析构函数进行释放。
    return 0;
}

智能指针的原理:

  • 1. RAII特性
  • 2. 重载operator*和opertaor->,具有像指针一样的行为。

常见的三种智能指针

auto_ptr

模拟测试 C++98版本的库提供的auto_ptr

在使用库里面提供的智能指针,在进行多个智能指针对象管理同一份资源,仅被释放一次。

class A
{
public:
	int _a = 1;
	int _b = 2;
	int _c = 3;
	~A()
	{
		cout << "~A()" << endl;
	}
};


int main()
{

	//多个智能指针对象管理同一份资源,仅被释放一次。
	//一般禁止使用
	auto_ptr<A>sp3(new A());//库里的智能指针会优化,对多个对象管理一份资源只释放一次
	sp3->_a = 20;
	cout << sp3->_a << endl;

	auto_ptr<A>copy(sp3);
	copy->_a = 100;
	cout << copy->_a << endl;
	auto_ptr<A>copy2(copy);
	copy2->_b = 1;
	cout << copy2->_b << endl;
    return 0;
}

 

假如我们使用自己上面写的SmartPtr进行多个对象管理同一份资源时,在使用结束后就会出错,出现二次释放问题

#include<iostream>
// C++库中的智能指针都定义在memory这个头文件中
#include<memory>

using namespace  std;
template<class T>
class SmartPtr {
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	~SmartPtr()
	{
		if (_ptr)
		{
			delete _ptr;
			cout << "delete" << endl;
			_ptr = nullptr;
		}
	}

	//模拟实现指针功能:  *  和  ->

	T& operator*()
	{
		return *_ptr;
	}
		
	T* operator->()
	{
		return _ptr;
	}

private:
	T* _ptr;
};

class A
{
public:
	int _a = 1;
	int _b = 2;
	int _c = 3;
	~A()
	{
		cout << "~A()" << endl;
	}
};


int main()
{
	//使用自己定义的智能指针多次释放
	SmartPtr<A>sp3(new A());
	sp3->_a = 20;
	cout << sp3->_a << endl;
	SmartPtr<A> copy (sp3);//假如多个我们自己实现智能指针同时管理一份资源,就会造成资源二次释放。
	copy->_a = 100;
	SmartPtr<A> copy2 (copy);
	copy2->_b = 1;
	

	//system("pause");
	return 0;
}

 

//测试 系统中auto_ptr智能指针,在对同一块空间被多个对象使用会不会造成程序奔溃问题
#include<iostream>
// C++库中的智能指针都定义在memory这个头文件中
#include<memory>
using namespace  std;

class A
{
public:
	int _a = 1;
	int _b = 2;
	int _c = 3;
	~A()
	{
		cout << "~A()" << endl;
	}
};


int main()
{	
    auto_ptr<A>sp3(new A());//库里的智能指针
	sp3->_a = 20;
	cout << sp3->_a << endl;

	auto_ptr<A>copy(sp3);//这里发生一次拷贝,顺便将资源的管理权交给新的对象,它不会再拥有管理资源的能力
	
	//sp3已经完成对资源管理权的转移,因此无法访问到_a成员,sp3指针被悬空,因此在这里程序会崩溃,空指针无法解引用。
	//指针悬空
	//sp3->_a = 1;

	copy->_a = 100;
	auto_ptr<A>copy2(copy);
	//管理权转移,指针悬空,因此不能解引用访问。
	//copy->_b = 1;

	copy2->_b = 1;

	return 0;
}

假如在上面程序中我们去掉//sp3->_a = 1;  和  //copy->_b = 1;注释,运行程序结果如下:

崩溃原因:sp3已经完成对资源管理权的转移,因此它便无法访问到_a成员,sp3指针因此会被悬空,所以在这里程序会崩溃,空指针无法解引用。下面的copy原因同理类似。

auto_pt模拟实现

auto_ptr的实现原理:实现管理权转移的思想

拷贝时发生管理权转移,会导致当前被拷贝的智能指针悬空,最终会导致智能指针访问异常,程序崩溃,因此auto_ptr 禁止使用。

//模拟实现auto_ptr智能指针: 解决之前的auto_ptr存在二次释放问题
template<class T>
class AutoPtr {
public:
	AutoPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	~AutoPtr()
	{
		if (_ptr)
		{
			delete _ptr;
			cout << "delete" << endl;
			_ptr = nullptr;
		}
	}

	// 一旦发生拷贝,就将ap中资源转移到当前对象中,然后另ap与其所管理资源断开联系,
	// 这样就解决了一块空间被多个对象使用而造成程序奔溃问题
	//浅拷贝,指针只是管理资源,不拥有资源
	//管理权转移 拷贝构造
	AutoPtr(AutoPtr<T>& ap)
		:_ptr(ap._ptr)
	{
		ap._ptr = nullptr;
	}

	//赋值运算符重载
	AutoPtr<T>& operator=(AutoPtr<T>& ap)
	{
		// 检测是否为自己给自己赋值
		if (this != ap)
		{
			// 释放当前对象中资源
			if (_ptr)
				delete _ptr;
			//管理权转移
			// 转移ap中资源到当前对象中
			_ptr = ap._ptr;
			ap._ptr = nullptr;	
		}
		return *this;
	}

	//实现指针功能:解引用 ->

	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}

private:
	T* _ptr;
};

class A
{
public:
	int _a = 1;
	int _b = 2;
	int _c = 3;
	~A()
	{
		cout << "~A()" << endl;
	}
};

int main()
{
	AutoPtr<A> sp4(new A());
	sp4->_a = 10;

	AutoPtr<A> copy3(sp4);
	copy3->_a = 100;

	AutoPtr<A> copy4(copy3);
	copy4->_b = 1;
	//只会调用一次析构 一次 delete
	//system("pause");
	return 0;
}

unique_ptr

使用库里面的unique_ptr

#include<iostream>
// C++库中的智能指针都定义在memory这个头文件中
#include<memory>
using namespace  std;

int main()
{
	unique_ptr<A> up(new A());
	up->_a = 10;
	
	unique_ptr<A> up2(new A());
	//使用库里面unique_ptr:它禁止相互间的赋值行为,防赋值
	up2 = up;
	//使用库里面unique_ptr:它禁止相互间的拷贝行为,防拷贝
	unique_ptr<A> copy(up);
    return 0;
}

我们可以看到库里面的unique_ptr是防拷贝,防赋值。

模拟实现unique_ptr

只需要在上面实现SmartPtr 基础上禁止掉拷贝构造和赋值运算符重载函数。而在禁止掉拷贝构造和赋值运算符重载函数:有两种方法 {1}使用C++11语法delete  {2}将两个函数定义为私有,只声明不实现

#include<iostream>
using namespace  std;

//模拟实现Unique_Ptr:只需要禁止掉拷贝构造和赋值运算符重载函数
template<class T>
class UniquePtr {
public:
	UniquePtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	~UniquePtr()
	{
		if (_ptr)
		{
			delete _ptr;
			cout << "delete" << endl;
			_ptr = nullptr;
		}
	}

	//禁止掉拷贝构造和赋值运算符重载函数:有两种方法 {1}使用C++11语法delete  {2}将两个函数定义为私有,只声明不实现

	//禁止拷贝构造 
	//C++11语法 声明成delete函数
	UniquePtr(UniquePtr<T>& ap) = delete;
	//C++11语法
	//禁止赋值运算符重载 声明成delete函数
	UniquePtr<T>& operator=(UniquePtr<T>& ap) = delete;

	//实现指针功能:解引用 还有  ->

	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}

private:

	//定义成私有,只声明不实现
	//防拷贝构造 
	UniquePtr(UniquePtr<T>& ap);
	//防赋值运算符重载
	UniquePtr<T>& operator=(UniquePtr<T>& ap);

	T* _ptr;
};


class A
{
public:
	int _a = 1;
	int _b = 2;
	int _c = 3;
	~A()
	{
		cout << "~A()" << endl;
	}
};

int main()
{
	
	//使用自己实现的UniquePtr测试拷贝和赋值操作
	UniquePtr<A> up3(new A());
	UniquePtr<A> copy2(up3);
	UniquePtr<A> up4(new A());
	up4 = up3;

	//system("pause");
    return 0;
}

unique_ptr特点:防拷贝,防赋值,安全的,可以使用。缺陷:不能进行拷贝赋值。

shared_ptr

使用库里面的shared_ptr

#include<iostream>
// C++库中的智能指针都定义在memory这个头文件中
#include<memory>
using namespace  std;

class A
{
public:
	int _a = 1;
	int _b = 2;
	int _c = 3;
	~A()
	{
		cout << "~A()" << endl;
	}
};
int main()
{
	shared_ptr<A> sp(new A());
	//查看引用计数,看当前资源被几个指针管理
	cout << sp.use_count() << endl;//1
	//可以拷贝
	shared_ptr<A> cope(sp);
	cout << cope.use_count() << endl;//2
	//可以赋值
	shared_ptr<A> sp2(new A());
	cout << sp2.use_count() << endl;//1
	sp2 = sp;
	cout << sp2.use_count() << endl;//3
	//程序结束释放两次delte
	system("pause");
	return 0;
}

我们发现系统提供的shared_ptr是完全安全的。

模拟实现shared_ptr

我们在shared_ptr模拟实现:在释放资源时采用引用计数方法,因此在++引用计数或者--引用计数时必须保证原子性操作,因此我们使用了  mutex* _mtx 进而保证原子性操作

#include<mutex>
#include<iostream>
using namespace std;
//shared_ptr模拟实现:释放时采用引用计数方法
template<class T>
class SharedPtr{
public:
	SharedPtr(T* ptr)
		:_ptr(ptr)
		, _pcount(new int(1))
		, _mtx(new mutex())
	{}

	SharedPtr(SharedPtr<T>& sp)
		:_ptr(sp._ptr)
		, _pcount(sp._pcount)
		, _mtx(sp._mtx)
	{
		_mtx->lock();
		++(*_pcount);
		_mtx->unlock();
	}
	
	SharedPtr<T>& operator=(SharedPtr<T>& sp)
	{
		//if (this != sp)
		if (_ptr != sp._ptr)
		{
			_mtx->lock();
			--(*_pcount);
			_mtx->unlock();
			if (*_pcount == 0)
			{
				delete _pcount;
				delete _ptr;
			}
			_ptr = sp._ptr;
			_pcount = sp._pcount;

			_mtx->lock();
			++(*_pcount);
			_mtx->unlock();
		}
		return *this;
	}

	~SharedPtr()
	{
		_mtx->lock();
		--(*_pcount);
		_mtx->unlock();
		if (*_pcount == 0)
		{
			if (_ptr)
			{
				delete _ptr;
				delete _pcount;
				cout << "delete _ptr" << endl;
				_ptr = nullptr;
				_pcount = nullptr;
			}
		}
	}

	//实现指针功能:解引用 还有  ->
	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}


	int useCount()
	{
		return *_pcount;
	}

private:
	T* _ptr;

	int *_pcount;
	//不能给成静态的  不能使用一个引用计数表示所有资源被引用的数量,应该每一个资源都拥有自己的引用计数
	//static int _count;

	mutex* _mtx;//保证原子性操作
};

//template<class T>
//int SharedPtr<T>::_count = 0;

int main()
{
	SharedPtr<int> sp(new int(1));
	SharedPtr<int> sp2(new int(2));
	cout << sp.useCount() << endl;//1
	cout << sp2.useCount() << endl;//1
	SharedPtr<int> copy(sp);
	cout << sp.useCount() << endl;//2
	cout << copy.useCount() << endl;//2

	sp2 = sp;
	cout << sp.useCount() << endl;//3
	cout << sp2.useCount() << endl;//3
	copy = sp;
	cout << copy.useCount() << endl;//3
	cout << sp.useCount() << endl;//3
	
	system("pause");
	return 0;
}

在多线程条件下的shared_ptr

#prama once //防止头文件多次引用
#include<mutex>
#include<thread>
#include<iostream>
using namespace std;
// C++库中的智能指针都定义在memory这个头文件中
#include<memory>

template<class T>
class SharedPtr{
public:
	SharedPtr(T* ptr)
		:_ptr(ptr)
		, _pcount(new int(1))
		, _mtx(new mutex())
	{}

	SharedPtr(SharedPtr<T>& sp)
		:_ptr(sp._ptr)
		, _pcount(sp._pcount)
		, _mtx(sp._mtx)
	{
		/*_mtx->lock();
		++(*_pcount);
		_mtx->unlock();*/
		addRef();
	}

	SharedPtr<T>& operator=(SharedPtr<T>& sp)
	{
		//if (this != sp)
		if (_ptr != sp._ptr)
		{
			/*_mtx->lock();
			--(*_pcount);
			_mtx->unlock();*/
			if (subRef() == 0)
			{
				delete _pcount;
				delete _ptr;
			}
			_ptr = sp._ptr;
			_pcount = sp._pcount;

			/*_mtx->lock();
			++(*_pcount);
			_mtx->unlock();*/
			addRef();
		}
		return *this;
	}

	~SharedPtr()
	{
		
		/*_mtx->lock();
		--(*_pcount);
		_mtx->unlock();*/
		if (subRef() == 0)
		{
			if (_ptr)
			{
				delete _ptr;
				delete _pcount;
				cout << "delete _ptr" << endl;
				_ptr = nullptr;
				_pcount = nullptr;
			}
		}
	}

	//实现指针功能:解引用 还有  ->
	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}


	int useCount()
	{
		return *_pcount;
	}

	int addRef()
	{
		_mtx->lock();
		++(*_pcount);
		_mtx->unlock();
		return *_pcount;
	}

	int subRef()
	{
		_mtx->lock();
		--(*_pcount);
		_mtx->unlock();
		return *_pcount;
	}

private:
	T* _ptr;

	int *_pcount;
	//不能给成静态的  不能使用一个引用计数表示所有资源被引用的数量,应该每一个资源都拥有自己的引用计数
	//static int _count;

	mutex* _mtx;//保证原子性操作
};

#include<thread>
mutex mtx;
//使用自己定义的 SharedPtr
void fun(SharedPtr<int>& sp, int n)
{
	for (int i = 1; i <= n; ++i)
	{
		mtx.lock();
		++(*sp);
		mtx.unlock();
		SharedPtr<int> copy(sp);
	}
}
void test1()
{
	int n = 10000;
	SharedPtr<int> sp(new int(0));
	thread t1(fun, sp, n);
	thread t2(fun, sp, n);
	t1.join();
	t2.join();
	cout << *sp << endl;//20000
	cout << sp.useCount() << endl;//1
}
//使用库里面的 shared_ptr
void fun2(shared_ptr<int>sp, int n)
{
	for (int i = 0; i < n; ++i)
	{
		mtx.lock();
		++(*sp);
		mtx.unlock();
		shared_ptr<int> copy(sp);
	}
}


void test2()
{
	int n = 10000;
	shared_ptr<int> sp(new int(0));
	thread t1(fun2, sp, n);
	thread t2(fun2, sp, n);
	t1.join();
	t2.join();
	cout << *sp << endl;//20000
	cout << sp.use_count() << endl;//1
}

int main()
{
	test1();
	//test2();
	system("pause");
	return 0;
}

在这里test1()是我们自己实现的shared_ptr ,而test2()我们使用的库里的shared_ptr 二者进行对比:

shared_ptr的循环引用问题

//循环引用 造成无法释放结点
template <class T>
class ListNode
{
public:
	shared_ptr<ListNode<T>> _prev;
	shared_ptr<ListNode<T>> _next;
	~ListNode()
	{ 
		cout << "~ListNode()" << endl; 
	}
};

void test3()
{
	shared_ptr<ListNode<int>> sp(new ListNode<int>());
	shared_ptr<ListNode<int>> sp2(new ListNode<int>());
	cout << sp.use_count() << endl;//1
	cout << sp2.use_count() << endl;//1
	sp->_next = sp2;
	sp2->_prev = sp;
	cout << sp.use_count() << endl;//2
	cout << sp2.use_count() << endl;//2

	//无法释放空间,没调用析构函数
}

int main()
{
	test3();
	system("pause");
	return 0;
}

 

分析循环引用问题

  • 1. node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
  • 2. node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
  • 3. node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
  • 4. 也就是说_next析构了,node2就释放了。
  • 5. 也就是说_prev析构了,node1就释放了。
  • 6. 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放。

如何解决?

//解决循环引用方法
template <class T>
class ListNode
{
public:
	//weak_ptr:不会管理资源,也不会修改引用计数
	weak_ptr<ListNode<T>> _prev;
	weak_ptr<ListNode<T>> _next;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

void test3()
{
	shared_ptr<ListNode<int>> sp(new ListNode<int>());
	shared_ptr<ListNode<int>> sp2(new ListNode<int>());
	cout << sp.use_count() << endl;//1
	cout << sp2.use_count() << endl;//1
	sp->_next = sp2;
	sp2->_prev = sp;
	cout << sp.use_count() << endl;//1
	cout << sp2.use_count() << endl;//1
	
	//weak_ptr: 不可以单独使用,需要借助shared_ptr进行初始化。
	//weak_ptr<int> wp(new int(2));
}

int main()
{
	test3();
	system("pause");
	return 0;
}

原理:sp->_next = sp2;和sp2->_prev = sp1;时weak_ptr的_next和_prev不会增加sp1和sp2的引用计数。

注意:weak_ptr: 不可以单独使用,需要借助shared_ptr进行初始化

shared_ptr小结

通过引用计数完成拷贝,引用计数类型为指针类型,拷贝,属于shared_ptr的成员变量,不同的资源会有一个唯一的记录资源被引用的个数的空间,因此它是是安全的,可以使用的。

智能指针如果指向同一个资源,所访问的引用计数即为同一个空间的内容。

在拷贝时进行引用计数的增加,析构时进行引用计数的减减。释放资源:引用计数为0时释放资源。

线程安全:引用计数要保证线程安全,给每一份资源设置一把锁,修改对应的资源的引用计数时,先加锁。保证计数的修改为一个串行的操作,保证线程安全。

仿函数定制删除器

如果不是new出来的对象如何通过智能指针管理呢?其实shared_ptr设计了一个删除器来解决这个问题。

//仿函数的删除器,定制删除器
class B
{
public:
	~B()
	{
		cout << "~B()" << endl;
	}
};

template <class T>
class DeleteArray
{
public:
	//仿函数 : 重载括号运算符函数
	void operator() (T* ptr)
	{
		cout << "delete[]" << ptr << endl;
		delete[] ptr;
	}
};

template <class T>
class FreeDelete
{
public:
	//仿函数 : 重载括号运算符函数
	void operator() (T* ptr)
	{
		cout << "free:" << ptr << endl;
		free(ptr);
	}
};


void test4()
{
	shared_ptr<int> sp(new int[10], DeleteArray<int>());
	shared_ptr<int> sp2((int*)malloc(100), FreeDelete<int>());

	shared_ptr<B> sp3(new B[10], DeleteArray<B>());
	shared_ptr<B> sp4((B*)malloc(100),FreeDelete<B>());
}

int main()
{
	test4();
	system("pause");
	return 0;
}

守卫锁

守卫锁:其实也是利用RAII思想,利用对象声明周期管理锁的生命周期:构造加锁,析构解锁。

//守卫锁 也是利用RAII思想,利用对象声明周期管理锁的生命周期:构造加锁,析构解锁。
template<class Mtx>
class LockGuard
{
public:
	LockGuard(Mtx& mtx)
		:_mtx(mtx)
	{
		_mtx.lock();
	}
	~LockGuard()
	{
		cout << "~LockGuard()" << endl;
		_mtx.unlock();
	}
	//防拷贝,   如果可以拷贝的话,导致多个对象同时解锁,而一个锁不能被解锁多次,因此需要防拷贝
	LockGuard(const LockGuard<Mtx>& lg) = delete;

private:
	// 注意这里必须使用引用,不支持拷贝,否则锁的就不是一个互斥量对象
	Mtx& _mtx;
};

mutex mtx;
void fun()
{
	int i;
	cin >> i;
	//mtx.lock();
	LockGuard<mutex> lg(mtx);
	if (i == 9)
	{
		return;
		cout << i << endl;
	}
	//mtx.unlock();
	
}

int main()
{
	thread t1(fun);
	thread t2(fun);
	t1.join();
	t2.join();
	system("pause");
	return 0;
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值