vector实现遇到的问题

        前言:vector是表示可变大小数组的序列容器。就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。 本质讲,vector使用动态分配数组来存储它的元素。当新元素插入时候,这个数组需要被重新分配大小为了增加存储空间。其做法是,分配一个新的数组,然后将全部元素移到这个数组。就时间而言,这是 一个相对代价高的任务,因为每当一个新的元素加入到容器的时候,vector并不会每次都重新分配大小,下面,我们就来介绍一下,实现vector时可能会遇到的一些问题和接决策略,最终奉上模拟实现代码。

还是老规矩,我们给出实现类的初始代码,以方便更好的阅读后续的代码,该部分的代码不再做过多赘述,有兴趣的读者可以评论:

#pragma once
#include<assert.h>

namespace my_std
{
	template<class T>//vector存储的元素需要用模板来表示
	class vector {
	public:
		typedef T* iterator;
		typedef const T* const_iterator;

		iterator begin()
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}

		const_iterator begin() const
		{
			return _start;
		}

		const_iterator end() const
		{
			return _finish;
		}
		//默认构造
		vector()//声明中已给出缺省值,可以省略不写
		{}
        ~vector()
		{
			delete[] _start;//采用new/delete的形式存储
			_finish = _endofstorage = nullptr;
		}
		size_t capacity() const //空间容量
		{
			return _endofstorage - _start;
		}
		size_t size() const //数组长度
		{
			return _finish - _start;
		}
        T& operator[](size_t pos)//可读可写
		{
			assert(pos < size());
			return _start[pos];
		}
		const T& operator[](size_t pos) const//只读
		{
			assert(pos < size());

			return _start[pos];
		}
    private:
		//初始化加上缺省值
		iterator _start=nullptr;//开头
		iterator _finish = nullptr;//结尾加1
		iterator _endofstorage = nullptr;//空间容量
	};
}

目录

1.尾插元素和扩容函数

异地扩容导致原地址失效

2.匿名对象做缺省值的resize函数

3.memcpy-特殊类型的浅拷贝“隐藏杀手”

string类成员的浅拷贝

4.迭代器失效问题

insert函数带来的迭代器失效

erase函数带来的迭代器失效

5.拷贝构造和赋值问题

6.迭代器区间初始化构造

7.完整代码

vector.h

test.cpp


1.尾插元素和扩容函数

异地扩容导致原地址失效

       我们的vector实际上是一个数组容器,也是一个由头尾指针组成的一定长度的顺序表,我们在实际操作时,只能操作头指针或者尾指针中的一个来控制数组,另一个指针是随着操作的指针变化而变化的,我们只需要知道数组长度即可,但是,在当前空间不够用的情况下,又采取异地扩容,可能会导致头尾指针分离一段时间,如果不能察觉,就会导致数组出现错误:

void reserve(size_t n)//扩容函数
		{
			if (n > capacity())
			{
				T* tmp = new T[n];
				size_t sz = size();

				if (_start)
				{
					//memcpy(tmp, _start, sizeof(T) * sz);
					for (size_t i = 0; i < sz; i++)
					{
						tmp[i] = _start[i];
					}

					delete[] _start;
				}

				_start = tmp;
				_finish = _start + sz;
				_endofstorage = _start + n;
			}
		}
void push_back(const T& x)
		{
            //如果空间满了先扩容
			size_t n = _finish - _start;//提前算出保存,避免后序因异地扩容而导致原地址失效
			if (_finish == _endofstorage)
			{
				size_t len = capacity() == 0 ? 4 : capacity() * 2;
				T* temp = new T[len];
				if (_start)//如果原来有内容
				{
					memcpy(temp, _start, sizeof(T)*n);
					delete[] _start;
				}
				_start = temp;
				_finish = _start + n;
				_endofstorage = _start + len;
				//reserve(len);如果复用将从T* temp开始到_endofstorge=_start+len;注释即可
			}
			*_finish = x;
			++_finish;
			//insert(end(), x);
		}

2.匿名对象做缺省值的resize函数

     如果我们调用resize函数,其实就是重新给数组分配一定的空间,那么,这个空间内部的元素该如何初始化呢,这一般都由输入者自定义的类型来,这样一来,我们就不能将缺省值特别规定为int或者别的类型,而是使用模板,按照代码编辑者需要的类型自动生成对应的缺省值类型即可,并且,在C++中,我们的内置类型(int,double)等也有自己的构造函数,同时,我们可以将参数匿名对象设为const,这样一来可以延长匿名对象的使用周期到其使用结束后销毁。

void resize(size_t n,const T& val=T())//匿名对象表示填入的缺省值,其有可能是整形,字符串,甚至是vector等,所以采用匿名对象来调用默认构造,而const可以延长匿名对象的生命周期到不用之后再销毁
		{
			if (n <= size())//不大于尾部不用变
			{
				_finish = _start + n;
			}
			else
			{
				reserve(n);//在reserve函数内部判断是否需要扩容,并返回扩容后的空间,_finish指针不会改变相对于_start的距离
				while (_finish < _start + n)
				{
					*_finish = val;
					++_finish;
				}
			}
		}

3.memcpy-特殊类型的浅拷贝“隐藏杀手”

memcpy函数的简介:

1. memcpy是内存的二进制格式拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中

2. 如果拷贝的是自定义类型的元素,memcpy既高效又不会出错,但如果拷贝的是自定义类型元素,并且 自定义类型元素中涉及到资源管理时,就会出错,因为memcpy的拷贝实际是浅拷贝。

string类成员的浅拷贝

       我们知道string类对象的字符串并不是存放在对象本身的空间里,而是存放在对象的数据成员所指向的堆中,所以在使用memcpy时,就会导致只是将string对象的数据成员原封不动的复制拷贝下来,相当于其指向的string对象的成员的数据还是保存在原来的堆空间中,并没有发生深拷贝,出现了两个指针指向同一处的情况,这就会在原拷贝对象在析构时产生重复析构的错误。

     解决办法也比较简单,那就是用我们的笨方法,采用循环的方式将原对象一一拷贝到新的空间里来就行了,当然,这种错误的出现只会发生在需要扩容的情况下,所以,我们只需要调整扩容函数即可。

void reserve(size_t n)//扩容函数
		{
			if (n > capacity())
			{
				T* tmp = new T[n];
				size_t sz = size();

				if (_start)
				{
					//memcpy(tmp, _start, sizeof(T) * sz);//自定义对象造成异地扩容浅拷贝
					for (size_t i = 0; i < sz; i++)
					{
						tmp[i] = _start[i];
					}

					delete[] _start;
				}

				_start = tmp;
				_finish = _start + sz;
				_endofstorage = _start + n;
			}
		}

4.迭代器失效问题

迭代器失效的特征:

1.如果迭代器失效了,就不能再使用这个迭代器;

2.如果使用了这个迭代器,其结果将会是未定义的。

insert函数带来的迭代器失效

        在vector中,我们使用的一般都是迭代器的位置来表示元素的位置,像在某个元素的位置后面插入一个元素的函数,其定义一般为void insert(iterator pos, const T& x);,那么就可能会发生如下的场景,

       在这样的场景下,插入元素势必就会导致出错,所以,其中一个方法就是,我们记住pos与_start的相对偏移量,在扩容后数组移动到新的空间之后再将pos也更新到新的位置即可。

void insert(iterator pos, const T& x)
		{
			assert(pos >= _start && pos <= _finish);//等于_finish相当于尾插
			//首先检查是否空间满了,满了就要先开空间
			if (_finish == _endofstorage)
			{
				size_t len1 = pos - _start;
				cout <<"扩容前待插入位置相对于begin的偏移量为"<< len1 << endl;
				reserve(capacity() == 0 ? 4 : capacity() * 2);//注意,扩容之后会导致迭代器发生变化,也将会导致原来传入的参数失效
				size_t len2 = pos - _start;
				cout <<"扩容后待插入位置相对于begin的偏移量为"<< len2 << endl;
				pos = _start + len1;
			}
			iterator end = _finish-1;
			_finish++;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}
			*pos = x;
		}

上述的做法还有些许不妥之处,如果我们有如下的需求:

这样的话,返回的迭代器还能使用吗

       首先,我们知道insert函数在内部修改了传入的迭代器,但是这和实参并没有什么关系,因为我们的insert函数是传值传参,众所周知,传值传参不影响实参,所以,一旦调用了insert函数,迭代器就会出现诸多的可能性,也就是迭代器失效了。

为何不建议传引用传参

      针对上面的问题,有的人可能会想到传引用传参,但是,我们得形参是实参的一份拷贝,而实参又是一个常量,形参作为临时变量而具有了常性,所以不能被修改,逻辑上也就不对了,如果再将引用加上const,那我们在insert函数内部就不能对参数迭代器再进行修改了,所以,对于insert函数来说,正常的使用我们一般选择传值传参,但是其使用会导致迭代器失效的问题,这一点,我们需要注意。


erase函数带来的迭代器失效

     vector的删除函数比较好写,我们传入迭代器参数,因为不涉及扩容问题,自然也就不存在上面的insert函数造成的迭代器失效的类型的问题,我们直接在原来的空间上进行移动覆盖即可,这里详细的也不再赘述:

void erase(iterator pos)
		{
			assert(pos >= _start && pos < _finish);//注意不能等于_finish,因为_finish指向的是尾部元素的下一个元素,该处本来没有元素所以也不能删除
			iterator it = pos + 1;
			while (it != end())
			{
				*(it - 1) = *it;
				it++;
			}
			--_finish;
			
		}

那删除完之后的迭代器失效了吗?

       我们不妨来实测一下,我们取三组样例,分别对样例中的偶数元素进行删除,分析并查看其结果:

       为什么会有这样的结果呢,其实重点就在迭代器的位置,我们知道,当迭代器判断到一个元素符合条件时,会执行删除指令,这个指令也就是我们上面的erase函数,其本质上是向前挪动数据覆盖达到删除效果,而此时的迭代器在删除完元素后其实是指向了被删除元素的下一个元素,下一步我们就需要判断该元素,如果此时仍然让迭代器++的话,就会导致上一个被删除元素后面紧挨着的下一个数据被跳过了判断,也就出现了错误问题。

      解决办法其实也简单,就是我们在判断符合条件时不执行迭代器++操作,只有当该元素不符合删除条件时才执行迭代器++操作,这样就可以保证每个元素都能够被判断到,也就解决了错误。

      那,如果我想访问这个迭代器,就真没办法了吗?别急,还真有,我们来看官方文档对于迭代器失效问题的解决方案:

      官方的意思是,让erase函数返回一个迭代器,这个迭代器指向上一个已删除元素的下一个元素,其实还是原来的那个迭代器的位置。  

iterator erase(iterator pos)
		{
			assert(pos >= _start && pos < _finish);//注意不能等于_finish,因为_finish指向的是尾部元素的下一个元素,该处本来没有元素所以也不能删除
			iterator it = pos + 1;
			while (it != end())
			{
				*(it - 1) = *it;
				it++;
			}
			--_finish;
			return pos;//删除元素的下一个位置,其实还是该迭代器的位置,因为向前挪动覆盖,原来迭代器的位置上删除后存储的就是下一个元素
		}
void test_vector5()
	{
		// 1 2 3 4 5
		// 1 2 3 4 5 6
		// 2 2 3 4 5
		std::vector<int> v;//调用库里的vector,默认原来的erase迭代器失效
		v.push_back(2);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		v.push_back(5);
		//v.push_back(6);

		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;

		auto it = v.begin();
		while (it != v.end())
		{
			
			if (*it % 2 == 0)
			{
				it=v.erase(it);//让迭代器重新赋值,再次有效
			}
			else
			    ++it;
		}

5.拷贝构造和赋值问题

      对于自己写的vector,如果不单独写拷贝构造函数,那么将会使用默认的拷贝函数,也就会造成浅拷贝的情况,和赋值操作同理,所以,我们也是需要编写拷贝构造函数和赋值函数,这部分在string的部分已经详细展开,这里也不再赘述,

//拷贝构造函数
		vector(const vector<T> & x)
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
			reserve(x.capacity());//修改空间为x的空间
			for (auto& e : x)
			{
				push_back(e);//赋值即可
			}
		}
//赋值操作
		// v1 = v3
		void swap(vector<T>& v)//写成vector &x也可以
		{
			std::swap(_start, v._start);
			std::swap(_finish, v._finish);
			std::swap(_endofstorage, v._endofstorage);
		}
		vector<T>& operator=(vector<T> tmp)
		{
			swap(tmp);//交换后tmp为临时变量后续自动销毁
			return *this;
		}


6.迭代器区间初始化构造

在官方的vector文档中,我们还可以看到vector构造函数的另外l两种初始化方案:

 在上面实现的功能的基础上,想实现这两个构造函数并不能,这里给出一种实现方案:

template <class InputIterator>
		vector(InputIterator first, InputIterator last)
		{
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}
		vector(size_t n, const T& val = T())
		{
			reserve(n);
			for (size_t i = 0; i < n; i++)
				push_back(val);
		}

但是如果我们来整上一个测试样例:vector<int> v0(10, 0);显然我们的目的是想让编译器给我们初始化10个空间,并且全都初始化为0,但是,编译器真的会乖乖的听我们的吗?

         很显然出错了,编译器不听我们的,因为,编译器不知道该匹配哪个了?因为这两个数据参数都可以看做同一个类型,所以,它也符合模板的迭代器初始化的函数的参数列表,所以,编译器就会调用最匹配的那一个函数来构造:

       这种情况下,官方采用的是将 vector(size_t n, const T& val = T())再次重载,使其更加符合我们类型,比如重载为vector(int n, const T& val = T()),就可以解决我们上面的错误了,但不过说实话,这种方式的代码复用性比较差,有点面向样例编程的滋味,但是目前也没有好的解决办法了。

7.完整代码

vector.h

#pragma once
#include<assert.h>
#include <vector>

namespace my_std
{
	template<class T>
	class vector {
	public:
		typedef T* iterator;
		typedef const T* const_iterator;

		iterator begin()
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}

		const_iterator begin() const
		{
			return _start;
		}

		const_iterator end() const
		{
			return _finish;
		}
		//默认构造
		vector()//声明中已给出缺省值,可以省略不写
		{}
		//拷贝构造函数
		vector(const vector<T> & x)
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
			reserve(x.capacity());//修改空间为x的空间
			for (auto& e : x)
			{
				push_back(e);//赋值即可
			}
		}
		template <class InputIterator>
		vector(InputIterator first, InputIterator last)
		{
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}
		vector(size_t n, const T& val = T())
		{
			reserve(n);
			for (size_t i = 0; i < n; i++)
				push_back(val);
		}
		vector(int n, const T& val = T())
		{
			reserve(n);
			for (size_t i = 0; i < n; i++)
				push_back(val);
		}
		//赋值操作
		// v1 = v3
		void swap(vector<T>& v)
		{
			std::swap(_start, v._start);
			std::swap(_finish, v._finish);
			std::swap(_endofstorage, v._endofstorage);
		}
		vector<T>& operator=(vector<T> tmp)
		{
			swap(tmp);//交换后tmp为临时变量后续自动销毁
			return *this;
		}

		~vector()
		{
			delete[] _start;//采用new/delete的形式存储
			_finish = _endofstorage = nullptr;
		}
		size_t capacity() const //空间容量
		{
			return _endofstorage - _start;
		}
		size_t size() const //数组长度
		{
			return _finish - _start;
		}
		void reserve(size_t n)//扩容函数
		{
			if (n > capacity())
			{
				T* tmp = new T[n];
				size_t sz = size();

				if (_start)
				{
					//memcpy(tmp, _start, sizeof(T) * sz);//自定义对象造成异地扩容浅拷贝
					for (size_t i = 0; i < sz; i++)
					{
						tmp[i] = _start[i];
					}

					delete[] _start;
				}

				_start = tmp;
				_finish = _start + sz;
				_endofstorage = _start + n;
			}
		}
		void resize(size_t n,const T& val=T())//匿名对象表示填入的缺省值,其有可能是整形,字符串,甚至是vector等,所以采用匿名对象来调用默认构造,而const可以延长匿名对象的生命周期到不用之后再销毁
		{
			if (n <= size())//不大于尾部不用变
			{
				_finish = _start + n;
			}
			else
			{
				reserve(n);//在reserve函数内部判断是否需要扩容,并返回扩容后的空间,_finish指针不会改变相对于_start的距离
				while (_finish < _start + n)
				{
					*_finish = val;
					++_finish;
				}
			}
		}
		void push_back(const T& x)
		{
            //如果空间满了先扩容
			size_t n = _finish - _start;//提前算出保存,避免后序因异地扩容而导致原地址失效
			if (_finish == _endofstorage)
			{
				size_t len = capacity() == 0 ? 4 : capacity() * 2;
				//T* temp = new T[len];
				//if (_start)//如果原来有内容
				//{
				//	memcpy(temp, _start, sizeof(T)*n);
				//	delete[] _start;
				//}
				//_start = temp;
				//_finish = _start + n;
				//_endofstorage = _start + len;
				reserve(len);
			}
			*_finish = x;
			++_finish;
			//insert(end(), x);
		}
		void insert(iterator pos, const T& x)//而且此处不能加引用
		{
			assert(pos >= _start && pos <= _finish);//等于_finish相当于尾插
			//首先检查是否空间满了,满了就要先开空间
			if (_finish == _endofstorage)
			{
				size_t len1 = pos - _start;
				cout <<"扩容前待插入位置相对于begin的偏移量为"<< len1 << endl;
				reserve(capacity() == 0 ? 4 : capacity() * 2);//注意,扩容之后会导致迭代器发生变化,也将会导致原来传入的参数失效
				size_t len2 = pos - _start;
				cout <<"扩容后待插入位置相对于begin的偏移量为"<< len2 << endl;
				pos = _start + len1;
			}
			iterator end = _finish-1;
			_finish++;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}
			*pos = x;
		}
		iterator erase(iterator pos)
		{
			assert(pos >= _start && pos < _finish);//注意不能等于_finish,因为_finish指向的是尾部元素的下一个元素,该处本来没有元素所以也不能删除
			iterator it = pos + 1;
			while (it != end())
			{
				*(it - 1) = *it;
				it++;
			}
			--_finish;
			return pos;//删除元素的下一个位置,其实还是该迭代器的位置,因为向前挪动覆盖,原来迭代器的位置上删除后存储的就是下一个元素
		}
		T& operator[](size_t pos)//可读可写
		{
			assert(pos < size());
			return _start[pos];
		}
		const T& operator[](size_t pos) const//只读
		{
			assert(pos < size());

			return _start[pos];
		}
	private:
		//初始化加上缺省值
		iterator _start=nullptr;//开头
		iterator _finish = nullptr;//结尾加1
		iterator _endofstorage = nullptr;//空间容量
	};
	void test1()
	{
		vector<int> v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		v.push_back(5);


		for (size_t i = 0; i < v.size(); i++)
			cout << v[i] << " ";
		cout << endl;
		for (vector<int>::iterator it = v.begin(); it != v.end(); it++)
		{
			*it *= 10;
			cout << *it << " ";
		}
		cout << endl;
		for (auto e : v)
			cout << e << " ";
		cout << endl;

	}
	void test_vector2()
	{
		int i = 0;
		int j(1);//可见内置类型在模板中是存在默认构造函数的
		int k = int(2);

		vector<int*> v1;
		v1.resize(10);

		vector<string> v2;
		//v2.resize(10, string("xxx"));
		v2.resize(10, "xxx");//单参数的构造函数支持隐式类型的转换

		for (auto e : v1)
		{
			cout << e << " ";
		}
		cout << endl;

		for (auto e : v2)
		{
			cout << e << " ";
		}
		cout << endl;
	}
	void test_vector3()
	{
		vector<int> v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		v.push_back(5);
		v.push_back(6);
		v.push_back(7);

		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;

		vector<int>::iterator it = v.begin() + 2;
		v.insert(it, 30);

		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;

		//v.insert(v.begin(), 30);
		v.insert(v.begin() + 3, 30);
		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;
	}
	void test_vector4()
	{
		vector<int> v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		v.push_back(5);
		v.push_back(6);
		v.push_back(7);

		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;

		auto pos = v.begin();
		v.erase(pos);

		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;

		v.erase(v.begin() + 3);
		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;
	}
	void test_vector5()
	{
		// 1 2 3 4 5
		// 1 2 3 4 5 6
		// 2 2 3 4 5
		std::vector<int> v;//调用库里的vector,默认原来的erase迭代器失效
		v.push_back(2);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		v.push_back(5);
		//v.push_back(6);

		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;

		auto it = v.begin();
		while (it != v.end())
		{
			
			if (*it % 2 == 0)
			{
				it=v.erase(it);//让迭代器重新赋值,再次有效
			}
			else
			    ++it;
		}

		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;
	}
	void test_vector6()
	{
		vector<string> v;
		v.push_back("111111111111111111111");
		v.push_back("111111111111111111111");
		v.push_back("111111111111111111111");
		v.push_back("111111111111111111111");
		v.push_back("111111111111111111111");


		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;
	}
	void test_vector7()
	{
		vector<int> v1;
		v1.push_back(1);
		v1.push_back(1);
		v1.push_back(1);
		v1.push_back(1);
		v1.push_back(1);

		vector<int> v2(v1);

		for (auto e : v1)
		{
			cout << e << " ";
		}
		cout << endl;

		for (auto e : v2)
		{
			cout << e << " ";
		}
		cout << endl;

		vector<int> v3;
		v3.push_back(10);
		v3.push_back(20);
		v3.push_back(30);
		v3.push_back(40);

		v1 = v3;

		for (auto e : v1)
		{
			cout << e << " ";
		}
		cout << endl;
	}
	void test_vector8()
	{
		vector<int> v0(10, 0);
		vector<string> v1(10, "xxxx");

		for (auto e : v1)
		{
			cout << e << " ";
		}
		cout << endl;

		vector<int> v2;
		v2.push_back(10);
		v2.push_back(20);
		v2.push_back(30);
		v2.push_back(40);

		vector<int> v3(v2.begin(), v2.end());

		string str("hello world");
		vector<int> v4(str.begin(), str.end());
		for (auto e : v3)
		{
			cout << e << " ";
		}
		cout << endl;

		for (auto e : v4)
		{
			cout << e << " ";
		}
		cout << endl;
	}
}

test.cpp

#include<iostream>
using namespace std;
#include "vector.h"
int main()
{
	//my_std::test1();
	my_std::test_vector8();



	return 0;
}

       

        每个人都有一段异常艰难的时光,生活的压力,工作的失意,学业的压力,爱的惶惶不可终日,如果此刻不太顺利的话,一定是有十倍百倍的运气在前方等着你!不要怀疑自我,大步往前走,以后会很好很好的。

    

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值