C++初阶:string类

1 为什么要学习string类

C语言中,字符串是以’\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。

2标准库中的string类

在这里插入图片描述

可以看出:
1 string是表示字符串的字符串类
2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
3. string类是basic_string类模板的一个实例化,使用char作为其字符类型。
4. 不能操作多字节或者变长字符的序列

使用string类的说明
1包含#include 头文件
2 使用using namespace std;

3 string类的常用接口说明

3.1 string类对象的常见构造

在这里插入图片描述

① 构造空的string类对象,即空字符串
②拷贝构造函数
③用C-string来构造string类对象

示例

void test1()
{
	//构造空的string类
	string str;
	cout << str << endl;
	//用字符串构造string类
	string s("hello world");
	cout << s << endl;
	//拷贝构造
	string ss(s);
	cout << ss << endl;
}

3.2 析构函数

在这里插入图片描述

析构函数会自动调用

3.3赋值重载

在这里插入图片描述

可以使用string对象,字符串,以及字符进行赋值

void test10()
{
	string s1  ("hello world");
	string s2;
	//string对象进行赋值
	s2 = s1;
	cout << s2 << endl;
	//字符串进行赋值
	s2 = "cppp";
	cout << s2 << endl;
	//字符进行赋值
	s2 = 'c';
	cout << s2 << endl;

}

3.4 string类对象的容量操作

函数名称功能说明
size返回字符串有效字符长度
length返回字符串有效字符长度
capacity返回空间总大小
reserve为字符串预留空间
resize为字符串预留空间并初始化
void test2()
{
	string s1("hello world");
	cout << s1.size() << endl;
	cout << s1.length() << endl;
	//size 和length都是返回字符串的长度
	cout << s1.capacity() << endl;
	//返回空间总大小
	string s2;
	s2.reserve(100);
	//开好100个字符空间的大小
	
	string s3;
	s3.resize(10, 'c');
	//开10个字符空间,并初始化为'c'
}

在这里插入图片描述

注:当reserve的参数小于string的底层空间总大小(15)时,reserve不会改变容量大小。

在这里插入图片描述

3.5 string类对象的访问及遍历操作

函数名称功能说明
operator[ ]返回pos位置的字符
begin + endbegin返回起始位置,end返回结尾字符的下一个位置
rbegin + rendrbegin返回反向起始位置,end返回反向结尾字符的下一个位置
范围forC++11支持更简洁的范围for的新遍历方式

注意:用operator[ ]访问string对象是用下标进行访问,类似于用下标访问数组的方式;而用begin,end,rbegin,rend对string对象进行访问,是通过迭代器进行。迭代器是为STL容器专门打造的一种机制,访问容器中的元素,需要通过迭代器进行,迭代器是像指针一样的类型,所以用法也和指针类似。通过迭代器就可以对它所指向的元素进行相关操作。

operator[ ]
在这里插入图片描述
因为operator[ ] 返回的是pos位置的引用,所以支持修改string对象

void test4()
{
	string s1 = "hello world";
	cout << s1[2] << endl;
	//将s1[2]位置的字符修改为'w’
	s1[2] = 'w';
	cout << s1[2] << endl;
	cout << s1 << endl;
	int i;
	for (i = 0; i < s1.size(); i++)
	{
		//对s1[i]位置的字符++
		s1[i]++;
	}
	cout << s1 << endl;
}

在这里插入图片描述
迭代器

正向迭代器

void test5()
{
	string s1 = "hello world";
	string::iterator it = s1.begin();
	//begin()返回字符串的起始下标
	while (it != s1.end())
	{
	//end()返回字符串最后一个元素的下一个位置,即'\0'
		cout << *it ;
		++it;

	}
}

注意:iterator是迭代器的类型名,在使用时必须指明类域

反向迭代器

顾名思义:倒着遍历字符串

void test6()
{
	//反向迭代器

	string s1 = "hello world";
	string::reverse_iterator rit = s1.rbegin();
	while (rit != s1.rend())
	{
		cout << *rit;
		rit++;
	}
	cout << endl;
	
}

常量迭代器

常量迭代器不支持修改对象,一般用于常对象或者常对象做形参防止函数内部对对象进行修改

void PrintString(const string& s)//s为常对象
{
	//正向迭代
	string::const_iterator cit = s.begin();
	/*auto cit = s.begin();*///如果cit的类型太长,也可以使用auto关键字自动推导类型
	while (cit != s.end())
	{
		cout << *cit;
		cit++;
	}
	cout << endl;
	//反向迭代
	string::const_reverse_iterator rit = s.rbegin();
	while (rit != s.rend())
	{
		cout << *rit;
		rit++;
	}
	cout << endl;

}

范围for

void test7()
{
	string s1("hello world");
	for (auto &ch : s1)
		{
            cout << ch;
			ch++;
	    }
}

范围for在使用起来非常方便,它可以自动判断数组元素的类型和数组的大小,但是只能正向遍历
范围for的底层实际就是迭代器。

3.6 string类对象的修改操作

函数名称功能说明
push_back在字符串后尾插字符
append在字符串后追加一个字符串
operator+=在字符串后追加字符串str
insert在pos位置插入字符或字符串
erase删除字符串中从pos位置开始并跨越len个字符的部分

push_back和 append

void test8()
{
	string s1("hello,world");
	//s1后面插入'x'
	s1.push_back('x');
	cout << s1 << endl;
	//s1后面插入字符串"hi"
	s1.append("hi");
	cout << s1 << endl;
}

其中append还支持使用迭代器追加一段区间的字符串

在这里插入图片描述

void test8()
{
	string s1("hello,world");
	string s("this is a demo ");
	//在字符串s的后面追加字符,范围是从[s1.begin()    s1.end()]
	s.append(s1.begin(), s1.end());
	cout << s << endl;
	//在字符串s的后面追加字符,范围是从[s1.begin()+3    s1.end()-3]
	s.append(s1.begin() + 3, s1.end() - 3);
	cout << s << endl;
}

operator+=
在这里插入图片描述

可以看出,operator+=支持追加字符,也支持追加字符串,还支持追加string类型的对象,所以平常使用的过程中,我们更倾向于使用operator+=来进行追加,其代码可读性也更强

void test9()
{
	string s1("hello,world");
	string s("!!!");
	//追加字符'x'
	s1 += 'x';
	//追加字符串"你好"
	s1 += "你好";
	//追加string类型的对象s
	s1 += s;
	cout << s1 << endl;
}

insert

假设在字符串的每个空格位置前插入###,该如何实现呢?

void test_string1()
{
	string s1("I am back");
	for (int i = 0; i < s1.size();)
	{
	//遇到空格就进行插入
		if (s1[i] == ' ')
		{
			s1.insert(i, "###");
			//在插入完成后,要改变i,使i指向当前空格的下一个位置,才能继续插入
			i += 4;
			
		}
		else
		{
		//没有遇到空格就往后++找空格
			i++;
		}

	}
	cout << s1 << endl;
}

补充:如果要将字符串的空格位置替换为###,又该如何实现呢?
构造一个新的字符串news,遍历s,没有遇到空格就把s的字符+=到news后面,遇到空格,+= ###到news的后面

oid test_string4()
{
	string s("I am back");
	string news;
	for (int i = 0; i < s.size();i++)
	{
		if (s[i] == ' ')
		{
			news+="###";

		}
		else
		{
			news += s[i];
		}

	}
	cout << news << endl;

}

erase

void test_string2()
{
	string s("I am back");
	for (int i = 0; i < s.size(); i++)
	{
		if (s[i] == ' ')
		{
			//将空格删除
			s.erase(i,1);
		}
	}
	cout << s << endl;
}

3.7 string类的查找操作

函数名称功能说明
c_str返回c格式的字符串
find+npos从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置
rfind从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置
substr在str中从pos位置开始,截取n个字符,然后将其返回

find ,rfind,substr

void test_string5()
{
	string filename1("test.cpp");
	//找文件名的后缀
	//方法,find()函数找到"."的位置,substr()函数返回字符"."以后的字符串,即为后缀
	size_t  pos1= filename1.find('.');
	if (pos1 != string::npos)
	{
		string str = filename1.substr(pos1);
		cout << str << endl;
	}
	string filename2("test.cpp.zip.tar");
	//找文件名的真实后缀,即最后一个字符'.'对应的后缀
	//这时就需要倒着找,使用rfind();
	size_t pos2 = filename2.rfind('.');
	if (pos2 != string::npos)
    {
		string ss = filename2.substr(pos2);
		cout << ss << endl;

	}
}

注: string::npos参数 —— npos 是一个常数,表示无符号整型的最大值,用来表示不存在的位置

使用find+substr分割网址

void test_string6()
{
	string  url("https://cplusplus.com/reference/string/basic_string/basic_string/");
	size_t pos1 = url.find("://");
	if (pos1 == string::npos)
	{
		cout << "非法的url" << endl;
		return;
	}
	string protocol = url.substr(0,pos1);
	size_t pos2 = url.find('/', pos1 + 3);
	if (pos1 == string::npos)
	{
		cout << "非法的url" << endl;
		return;
	}
	string domain = url.substr(pos1 + 3, pos2 - pos1 - 3);
	string uri = url.substr(pos2+1);
	cout << protocol << endl;
	cout << domain << endl;
	cout << uri << endl;

	
}

3.8 string类的其它函数

这些函数都是全局函数并没有实现在string类里

其它接口功能说明
字符串与其它类型进行转换stoi,stol,stof,stod等分别是字符串转整形,长整形,单精度浮点型,双精度浮点型 ;to_string是其它类型转为字符串
getline读取一行字符串,不会因为空格而结束
void test_string7()
{
	int vali = 999;
	double vald = 999.88;
	//int转为字符串
	string stri = to_string(vali);
	//double转为字符串
	string strd = to_string(vald);
	cout << stri << endl;
	cout << strd << endl;
	stri = "888";
	strd = "888.99";
	//字符串转为int
	vali = stoi(stri);
	//字符串转为double
	vald = stod(strd);
	cout << vali << endl;
	cout << vald << endl;
}

cin在输入字符串的时候,会以空格作为单词的分割,如果输入的字符串里面带有空格,则需要用getline

假设输入"hello world"

在这里插入图片描述
如果使用cin输入,则只会输出hello
在这里插入图片描述

4 string的模拟实现

4.1 类的定义

namespace zbt
{
	class string
	{
	public:
	string()//构造函数
	{
	
	}
	~string()//析构函数
	{
		
	}
	private:
		char* _str;
		size_t _size;//指向有效字符的下一个
		size_t _capacity;//区间容量
	};
	void test1()
	{
	...
	}
 }
		

为了和C++库里的string区分开,我们自己定义一个命名空间,将自己实现的string类以及测试函数都放在里面

4.2 构造函数

string(const char* str = "")//给一个缺省值,如果没有传参,默认为空值""
		{
			_size = strlen(str);//strlen算出有效字符的个数,赋值给_size
			_capacity = _size;
			_str = new char[_size + 1];//开空间时要多开一个,存储'\0'
			strcpy(_str, str);//将str的内容拷贝到_str中

		}

需注意,在初始化_str时,需动态开辟一个和str一样大的空间,再将str的值拷贝到_str中

4.3析构函数

~string()
		{
			delete[]_str;//delete[]和new[]对应
			_str = nullptr;
			_size = _capacity = 0;
		}

4.4 operator[ ]

char& operator[](size_t pos)
		{
			assert(pos < _size);//首先判断给的下标要小于_size
			return _str[pos];//返回pos位置的值,传引用返回,支持修改string对象

		}
const char& operator[](size_t pos)const//const类型,只读,不可修改
{
	assert(pos < _size);
	return _str[pos];
}

4.5 迭代器

这里我们先实现正向迭代器,string的迭代器底层实际就是指针

typedef char* iterator;//将char*重名为iterator
typedef const char* const_iterator;// const char*重名为const_iterator
		iterator begin()//返回字符串的起始下标,即为_str
		{
			return _str;
		}
		iterator end()//返回有效字符的下一个位置,即'\0'
		{
			return _str + _size;//因为是指针,所以起始位置+ _size即为'\0’的位置
		}
		const_iterator begin()const//常量正向迭代器,加上const即可
		{
			return _str;
		}
		const_iterator end()const
		{
			return _str + _size;
		}

4.6 拷贝构造函数

这里就会涉及深浅拷贝问题,string的成员变量都为内置类型,如果不自己实现拷贝构造函数,那么编译器会自己默认生成一个浅拷贝的构造函数,假设用s1去拷贝构造s2,s1和s2中的成员变量_str便会指向同一块空间,在执行析构函数的时候,便会对同一块空间释放两次,程序会崩溃掉。深浅拷贝详细讲解请点击这里

那么拷贝构造函数应该实现的是深拷贝,即s1和s2中的成员变量_str指向两块不同的空间,两块空间上内容一样。即给每个对象独立分配资源,保证多个对象之间不会因为共享资源而导致多次释放

既然知道了string的拷贝构造函数是深拷贝,那么具体该如何实现呢?首先是要给s2开辟一块新空间,大小和s1一样,其中s2成员变量中的_size,_capacity应该和s1中的一样,最后再将s1中的字符串内容拷贝给s2。

//s2(s1)  s为s1的别名
//传统版本
	string(const string& s)
			:_str(new char[s._capacity + 1])
		    , _size(s._size)
			, _capacity(s._capacity)
		{
			strcpy(_str, s._str);
		}

对于成员变量的初始化,采用的是初始化列表的方法,函数体内实现拷贝字符串内容。

这里是传统版本的写法,其代码的可读性高。还有一种版本是现代版本,其代码简洁,一起来看看吧

现代版本的核心思想是老板思维,要实现拷贝构造函数,不自己实现,而是去构造出一个临时对象tmp(可以理解为打工人),tmp中的内容和s1一样,最后将s2和tmp交换即可

void swap(string& tmp)
		{
			//函数里面调用的是库里面的swap函数实现成员变量的交换,并不是我们自己写的swap函数
			::swap(_str, tmp._str);
			::swap(_size, tmp._size);
		   ::swap(_capacity, tmp._capacity);

		}
		//s2(s1),s是s1的别名
		string(const string& s)
			:_str(nullptr)
			,_size(0)
			,_capacity(0)
		{
			string tmp(s._str);//调用构造函数,构造出临时对象tmp
			swap(tmp);//将s2和tmp的内容交换

	    }

需要对s2的成员变量_str初始化为空,因为当s2和tmp交换后,tmp即为原来的s2,出函数作用域的时候,tmp是局部变量,会调用tmp的析构函数释放tmp,如果_str没有初始化,那就是随机值,会出错。

4.7 赋值运算符重载

(1) 原始写法

实现 s1=s2;函数里面s便是s2的别名

string& operator=(const string& s)
{
			if (this != &s)//防止自己给自己赋值
			{
				char* tmp = new char[s._capacity + 1];
			strcpy(tmp, s._str);
				delete[] _str;
			_str = tmp;
		_size = s._size;
			_capacity = s._capacity;
			}
				return *this;
		}

首先需要开辟一块数组空间tmp,里面存储s2的内容,既然要将s1的内容改为s2,那么s1原有的内容就应该先释放掉,然后s1里面的内容为之前tmp保存的内容。size和capacity也应该和s2一致。

(2) 现代写法

string& operator=(const string& s)
{
	if (this != &s)
		{
			string tmp(s._str);//调用构造函数
		//		string tmp(s);//调用拷贝构造
			swap(tmp);
		}
		return *this;
}

要实现运算符重载,不自己实现,构造一个tmp对象,里面的内容和s2一样,将s1和tmp进行交换即可。因为上文已经实现了拷贝构造函数,所以这里在构造tmp对象的时候也可以调用拷贝构造函数。

(3)更为简洁的现代写法

//s1=s2,s是s2的一份拷贝,这里让s做tmp完成交换
		string& operator=(string s)
		{
			swap(s);
			return *this;
		}

这种写法需要注意,形参不能用引用,如果用引用的话,s是s2的别名,s和s1进行交换也就是s2和s1进行交换,会改变s2的值。

4.8 插入操作

4.8.1 push_back

void push_back(char ch)//尾插一个字符
		{
			if (_size == _capacity)//满了就需要扩容
			{
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}
			_str[_size] = ch;
			_size++;
			_str[_size] = '\0';
		}

因为_size指向的是有效字符的下一个位置,所以在下标为_size的位置直接插入ch即可,因为是字符串,所以插入完成后要在下一个位置要添上\0

在空间容量满的时候,需要扩容,再来看看扩容的实现吧

void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete[]_str;
				_str = tmp;
				_capacity = n;
			}
		}

自己实现扩容,只能实现异地扩。即自己再新开一块空间,将原有的内容拷贝过去,_str指向新开辟的空间,_capacity更新为现有的容量,别忘了原来开辟的空间也要释放掉。

实现了reserve,再来看看和它很像的resize函数吧
resize是开空间并初始化

void resize(size_t n, char ch = '\0')
		{
			if (n > _size)//初始化字符的个数比原有的有效字符个数多
			{
				reserve(n);//先调用reserve开好空间
				int i;
				for ( i = _size; i < n; i++)
				{
					_str[i] = ch;//将_size之后的字符初始化为ch
				}
				_str[i] = '\0';
				_size = n;
			}
			else//初始化字符的个数比原有的字符个数少
			{//删除数据,不初始化
				_str[n] = '\0';
				_size = n;
			}
		}

4.8.2 append

void append(const char* s)//尾插一串字符串
		{
			size_t len = strlen(s);
			if (_size + len > _capacity)//先计算要插入字符的个数,空间不够就继续扩容
			{
				reserve(_size + len);
			}
			strcpy(_str + _size, s);//将要插入的字符串s拷贝到原有字符串的后面
		
			_size += len;//更新有效字符的个数
		}

注意这里扩容不能像以前一样直接扩二倍,有可能插入的字符串很长,扩二倍容量还是不够,所以需要多少空间就扩多少。

void append(const string &s)//插入string类型的对象
		{
			append(s._str);//调用尾插字符串的函数
		}

4.8.3 operator+=

直接复用之前写好的尾插函数

string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}
string& operator+=(const char* s)
		{
			append(s);
			return *this;
		}
string& operator+=(const string& str)
		{
			append(str);
			return*this;
		}

4.9 在任意位置插入

任意位置插入一个字符

string& insert(size_t pos, char ch)
{
			assert(pos <= _size);
			if (_size == _capacity)//空间不够就扩容
			{
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}
			size_t end = _size + 1;
			while (end > pos)//把pos位置到结尾的字符统一向后移动一个单位长度
			{
				_str[end] = _str[end - 1];
				end--;
			}
			_str[pos] = ch;//ch放入pos位置
			_size++;
			return *this;
}

任意位置插入一串字符串

string& insert(size_t pos, const char* str)
		{
			assert(pos <= _size);
			size_t len = strlen(str);//计算出要插入字符的个数
			//空间不够就扩容
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}
			size_t end = _size + len;
			while (end>=pos+len)//pos位置至结尾的字符统一向后移动len个单位长度
			{
				_str[end] = _str[end - len];
				end--;
			}
			strncpy(_str + pos, str, len);//插入的字符串拷贝过来
			_size = _size + len;
			return *this;

		}

扩容的时候同样也要注意,不能扩二倍,而是需要多少空间就扩多少

4.10 任意位置删除

string& erase(size_t pos, size_t len = npos)
		{
			//从pos位置开始删除len个字符
			if (len == npos || pos + len >= _size)//直接从pos位置删到结尾
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				strcpy(_str + pos, _str + pos + len);//pos+len位置之后的字符拷贝到pos位置处
				_size = _size - len;
			}
			return *this;
		}

第一种情况,如果从pos位置之后要删除完,那么pos位置直接置为’\0’就好,第二种情况,没有删除完,那么pos+len位置之后的字符串要拷贝到pos位置处

4.11 查找

4.11.1 查找一个字符

size_t find(char ch, size_t pos = 0)const
		{
			//从pos位置开始查找一个字符
			assert(pos < _size);
			for (size_t i = pos; i < _size; i++)
			{
				if(_str[i]==ch)//如果找到了,返回下标
					return i;
			}
			return npos;//没找到,返回npos
		}

4.11.2 查找一串字符

size_t find(const char* sub, size_t pos = 0)const
		{
			assert(sub);
			assert(pos < _size);
			char* ptr = strstr(_str + pos, sub);//调用strstr函数,看有没有匹配的子串
			if (ptr == nullptr)
			{
				//ptr为空,说明匹配失败,返回npos
				return npos;
			}
			else
				return ptr - _str;//找到了,返回子字符串的起始下标
		}

查找字符串的时候,调用的是c里面的strstr函数,看从pos位置往后的字符串里面有没有与sub匹配的字符串,有的话ptr就会指向找到的字符串,没有的话指向空。

4.11.3 返回子字符串

string substr(size_t pos = 0, size_t len = npos)const
{
			//从pos位置开始,返回len个字符
			assert(pos < _size);
			size_t reallen = len;//记录返回字符的真实个数
			if (len == npos || pos + len > _size)
			{
				reallen = _size - pos;//真实的个数为总的字符个数减去pos位置之前的个数
			}
			string sub;
			for (int i = 0; i < reallen; i++)
			{
				sub += _str[pos + i];
			}
			return sub;

}

在返回子字符串的时候,我们是把从pos位置往后的len个字符+=到sub中,最后返回sub。在访问字符的时候,使用的是operator[ ],那么这就要求下标不能越界,但是有可能返回字符的个数len大于_size,或者从pos位置往后加上len个字符会大于_size,此时就要修改返回字符的真实个数。

4.12 其他接口

bool operator>(const string s)const
		{
			return strcmp(_str, s._str) > 0;
		}
		bool operator==(const string s)const
		{
			return strcmp(_str, s._str) == 0;
		}
		bool operator>=(const string s)const
		{
			return *this>s||*this==s;
		}
		bool operator<(const string s)const
		{
			return !(*this >= s);
		}
		bool operator<=(const string s)const
		{
			return !(*this > s);
		}
		
		bool operator!=(const string s)const
		{
			return !(*this == s);
		}

字符串之间的比较,直接调用strcmp函数即可,实现>和==,其他的复用。

4.13 输入输出

4.13.1 opeerator <<

ostream& operator<<(ostream& out, const string&s)
	{
		for (size_t i = 0; i < s.size(); i++)
		{
			out << s[i];
		}
		return out;
	}

通过访问下标的方式,输出每一个字符
实现在string类的外面,但是并没有访问类里面的私有成员,所以也不用写为友元函数

4.13.2 operator>>

void clear()
		{
			_str[0] = '\0';
			_size = 0;
		}

istream& operator>>(istream& in, string& s)
	{
		s.clear();//s本身有数据,还要输入,要把之前的数据清空再输入
		char ch;
		ch = in.get();
		const size_t N = 32;
		char buff[N];
		int i = 0;
		//先把从键盘获取到的字符存储在buff数组里面
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == N - 1)//buff数组满了
			{
				buff[i] = '\0';
				s += buff;//将获取到的一串字符串+=到s上
				i = 0;//下标置为0,重复利用buff数组,继续存储接下来的字符
			}
			ch = in.get();

		}
		//遇到空格或者换行,一串字符获取结束,将buff数组剩下的字符+=到s上
		buff[i] = '\0';
		s += buff;
		return in;
	}

当输入的字符串很长的时候,便会频繁+=,不断扩容,效率变低。所以这里采用先把字符存储在buff数组里面,buff数组满了以后再+=,并且buff数组的空间可以重复使用,会提高效率。

使用get()函数读取字符是因为cin在输入的时候以空格或者换行作为字符串的分割,如果使用cin读字符,便不会获取到空格或者换行,便无法得知字符输入何时结束,而get函数是cin的一个成员函数,它可以读取任意一个字符 。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值