C++11的列表初始化和右值引用

目录

前言

一、C++11的简介

二、C++11的小故事。

三、统一的列表初始化

1.列表初始化

2.initializer_list

四、右值引用

1.什么是左值

2.什么是右值

3.右值引用写法

4.右值的分类

5.右值引用的作用

6.STL容器中的右值引用

7.万能引用

总结


前言

        C++11相较于之C++98,能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多。许多功能看起来会很怪,效果却很好,所以我们要作为一个 重点去学习。

一、C++11的简介

        在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了 C++98称为C++11之前的最新C++标准名称。不过由于C++03(TC1)主要是对C++98标准中的漏洞 进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。 从C++0x到C++11,C++标准10年磨一剑,第二个真正意义上的标准珊珊来迟。

二、C++11的小故事。

        1998年是C++标准委员会成立的第一年,本来计划以后每5年视实际需要更新一次标准,C++国际 标准委员会在研究C++ 03的下一个版本的时候,一开始计划是2007年发布,所以最初这个标准叫 C++ 07。但是到06年的时候,官方觉得2007年肯定完不成C++ 07,而且官方觉得2008年可能也 完不成。最后干脆叫C++ 0x。x的意思是不知道到底能在07还是08还是09年完成。结果2010年的 时候也没完成,最后在2011年终于完成了C++标准。所以最终定名为C++11。 

三、统一的列表初始化

1.列表初始化

在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定,如下 

C++11提出一切都可以使用列表初始化。如下,甚至可以把赋值符号省略掉

对于结构体也是如此,可以省略赋值符号,我们这里看起来似乎用处不怎么大,但这种写法是在为后面的内容做准备 

上面的是使用的结构体,我们再来看看C++的class是否也一样。 我们从下图中发现没问题,但是不同的写法也是有区别的

 像如下这种写法,C++11也是支持的,这样也是挺方便的。

2.initializer_list

再讲解initializer_list之前,我们先来看一下下面这个例子,Date是上面用到的日期类,vector和list是STL库里面的容器。请问,他们后面的{1,2,3}是一样的吗?

 

这样乍眼一看,好像确实是一样的,实际上他们有本质的区别,d1后面接的{1,2,3}只是d1的三个参数, 他是构造+拷贝构造,被编译器优化为直接构造,并且固定只能写三个参数

而vector<int> v和 list<int> l 他们两后面接的{1,2,3},是initializer_list,他是库里面的容器

因为vector和list支持了 initializer_list去进行构造,因此可以这样玩。同时对于参数的个数是不设限制的

vector和list如此,其他很多容器也是这样,我们随便举个map的例子。

四、右值引用

1.什么是左值

C++中,值可以分为左值和右值,我们之前学的引用都是左值引用,如下

这里并不是说在左边的是左值,而是可以取地址的才是左值,左值引用就是给左值的引用,给左值取别名。如下,虽然第二行 i 在右边,但我们可以给 i 取地址,因此 i 是左值,

2.什么是右值

        右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边右值不能取地址。右值引用就是对右值的引用,给右值取别名。

举个例子

          

3.右值引用写法

右值引用是加&&,比左值还多一个&,如下

左值引用不能给右值取别名,除非加const,缩小权限。 

           

右值引用不能给左值取别名,除非加move()函数将左值转为右值(大家记住就好,具体我们后续了解)

                 

4.右值的分类

内置类型的右值称作纯右值

自定义类型的右值称作将亡值(下面会提到)

5.右值引用的作用

C++11为什么要搞一个右值引用,有什么东西是左值引用解决不了的吗?

我们模拟实现了一个简易版的string,帮助我们打印理解,代码如下

namespace bit
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			//cout << "string(char* str) -- 构造" << endl;

			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		// s1.swap(s2)
		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}

		// 拷贝构造
		string(const string& s)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;

			string tmp(s._str);
			swap(tmp);
		}

		// 赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(const string& s) -- 深拷贝" << endl;
			/*string tmp(s);
			swap(tmp);*/
			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;
		}

		~string()
		{
			delete[] _str;
			_str = nullptr;
		}

		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}

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

				_capacity = n;
			}
		}

		void push_back(char ch)
		{
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}

			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}

		//string operator+=(char ch)
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0; // 不包含最后做标识的\0
	};

	bit::string to_string(int x)
	{
		bit::string ret;
		while (x)
		{
			int val = x % 10;
			x /= 10;
			ret += ('0' + val);
		}

		reverse(ret.begin(), ret.end());

		return ret;
	}
}

我们来看下面的例子,我们有一个to_string函数,可以输入整形返回string,我们发现代码竟然发生了两次深拷贝(注意,这里我们是VS2019,使用VS2022编译器优化太厉害,结果会不一样

        具体流程如下,ret(将亡值)出了作用域就会析构,因此如果返回ret的话,他在返回时会拷贝生成一个临时对象,将这个临时对象进行operator= 再赋值给s,这样就会进行两次深拷贝

        这里ret是左值,但是左值引用解决不了问题,因为他出了作用域会析构,内容就不存在了,右值引用也无法解决问题,因为ret为左值,无法进行右值引用。就算你改为move(ret),这样是可以运行,但是ret被析构的事实你无法解决,你内存都还给编译器了,你还想赋值,结果肯定不对。

  • 那我们该如何处理呢?这就需要提到移动构造了(转移将亡值的资源,从而避免深拷贝

        如果在拷贝构造的地方重写一个右值引用的构造(被称作移动构造),把将亡值ret的资源转移给那个临时对象,临时对象再去调用operator=进行赋值拷贝,这样是不是可以减少一次深拷贝(移动构造的代价很低)

移动构造代码如下

string(string&& s)
{
	cout << "string(string&& s) -- 移动拷贝" << endl;
	swap(s); //swap是类里的函数,我们上面的代码中有。
}

        我们画图再分析一下,如下是没有写移动构造的情况,先构造一个ret对象,再使用ret对象深拷贝一个临时对象,再使用这个临时对象去operator=深拷贝赋值给s对象。深拷贝的代价比较大,这效率是真的不高。(这里三个_str的地址都不一样,第一次为构造,后两次为深拷贝)

        如果我们写了string类的移动拷贝,具体流程就是如下所示了,再ret返回时会进行移动构造而不是拷贝构造,因为ret是右值,重载的右值引用会更加符合。同时在移动构造内只有swap函数,进行资源的交换。这里可以看到下图1和图2string的_str地址都是一样的0x005e0198。因为移动拷贝交换了资源,没有深拷贝提高了效率。

那么我们是不是可以按照这个思路,函数重载一下operator=() 的右值引用版本,进行移动构造,是不是又可以节省一次深拷贝呢? 

我们添加如下代码进行operator=的移动拷贝

string& operator=(string&& s)
{
	cout << "string& operator=(string&& s) -- 移动拷贝" << endl;
	swap(s);
	return *this;
}

        调试看一看,将亡值ret在析构前将资源给到的临时对象,将亡值临时对象又再析构前将资源给到了s。因此只进行了两次移动拷贝,便完成了操作,三次操作_str的地址都是0x010da9a8。

6.STL容器中的右值引用

由于右值引用的效率比深拷贝高很多,因此C++11在STL容器里,很多地方都用到了右值引用

vector构造函数中的的右值引用

push_back和insert也有右值引用版本 

其他容器也是如此,就不多一一举例了 

我们直接使用库里面的vector来看一下效果,先reserve(20)是为了防止发生扩容,string进行深拷贝影响我们判断。如下,发生两个移动拷贝,1234转为string类型返回,将亡值ret移动构造了临时对象,将亡值临时对象再移动构造了string类型,最后被添加到 v 后面,好理解。

使用自己模拟的vector试一下。如下是我们之前模拟的vector。将他放到vector.h中,test.cpp文件包一下头文件。

#pragma once
namespace kky
{
	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>& v)
		{
			reserve(v.capacity());
			for (auto& e : v)
			{
				push_back(e);
			}
		}

		template <class InputIterator>
		vector(InputIterator first, InputIterator last)
		{
			while (first != last)
			{
				push_back(*first);
				first++;
			}
		}

		vector(int n, const T& val = T())
		{
			resize(n, val);
		}

		//vector(size_t n, const T& val = T())
		//{
		//	resize(n,val);
		//}

		vector<T>& operator=(vector<T> v)
		{
			swap(_start, v._start);
			swap(_finish, v._finish);
			swap(_endofstorage, v._endofstorage);
			return *this;
		}


		~vector()
		{
			delete[] _start;
			_start = _finish = _endofstorage = nullptr;
		}

		size_t size() const
		{
			return _finish - _start;
		}
		size_t capacity() const
		{
			return _endofstorage - _start;
		}

		void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t sz = size();
				T* tmp = new T[n];
				if (_start)
				{
					//memcpy对自定义类型是浅拷贝
					//memcpy(tmp, _start, sz * sizeof(T));
					//循环赋值,会调用自定义类型的赋值拷贝,就是深拷贝
					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())
		{
			if (n <= size())
			{
				_finish = _start + n;
			}
			else
			{
				reserve(n);
				for (size_t i = size(); i < n; i++)
				{
					_start[i] = val;
				}
				_finish = _start + n;
			}
		}

		void push_back(const T& x)
		{
			if (size() == capacity())
			{
				size_t cp = capacity() == 0 ? 4 : capacity() * 2;
				reserve(cp);
			}
			*_finish = x;
			_finish++;
		}

		iterator insert(iterator pos, const T& x)
		{
			assert(pos >= _start);
			assert(pos <= _finish);
			if (size() == capacity())
			{
				size_t len = pos - _start;
				//扩容后_start位置可能会发生改变了,因此要记录pos的相对位置,改变pos的值
				size_t cp = capacity() == 0 ? 4 : capacity() * 2;
				reserve(cp);
				pos = _start + len;
			}
			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				end--;
			}
			*pos = x;
			_finish++;
			return pos;
		}

		iterator erase(iterator pos)
		{
			assert(pos >= _start);
			assert(pos < _finish);
			iterator it = pos;
			while (it < _finish - 1)
			{
				*it = *(it + 1);
				++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;
		iterator _endofstorage = nullptr;
	};
}

测试一下,因为我们自己模拟实现的vector还没有添加右值引用版本的push_back,因此会发生深拷贝,如下 

 

那么我们添加上右值引用的push_back和insert再来试一下 

在vector.h添加如下代码(代码就是普通版本拷贝过来的,修改了参数类型为T&& x)

void push_back(T&& x)
{
	if (size() == capacity())
	{
		size_t cp = capacity() == 0 ? 4 : capacity() * 2;
		reserve(cp);
	}
	*_finish = x;
	_finish++;
}
iterator insert(iterator pos, T&& x)
{
	assert(pos >= _start);
	assert(pos <= _finish);
	if (size() == capacity())
	{
		size_t len = pos - _start;
		//扩容后_start位置可能会发生改变了,因此要记录pos的相对位置,改变pos的值
		size_t cp = capacity() == 0 ? 4 : capacity() * 2;
		reserve(cp);
		pos = _start + len;
	}
	iterator end = _finish - 1;
	while (end >= pos)
	{
		*(end + 1) = *end;
		end--;
	}
	*pos = x;
	_finish++;
	return pos;
}

 调试一下发现了问题,我们x是右值,进入push_back右值引用里没有问题,但是在下面赋值的地方,竟然会去深拷贝。这是为啥勒?string深拷贝的参数不是左值吗?

  • 这是因为被右值引用后,该值的属性会变为左值。C++11为何要这么设计?

大家看下面这个代码,这是我们之前写过的string的移动拷贝,按道理来说,s为右值,是无法被修改的,但这里我们竟然完成了swap交换函数,这下是不是可以讲得通了。不这样设计,右值引用就无法处理,引用来干啥?

那么针对不要进行深拷贝,要去移动拷贝的问题,我们解决起来也很简单,我们只需要给x  move一下,让他再变成右值就好了,因为你x本就是右值,就是将亡值,只是右值引用后变成了左值而已,我只是将你变成了本身的状态。

这样就变成了移动拷贝了。 

7.万能引用

大家看下面这段代码,第一时间感觉就是我们所学的右值引用,但其实不然。这里使用了模板,如果函数使用了模板参数,并且类型为T&& t。这种类型,那么这就是万能引用,左值和右值都会来调用这里。

 使用下面代码进行测试

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }

//万能引用
template<typename T>
void PerfectForward(T&& t)
{
	Fun(t);
}
int main()
{
	PerfectForward(10);           // 右值
	int a;
	PerfectForward(a);            // 左值
	PerfectForward(std::move(a)); // 右值
	const int b = 8;
	PerfectForward(b);      // const 左值
	PerfectForward(std::move(b)); // const 右值
	return 0;
}

我们发现打印的都是左值引用,这是因为 t 如果是左值,那么左值引用后也是左值,t 如果是右值,右值引用后也是左值,因此打印出来都是左值引用。

我们如何得到想要的效果呢? 这需要用到完美转发,使用forwad<T>(记住就好)

因此vector.h的这两个地方也可以修改为forward<T>(x),同时如果添加上template就可以删除掉拷贝构造了,因为有万能引用跟完美转发了。

总结

我们学习了C++11的两个较为重要的特性,统一的列表初始化,都可以用  {}  去赋值,还学习了右值引用,这里可能会有点啰嗦,因为知识点比较绕,也比较难。

最后附上文本的测试代码,如果是vs2019及以下,大家也可以看看效果(vs2022效果不一样)

vector.h如下 

#pragma once
namespace kky
{
	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>& v)
		{
			reserve(v.capacity());
			for (auto& e : v)
			{
				push_back(e);
			}
		}

		template <class InputIterator>
		vector(InputIterator first, InputIterator last)
		{
			while (first != last)
			{
				push_back(*first);
				first++;
			}
		}

		vector(int n, const T& val = T())
		{
			resize(n, val);
		}

		//vector(size_t n, const T& val = T())
		//{
		//	resize(n,val);
		//}

		vector<T>& operator=(vector<T> v)
		{
			swap(_start, v._start);
			swap(_finish, v._finish);
			swap(_endofstorage, v._endofstorage);
			return *this;
		}


		~vector()
		{
			delete[] _start;
			_start = _finish = _endofstorage = nullptr;
		}

		size_t size() const
		{
			return _finish - _start;
		}
		size_t capacity() const
		{
			return _endofstorage - _start;
		}

		void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t sz = size();
				T* tmp = new T[n];
				if (_start)
				{
					//memcpy对自定义类型是浅拷贝
					//memcpy(tmp, _start, sz * sizeof(T));
					//循环赋值,会调用自定义类型的赋值拷贝,就是深拷贝
					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())
		{
			if (n <= size())
			{
				_finish = _start + n;
			}
			else
			{
				reserve(n);
				for (size_t i = size(); i < n; i++)
				{
					_start[i] = val;
				}
				_finish = _start + n;
			}
		}


		template<class T>
		void push_back(T&& x)
		{
			if (size() == capacity())
			{
				size_t cp = capacity() == 0 ? 4 : capacity() * 2;
				reserve(cp);
			}
			*_finish = forward<T>(x);
			_finish++;
		}

		template<class T>
		iterator insert(iterator pos, T&& x)
		{
			assert(pos >= _start);
			assert(pos <= _finish);
			if (size() == capacity())
			{
				size_t len = pos - _start;
				//扩容后_start位置可能会发生改变了,因此要记录pos的相对位置,改变pos的值
				size_t cp = capacity() == 0 ? 4 : capacity() * 2;
				reserve(cp);
				pos = _start + len;
			}
			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				end--;
			}
			*pos = forward<T>(x);
			_finish++;
			return pos;
		}

		iterator erase(iterator pos)
		{
			assert(pos >= _start);
			assert(pos < _finish);
			iterator it = pos;
			while (it < _finish - 1)
			{
				*it = *(it + 1);
				++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;
		iterator _endofstorage = nullptr;
	};
}

test.cpp

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<vector>
#include<list>
#include<string>
#include<assert.h>
using namespace std;
#include"vector.h"
namespace bit
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			//cout << "string(char* str) -- 构造" << endl;

			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		// s1.swap(s2)
		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}

		// 拷贝构造
		string(const string& s)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;
			string tmp(s._str);
			swap(tmp);
			int a = 0;
		}

		string(string&& s)
		{
			cout << "string(string&& s) -- 移动拷贝" << endl;
			swap(s);
		}

		// 赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(const string& s) -- 深拷贝" << endl;
			/*string tmp(s);
			swap(tmp);*/
			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;
		}

		string& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) -- 移动拷贝" << endl;
			swap(s);
			return *this;
		}

		~string()
		{
			delete[] _str;
			_str = nullptr;
		}

		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}

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

				_capacity = n;
			}
		}

		void push_back(char ch)
		{
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}

			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}

		//string operator+=(char ch)
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0; // 不包含最后做标识的\0
	};

	bit::string to_string(int x)
	{
		bit::string ret;
		while (x)
		{
			int val = x % 10;
			x /= 10;
			ret += ('0' + val);
		}

		reverse(ret.begin(), ret.end());

		return ret;
	}
}

int main()
{
	kky::vector<bit::string> v;
	v.reserve(20);
	bit::string s1("hello world");
	v.push_back(s1);
	cout << endl;
	v.push_back(bit::to_string(1234));
}

//void Fun(int& x) { cout << "左值引用" << endl; }
//void Fun(const int& x) { cout << "const 左值引用" << endl; }
//void Fun(int&& x) { cout << "右值引用" << endl; }
//void Fun(const int&& x) { cout << "const 右值引用" << endl; }
//
万能引用
//template<typename T>
//void PerfectForward(T&& t)
//{
//	Fun(forward<T>(t));
//}
//int main()
//{
//	PerfectForward(10);           // 右值
//	int a;
//	PerfectForward(a);            // 左值
//	PerfectForward(std::move(a)); // 右值
//	const int b = 8;
//	PerfectForward(b);      // const 左值
//	PerfectForward(std::move(b)); // const 右值
//	return 0;
//}

谢谢大家观看!!! 

 

  • 18
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值