C++11新特性

作者:billy
版权声明:著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处

前言

C++11标准在2011年正式发布,相信使用过C++11新特性的小伙伴一定会被她的魅力深深地吸引,感觉是用C++写出了Python、写出了kotlin这种高级又简便的语言。博主个人非常喜欢11标准,为了方便个人查阅以及分享给好友,整理了一些新特性,若有错误欢迎指出。


C++11的设计目标

  • 使C++成为更好的适用于系统开发和库开发的语言
  • 使C++更易于教学,语法更加一致化和简单化
  • 保持语言稳定性,与03标准和C语言兼容

相较于98/03标准,11标准有什么显著增强?

  • Native Concurrency:内存模型,线程,原子操作
  • 统一对泛型编程的支持:统一初始化表达式,auto,decltype,移动语义
  • 更好的系统编程:constexpr(常量表达式)、POD
  • 更好的库开发:内联命名空间,继承构造函数,右值引用

C++11常用新特性

  1. nullptr 关键字
    nullptr 出现的目的是为了替代 NULL,那么为什么需要nullptr? NULL有什么毛病呢?来看一段代码:
class Test
{
public:
    void TestWork(int index)
    {
        std::cout << "TestWork 1" << std::endl;
    }
    void TestWork(int * index)
    {
        std::cout << "TestWork 2" << std::endl;
    }
};

int main()
{
    Test test;
    test.TestWork(NULL);	//输出 TestWork 1
    test.TestWork(nullptr);	//输出 TestWork 2
}

我们调用test.TestWork(NULL),其实期望是调用的是void TestWork(int * index),但结果调用了void TestWork(int index)。传统 C++ 会把 NULL、0 视为同一种东西,这取决于编译器如何定义 NULL,有些编译器会将 NULL 定义为 ((void*)0),有些则会直接将其定义为 0。
为了解决二义性这个问题,C++11 引入了 nullptr 关键字,专门用来区分空指针和0

  1. auto 关键字
    功能:auto用于从初始化表达式中推断出变量的数据类型,auto并没有让C++成为弱类型语言,也没有弱化变量什么,只是使用auto的时候,编译器根据上下文情况,确定auto变量的真正类型。

举例说明:

auto i = 5;             // i 被推导为 int
auto arr = new auto(10) // arr 被推导为 int *

使用auto进行类型推导最常见的例子就是迭代器:
没用auto之前这样写:for(vector<int>::const_iterator itr = vec.cbegin(); itr != vec.cend(); ++itr)
用了auto之后这样写:for(auto itr = vec.cbegin(); itr != vec.cend(); ++itr)

使用auto有几个点需要注意:

  • auto定义变量时必须初始化(类似引用)
  • auto作为函数返回值时,只能用于定义函数,不能用于声明函数
  • 不允许使用auto定义函数参数
  • 不允许使用auto定义struct/class的成员变量
  • 不允许使用auto定义数组
  • 不允许使用auto作为模板参数传递

auto总结

  • C++11之后的标准下,应该尽量多的使用auto
  • auto不会有任何的效率损失,都是基于编译期的推导
  • auto还会带来更好的安全性
  1. decltype 关键字
    decltype 关键字是为了解决 auto 关键字只能对变量进行类型推导的缺陷而出现的。它的用法和 sizeof 很相似 decltype(表达式)
    功能:编译器分析表达式并得到它的类型,却不实际计算表达式的值。
    注意:如果给变量加上了一层括号,编译器会把它当作一个表达式,得到的则是引用类型

举例说明:

int i = 10;
float j = 5.1;

decltype(i) a;		// a的类型是int
decltype((i)) b = i;  	// b的类型是int&,必须为其初始化,否则会编译报错
decltype(i+j) c; 	// c的类型是int+float => float
  1. 新式函数声明
    C++11 引入了一个叫做拖尾返回类型(trailing return type),利用 auto 关键字将返回类型后置。

举例说明:

template<typename T, typename U>
auto add(T x, U y) -> decltype(x+y) {
    return x+y;
}
  1. 基于范围的 for 循环
    C++11 引入了基于范围的迭代写法,我们拥有了能够写出像 Python 一样简洁的循环语句。

举例说明:

// 原先的for循环
std::vector<int> arr(5, 100);
for(std::vector<int>::iterator i = arr.begin(); i != arr.end(); ++i) {
    std::cout << *i << std::endl;
}

// 现在的for循环
for(auto &i : arr) {    
    std::cout << i << std::endl;
}
  1. Lambda 表达式
    Lambda 表达式,实际上就是提供了一个类似匿名函数的特性,而匿名函数则是在需要一个函数,但是又不想费力去命名一个函数的情况下去使用的。

lambda表达式定义格式:

[capture] (parameters) opt-> return-type {
  body
};

1. capture是捕获列表; 
    1).[]不捕获任何变量
    2).[&]捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获)
    3).[=]捕获外部作用域中所有变量,并作为副本在函数体中使用(按值捕获)
    4).[=,&foo]按值捕获外部作用域中所有变量,并按引用捕获foo变量
    5).[bar]按值捕获bar变量,同时不捕获其他变量
    6).[this]捕获当前类中的this指针,让lambda表达式拥有和当前类成员函数同样的访问权限
    如果已经使用了&或者=,就默认添加此选项。捕获this的目的是可以在lamda中使用当前类的成员函数和成员变量
2. parameters是参数表;(选填) 
3. opt是函数选项;可以填mutable,exception,attribute(选填) 
    1).mutable说明lambda表达式体内的代码可以修改被捕获的变量,并且可以访问被捕获的对象的non-const方法
    2).exception说明lambda表达式是否抛出异常以及何种异常
    3).attribute用来声明属性
4. return-type是返回值类型(拖尾返回类型)(选填) 
5. body是函数体

简单的例子:

auto func = [](int x, int y) -> int {
    return x + y;
};

cout << func(3, 4) << endl;	// 7
  1. std::function、std::bind封装可执行对象
    std::bind和std::function是从boost中移植进来的C++新标准,这两个语法使得封装可执行对象变得简单而易用。此外,std::bind和std::function也可以结合我们一下所说的lamda表达式一起使用,使得可执行对象的写法更加“花俏”。

举例说明:

Test.h

class Test
{
public:
    void Add(std::function<int(int, int)> fun, int a, int b)
    {
        int sum = fun(a, b);
        std::cout << "sum:" << sum << std::endl;
    }
};
main.cpp

int add(int a, int b)
{
    std::cout << "add" << std::endl;
    return a + b;
}

class TestAdd
{
public:
    int Add(int a, int b)
    {
        std::cout << "TestAdd::Add" << std::endl;
        return a + b;
    }
};

int main()
{
    Test test;
    test.Add(add, 1, 2);

    TestAdd testAdd;
    test.Add(std::bind(&TestAdd::Add, testAdd, std::placeholders::_1, std::placeholders::_2), 1, 2);
    return 0;
}
运行结果:
add
sum:3
TestAdd::Add
sum:3

解释:
std::bind第一个参数为对象函数指针,表示函数相对于类的首地址的偏移量;
testAdd为对象指针;
std::placeholders::_1和std::placeholders::_2为参数占位符,表示std::bind封装的可执行对象可以接受两个参数。
我们的Test函数在函数指针和类对象函数中,两种情况下都完美运行。
  1. 初始化列表
    C++11 提供了统一的语法来初始化任意的对象。

举例说明:

struct A {
	int a;
	float b;
};

struct B {
	B(int _a, float _b): a(_a), b(_b) {}
private:
	int a;
	float b;
};

A a {1, 1.1};    // 统一的初始化语法
B b {2, 2.2};

C++11 还把初始化列表的概念绑定到了类型上,并将其称之为 std::initializer_list,允许构造函数或其他函数像参数一样使用初始化列表,这就为类对象的初始化与普通数组和 POD 的初始化方法提供了统一的桥梁。

举例说明:

#include <initializer_list>

class Magic {
public:
    Magic(std::initializer_list<int> list) {}
};

Magic magic = {1,2,3,4,5};
std::vector<int> v = {1, 2, 3, 4};
  1. STL容器
  • std::array跟数组并没有太大区别,std::array相对于数组,增加了迭代器等函数。

简单的例子:

#include <array>
int main()
{
    std::array<int, 4> arrayDemo = { 1,2,3,4 };
    std::cout << "arrayDemo:" << std::endl;
    for (auto itor : arrayDemo)
    {
        std::cout << itor << std::endl;
    }
    int arrayDemoSize = sizeof(arrayDemo);
    std::cout << "arrayDemo size:" << arrayDemoSize << std::endl;
    return 0;
}

运行结果:
arrayDemo:
1
2
3
4
arrayDemo size:16
  • std::forward_list为C++新增的线性表,与list区别在于它是单向链表。我们在学习数据结构的时候都知道,链表在对数据进行插入和删除是比顺序存储的线性表有优势,因此在插入和删除操作频繁的应用场景中,使用list和forward_list比使用array、vector和deque效率要高很多。

简单的例子:

#include <forward_list>
int main()
{
    std::forward_list<int> numbers = {1,2,3,4,5,4,4};
    std::cout << "numbers:" << std::endl;
    for (auto number : numbers)
    {
        std::cout << number << std::endl;
    }
    numbers.remove(4);
    std::cout << "numbers after remove:" << std::endl;
    for (auto number : numbers)
    {
        std::cout << number << std::endl;
    }
    return 0;
}

运行结果:
numbers:
1
2
3
4
5
4
4
numbers after remove:
1
2
3
5
  • std::unordered_map与std::map用法基本差不多,但STL在内部实现上有很大不同,std::map使用的数据结构为二叉树,而std::unordered_map内部是哈希表的实现方式,哈希map理论上查找效率为O(1)。但在存储效率上,哈希map需要增加哈希表的内存开销。

简单的例子:

#include <iostream>
#include <string>
#include <unordered_map>
int main()
{
    std::unordered_map<std::string, std::string> mymap =
    {
        { "house","maison" },
        { "apple","pomme" },
        { "tree","arbre" },
        { "book","livre" },
        { "door","porte" },
        { "grapefruit","pamplemousse" }
    };
    unsigned n = mymap.bucket_count();
    std::cout << "mymap has " << n << " buckets.\n";
    for (unsigned i = 0; i<n; ++i) 
    {
        std::cout << "bucket #" << i << " contains: ";
        for (auto it = mymap.begin(i); it != mymap.end(i); ++it)
            std::cout << "[" << it->first << ":" << it->second << "] ";
        std::cout << "\n";
    }
    return 0;
}

运行结果:
mymap has 8 buckets.
bucket #0 contains: ["book","livre"]
bucket #1 contains: ["door","porte"]
bucket #2 contains: ["grapefruit","pamplemousse"]
bucket #3 contains: ["house","maison"]
bucket #4 contains:
bucket #5 contains: ["tree","arbre"]
bucket #6 contains: 
bucket #7 contains: "apple","pomme"[]
  • std::unordered_set的数据存储结构也是哈希表的方式结构,除此之外,std::unordered_set在插入时不会自动排序,这都是std::set表现不同的地方。

简单的例子:

#include <iostream>
#include <string>
#include <unordered_set>
#include <set>
int main()
{
    std::unordered_set<int> unorder_set;
    unorder_set.insert(7);
    unorder_set.insert(5);
    unorder_set.insert(3);
    unorder_set.insert(4);
    unorder_set.insert(6);
    std::cout << "unorder_set:" << std::endl;
    for (auto itor : unorder_set)
    {
        std::cout << itor << std::endl;
    }

    std::set<int> set;
    set.insert(7);
    set.insert(5);
    set.insert(3);
    set.insert(4);
    set.insert(6);
    std::cout << "set:" << std::endl;
    for (auto itor : set)
    {
        std::cout << itor << std::endl;
    }
}

运行结果:
unorder_set:
7
5
3
4
6
set:
3
4
5
6
7
  1. 多线程
  • std::thread为C++11的线程类,使用方法和boost接口一样非常方便,同时得益于C++11的可变参数的设计风格,C++11的std::thread还解决了boost::thread中构成参数限制的问题。

简单的例子:

#include <thread>
void threadfun1()
{
    std::cout << "threadfun1 - 1\r\n" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "threadfun1 - 2" << std::endl;
}

void threadfun2(int iParam, std::string sParam)
{
    std::cout << "threadfun2 - 1" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(5));
    std::cout << "threadfun2 - 2" << std::endl;
}

int main()
{
    std::thread t1(threadfun1);
    std::thread t2(threadfun2, 10, "abc");
    t1.join();		// 等待线程 t1 执行完毕
    std::cout << "join" << std::endl;
    t2.detach();	// 将线程 t2 与主线程分离
    std::cout << "detach" << std::endl;
}

运行结果:
threadfun1 - 1
threadfun2 - 1

threadfun1 - 2
join
detach

根据输出结果可以得知,t1.join()会等待t1线程退出后才继续往下执行,t2.detach()并不会等待,detach字符输出后,主函数退出,threadfun2还未执行完成,但是在主线程退出后,t2的线程也被已经被强退出。

  • std::atomic为C++11封装的原子数据类型
    这里解释一下何为原子数据类型,在多线程中用户不必对其添加互斥资源锁的类型,其内部已自己加了锁。(这里不举例说明了,可以自己尝试一下)

  • C++11中的std::condition_variable就像Linux下使用pthread_cond_wait和pthread_cond_signal一样,可以让线程休眠,直到被唤醒再从新执行。线程等待在多线程编程中使用非常频繁,经常需要等待一些异步执行的条件的返回结果。

C++11官网的例子:

#include <iostream>           // std::cout
#include <thread>             // std::thread
#include <mutex>              // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void print_id(int id) {
    std::unique_lock<std::mutex> lck(mtx);
    while (!ready) cv.wait(lck);
    // ...
    std::cout << "thread " << id << '\n';
}

void go() {
    std::unique_lock<std::mutex> lck(mtx);
    ready = true;
    cv.notify_all();
}

int main()
{
    std::thread threads[10];
    // spawn 10 threads:
    for (int i = 0; i<10; ++i)
        threads[i] = std::thread(print_id, i);
        
    std::cout << "10 threads ready to race...\n";
    go();                       // go!
    
    for (auto& th : threads) th.join();

    return 0;
}

以上的代码,在12行中调用cv.wait(lck)的时候,线程将进入休眠。
在调用31行的go函数之前,10个线程都处于休眠状态。
当20行的cv.notify_all()运行后,12行的休眠将结束,继续往下运行,最终输出如上结果。
  1. 智能指针内存管理
    智能指针是用对象去管理一个资源指针,同时用一个计数器计算当前指针引用对象的个数,当管理指针的对象增加或减少时,计数器也相应加1或减1,当最后一个指针管理对象销毁时,计数器为1,此时在销毁指针管理对象的同时,也把指针管理对象所管理的指针进行delete操作。这里介绍一下两个智能指针std::shared_ptr、std::weak_ptr的用法。
  • std::shared_ptr包装了new操作符动态分别的内存,可以自由拷贝复制,基本上是使用最多的一个智能指针类型。

简单的例子:

#include <memory>
class Test
{
public:
    Test()
    {
        std::cout << "Test()" << std::endl;
    }
    ~Test()
    {
        std::cout << "~Test()" << std::endl;
    }
};
int main()
{
    std::shared_ptr<Test> p1 = std::make_shared<Test>();
    std::cout << "1 ref:" << p1.use_count() << std::endl;
    {
        std::shared_ptr<Test> p2 = p1;
        std::cout << "2 ref:" << p1.use_count() << std::endl;
    }
    std::cout << "3 ref:" << p1.use_count() << std::endl;
    return 0;
}

运行结果:
Test()
1 ref:1
2 ref:2
3 ref:1
~Test()
  • 对于std::weak_ptr,很多人说其实是为了解决std::shared_ptr在相互引用的情况下出现的问题而存在的。C++官网对这个智能指针的解释也不多,那就暂时接受这个观点吧。
    std::weak_ptr与std::shared_ptr最大的差别就是在赋值时不会引起智能指针计数增加

一个智能指针相互引用的例子:

#include <memory>
class TestB;
class TestA
{
public:
    TestA()
    {
        std::cout << "TestA()" << std::endl;
    }
    void ReferTestB(std::shared_ptr<TestB> test_ptr)
    {
        m_TestB_Ptr = test_ptr;
    }
    void TestWork()
    {
        std::cout << "~TestA::TestWork()" << std::endl;
    }
    ~TestA()
    {
        std::cout << "~TestA()" << std::endl;
    }
private:
    std::weak_ptr<TestB> m_TestB_Ptr;
};

class TestB
{
public:
    TestB()
    {
        std::cout << "TestB()" << std::endl;
    }

    void ReferTestB(std::shared_ptr<TestA> test_ptr)
    {
        m_TestA_Ptr = test_ptr;
    }
    void TestWork()
    {
        std::cout << "~TestB::TestWork()" << std::endl;
    }
    ~TestB()
    {
        std::shared_ptr<TestA> tmp = m_TestA_Ptr.lock();
        tmp->TestWork();
        std::cout << "2 ref a:" << tmp.use_count() << std::endl;
        std::cout << "~TestB()" << std::endl;
    }
    std::weak_ptr<TestA> m_TestA_Ptr;
};

int main()
{
    std::shared_ptr<TestA> ptr_a = std::make_shared<TestA>();
    std::shared_ptr<TestB> ptr_b = std::make_shared<TestB>();
    ptr_a->ReferTestB(ptr_b);
    ptr_b->ReferTestB(ptr_a);
    std::cout << "1 ref a:" << ptr_a.use_count() << std::endl;
    std::cout << "1 ref b:" << ptr_a.use_count() << std::endl;
    return 0;
}

运行结果:
TestA()
TestB()
1 ref a:1
1 ref b:1
~TestA::TestWork()
2 ref a:2
~TestB()
~TestA()
  1. 右值引用 与 std::move
    在说右值引用之前,先来说一下什么是左值什么是右值:
    左值的定义:非临时对象
    右值的定义:临时的对象

简答的例子:

int i = 0;  		// 在这条语句中,i 是左值,0 是临时值,就是右值。
A a =  getA();		// 在这条语句中,a 是左值,getA()的返回值是右值。

右值引用 (Rvalue Referene) 实现了转移语义 (Move Sementics) 和精确传递 (Perfect Forwarding),它的主要目的有两个方面:
1)交互时不必要的对象拷贝,节省运算存储资源,提高效率
2)洁明确地定义泛型函数

简单的例子:

#include "stdafx.h"
#include <iostream>
#include <vector>

class B
{
public:
    B() {}
    B(const B&) { std::cout << "B Constructor" << std::endl; }
};

class A
{
public:
    A(): m_b(new B()) { std::cout << "A Constructor" << std::endl; }
    A(const A& src) :
        m_b(new B(*(src.m_b)))
    { 
        std::cout << "A Copy Constructor" << std::endl;
    }
    A(A&& src) :
        m_b(src.m_b)
    {
        src.m_b = nullptr;
        std::cout << "A Move Constructor" << std::endl;
    }
    ~A() { delete m_b; }

private:
    B* m_b;
};

static A getA()
{
    A a;
    std::cout << "================================================" << std::endl;
    return a;
}

int main()
{
    A a = getA();
    std::cout << "================================================" << std::endl;
    A a1(a);
    
    return 0;
}

运行结果:
A Constructor
================================================
A Move Constructor
================================================
B Constructor
A Copy Constructor

解析:
A a  =  getA(); 调用的是A的移动构造,而A a1(a); 调用的是A的拷贝构造。
A的拷贝构造需要对成员变量B进行深拷贝,而A的移动构造不需要,很明显,A的移动构造效率高。

std::move语句可以将左值变为右值而避免拷贝构造

简单的例子:

#include "stdafx.h"
#include <iostream>
#include <vector>

class B
{
public:
    B() {}
    B(const B&) { std::cout << "B Constructor" << std::endl; }
};

class A
{
public:
    A(): m_b(new B()) { std::cout << "A Constructor" << std::endl; }
    A(const A& src) :
        m_b(new B(*(src.m_b)))
    { 
        std::cout << "A Copy Constructor" << std::endl;
    }
    A(A&& src) noexcept :
        m_b(src.m_b)
    {
        src.m_b = nullptr;
        std::cout << "A Move Constructor" << std::endl;
    }
    ~A() { delete m_b; }

private:
    B* m_b;
};

static A getA()
{
    A a;
    std::cout << "================================================" << std::endl;
    return a;
}

int main()
{
    A a = getA();
    std::cout << "================================================" << std::endl;
    A a1(a);
    std::cout << "================================================" << std::endl;
    A a2(std::move(a1));
    return 0;
}

运行结果:
A Constructor
================================================
A Move Constructor
================================================
B Constructor
A Copy Constructor
================================================
A Move Constructor

解析:
 A a2(std::move(a1)); 将a1转换为右值,因此a2调用的移动构造而不是拷贝构造。
  1. 模板增强
  • 外部模板
    传统 C++ 中,模板只有在使用时才会被编译器实例化。只要在每个编译单元(文件)中编译的代码中遇到了被完整定义的模板,都会实例化。这就产生了重复实例化而导致的编译时间的增加。并且,我们没有办法通知编译器不要触发模板实例化。
    C++11 引入了外部模板,扩充了原来的强制编译器在特定位置实例化模板的语法,使得能够显式的告诉编译器何时进行模板的实例化

简单的例子:

template class std::vector<bool>;            	// 强行实例化
extern template class std::vector<double>;  	// 不在该编译文件中实例化模板
  • 类型别名模板
    在传统 C++中,typedef 可以为类型定义一个新的名称,但是却没有办法为模板定义一个新的名称。因为,模板不是类型。

简单的例子:

template< typename T, typename U, int value>
class SuckType {
public:
    T a;
    U b;
    SuckType():a(value),b(value){}
};
template< typename U>
typedef SuckType<std::vector<int>, U, 1> NewType; // 不合法

C++11 使用 using 引入了下面这种形式的写法,并且同时支持对传统 typedef 相同的功效

简单的例子:

template <typename T>
using NewType = SuckType<int, T, 1>;    // 合法
  • 默认模板参数
    在 C++11 中可以指定模板的默认参数:

简单的例子:

template<typename T = int, typename U = int>
auto add(T x, U y) -> decltype(x+y) {
    return x+y;
}
  1. 构造函数
  • 委托构造
    C++11 引入了委托构造的概念,这使得构造函数可以在同一个类中一个构造函数调用另一个构造函数,从而达到简化代码的目的:

简单的例子:

class Base {
public:
    int value1;
    int value2;
    Base() {
        value1 = 1;
    }
    Base(int value) : Base() {  // 委托 Base() 构造函数
        value2 = 2;
    }
};
  • 继承构造
    在继承体系中,如果派生类想要使用基类的构造函数,需要在构造函数中显式声明。
    假若基类拥有为数众多的不同版本的构造函数,这样在派生类中得写很多对应的“透传”构造函数。

简单的例子:

struct A
{
  A(int i) {}
  A(double d,int i){}
  A(float f,int i,const char* c){}
  //...等等系列的构造函数版本
};
struct B:A
{
  B(int i):A(i){}
  B(double d,int i):A(d,i){}
  B(folat f,int i,const char* c):A(f,i,e){}
  //......等等好多个和基类构造函数对应的构造函数
};

C++11的继承构造
简单的例子:

struct A
{
  A(int i) {}
  A(double d,int i){}
  A(float f,int i,const char* c){}
  //...等等系列的构造函数版本
};
struct B:A
{
  using A::A;
  //关于基类各构造函数的继承一句话搞定
  //......
};
  1. 虚函数的 override 和 final 指示符
    override 和 final 在java、korlin中经常被用到,现在C++11标准中也可以使用了。
  • override,表示函数应当重写基类中的虚函数(VS2010目前支持)
  • final,表示派生类不应当重写这个虚函数(VS2010目前不支持)
  1. static_assert() 静态断言
  • C提供的assert()只能在运行时断言
  • C++11/14 提供的static_assert()可以在编译期断言
结尾

暂时先整理这些,以后用到了再加进来。。。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值