C++(21):特殊工具与技术

控制内存分配

某些应用程序对内存分配有特殊需求,无法直接应用标准内存管理机制。需要自定义内存分配的细节。

重载 new 和 delete

void* operator new(std::size_t size) {
    // 自定义内存分配逻辑
    void* ptr = std::malloc(size);
    if (!ptr) {
        throw std::bad_alloc(); // 内存分配失败时抛出异常
    }
    return ptr;
}

上述代码中,重载了new运算符,std::size_t size参数表示要分配的字节数。在这个函数中,可以实现自己的内存分配逻辑,例如使用malloc来分配内存。如果分配失败,通常会抛出std::bad_alloc异常。

void operator delete(void* ptr) noexcept {
    // 自定义内存释放逻辑
    std::free(ptr);
}

重载了delete运算符,接受一个指向要释放内存的指针。在这个函数中,可以实现自己的内存释放逻辑,例如使用free来释放内存。

mallocfree 函数定义在头文件<cstdlib>中。

定位 new 表达式

定位 new 允许在已分配的内存块上创建对象,而不是使用默认的内存分配方式。通常,它用于在已分配的内存上构造对象,例如在内存池或特定内存区域中。

new (place_address) type 
new (place_address) type (initializers)
new (place_address) type [size]
new (place_address) type [size] { braced initializer list }

其中 place_address必须是一个指针,同时在initializers中提供一个(可能为空的)以逗号分隔的初始值列表,该初始值列表将用于构造新分配的对象。

当只传入一个指针类型的实参时,定位new表达式构造对象但是不分配内存。

#include <iostream>

class MyClass {
public:
    MyClass(int val) : value(val) {}
    void Print() {
        std::cout << "Value: " << value << std::endl;
    }
private:
    int value;
};

int main() {
    // 分配一块内存用于存储 MyClass 对象
    void* memory = ::operator new(sizeof(MyClass));
    
    // 使用定位 new 表达式在已分配的内存上创建对象
    MyClass* obj = new (memory) MyClass(42);

    // 访问对象的方法
    obj->Print();

    // 使用定位 delete 表达式释放对象
    obj->~MyClass();

    // 释放内存块
    ::operator delete(memory);

    return 0;
}

运行时类型识别

运行时类型识别(run-time type identification,RTTI)的功能由两个运算符实现:
typeid运算符,用于返回表达式的类型。
dynamic_cast运算符,用于将基类的指针或引用安全地转换成派生类的指针或引用。
当我们将这两个运算符用于某种类型的指针或引用,并且该类型含有虚函数时,运算符将使用指针或引用所绑定对象的动态类型。

dynamic_cast 运算符

dynamic_cast运算符的使用形式如下所示:

dynamic_cast<type*>(e)
dynamic_cast<type&>(e)
dynamic_cast<type&&>(e)

其中,type必须是一个类类型,并且通常情况下该类型应该含有虚函数。在第一种形式中,e必须是一个有效的指针;在第二种形式中,e必须是一个左值;在第三种形式中,e不能是左值。

dynamic_cast 运算符用于在继承层次结构中进行安全的向下转型。它可以将基类指针或引用转换为派生类指针或引用,并且在类型不匹配时返回 nullptr(对指针)或引发 std::bad_cast 异常(对引用)。

#include <iostream>

class Base {
public:
    virtual ~Base() {}
};

class Derived : public Base {
public:
    void DerivedFunction() {
        std::cout << "DerivedFunction called." << std::endl;
    }
};

int main() {
    Base* basePtr = new Derived;

    Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
    if (derivedPtr) {
        derivedPtr->DerivedFunction();
    } else {
        std::cout << "无法进行向下转型。" << std::endl;
    }

    delete basePtr;

    return 0;
}

//dynamic_cast 运算符用于将 basePtr 转换为 Derived* 类型的指针,如果转换成功,就可以调用 Derived 类的成员函数。

typeid 运算符

typeid表达式的形式是typeid(e),其中e可以是任意表达式或类型的名字。
typeid操作的结果是一个常量对象的引用,该对象的类型是标准库类型type_info或者type_info的公有派生类型。

typeid运算符可以作用于任意类型的表达式。

当运算对象不属于类类型或者是一个不包含任何虚函数的类时,typeid运算符指示的是运算对象的静态类型。而当运算对象是定义了至少一个虚函数的类的左值时,typeid的结果直到运行时才会求得。

typeid 运算符允许获取对象的实际类型信息,以便在运行时判断对象的类型。它通常与 std::type_info 类一起使用。

#include <iostream>
#include <typeinfo>

class Base {
public:
    virtual ~Base() {}
};

class Derived : public Base {
};

int main() {
    Base* basePtr = new Derived;

    if (typeid(*basePtr) == typeid(Derived)) {
        std::cout << "basePtr指向的对象是Derived类型" << std::endl;
    } else if (typeid(*basePtr) == typeid(Base)) {
        std::cout << "basePtr指向的对象是Base类型" << std::endl;
    }

    delete basePtr;

    return 0;
}

使用 RTTI

运行时类型识别(RTTI)可以用来获取对象的实际类型信息,并在运行时根据对象的类型进行不同的处理。

#include <iostream>
#include <typeinfo>

class Animal {
public:
    virtual void Speak() const {
        std::cout << "Animal speaks." << std::endl;
    }
};

class Dog : public Animal {
public:
    void Speak() const override {
        std::cout << "Dog barks." << std::endl;
    }
};

class Cat : public Animal {
public:
    void Speak() const override {
        std::cout << "Cat meows." << std::endl;
    }
};

int main() {
    Animal* animals[3];
    animals[0] = new Dog();
    animals[1] = new Cat();
    animals[2] = new Animal();

    for (int i = 0; i < 3; i++) {
        const std::type_info& typeInfo = typeid(*animals[i]);
        
        if (typeInfo == typeid(Dog)) {
            std::cout << "This is a Dog. ";
        } else if (typeInfo == typeid(Cat)) {
            std::cout << "This is a Cat. ";
        } else {
            std::cout << "This is an unknown animal. ";
        }
        
        animals[i]->Speak();
    }

    for (int i = 0; i < 3; i++) {
        delete animals[i];
    }

    return 0;
}

//在上述示例中,我们创建了一个基类Animal和两个派生类Dog和Cat。然后,我们创建了一个包含不同类型的Animal指针的数组,
//并使用typeid运算符获取对象的类型信息。最后,我们根据类型信息执行不同的操作。

//要注意的是,typeid返回的是std::type_info类型,可以用来进行类型比较。
//另外,为了使类型比较有效,Animal类中的Speak函数被声明为虚函数,以启用多态性。

尽管RTTI可以实现运行时类型检查和操作,但通常应该避免使用它,因为它可能会引入性能开销,并且有时更好的做法是使用多态性和虚函数来避免需要RTTI。 RTTI通常在某些特定的情况下才会用到。

type_info 类

type_info类的精确定义随着编译器的不同而略有差异。
它用于表示类型信息。std::type_info 的对象通常由运行时类型识别(RTTI)操作 typeid 返回。它包含有关一个类型的信息,例如类型的名称或类型的标识。

1.获取类型信息:std::type_info 通常通过 typeid 运算符来获取类型信息。

const std::type_info& typeInfo = typeid(MyClass);

2.比较类型信息:可以使用 std::type_info 对象来比较类型信息。

const std::type_info& typeInfo1 = typeid(MyClass1);
const std::type_info& typeInfo2 = typeid(MyClass2);

if (typeInfo1 == typeInfo2) {
    // 类型相同
} else {
    // 类型不同
}

3.获取类型名称:std::type_info 对象可以用于获取类型的名称。

const std::type_info& typeInfo = typeid(MyClass);
std::cout << "Type name: " << typeInfo.name() << std::endl;

4.比较类型信息的安全性:std::type_info 对象的比较是类型安全的,即使在继承层次结构中也可以正常工作。这对于执行基于多态性的类型检查非常有用。

Base* basePtr = new Derived;
const std::type_info& baseTypeInfo = typeid(Base);
const std::type_info& derivedTypeInfo = typeid(*basePtr);

if (derivedTypeInfo == baseTypeInfo) {
    // 此代码块不会执行,因为类型不同
}

枚举类型

枚举类型可以将一组整型常量组织在一起。和类一样,每个枚举类型定义了一种新的类型。枚举属于字面值常量类型。

C++包含两种枚举:限定作用域的和不限定作用域的。

定义限定作用域的枚举类型:首先关键字 enum class(或等价使用 enum struct),随后是枚举类型名字以及用花括号括起来以逗号分隔的枚举成员列表,最后是一个分号:enum class open_modes {input,output,append) ;

定义不限定作用域的枚举类型:省略掉关键字 class(或 struct),枚举类型的名字是可选的:

enum color{red, yellow, green};	//不限定作用域的枚举类型
//未命名的、不限定作用域的枚举类型
enum (floatPrec = 6, doublePrec = 10,double_doublePrec = 10);

枚举成员

在限定作用域的枚举类型中,枚举成员的名字遵循常规的作用域准则,并且在枚举类型的作用域外是不可访问的。
与之相反,在不限定作用域的枚举类型中,枚举成员的作用域与枚举类型本身的作用域相同。

默认情况下,枚举值从0开始,依次加1。不过也能为一个或几个枚举成员指定专门的值。

枚举成员是const,因此在初始化枚举成员时提供的初始值必须是常量表达式。
可以定义枚举类型的constexpr变量:

constexpr intTypes charbits = intTypes::charTyp;

switch 语句中 case 标签的值必须是常量表达式,可以用枚举成员做 case 标签。

和类一样,枚举也定义新的类型

只要enum有名字,我们就能定义并初始化该类型的成员。要想初始化enum对象或者为enum对象赋值,必须使用该类型的一个枚举成员或者该类型的另一个对象。

一个不限定作用域的枚举类型的对象或枚举成员自动地转换成整型。

枚举类型的前置声明

可以提前声明 enum,但是不限定作用域的枚举类型在声明时必须指定成员类型。
enum 的声明和定义的成员类型必须匹配。

enum class Color1;    // 前置声明 Color1,成员类型默认为 int
enum uid:long long;   // 前置声明 uid,必须指定成员类型

类成员指针

成员指针是指可以指向类的非静态成员的指针。一般情况下,指针指向一个对象,但是成员指针指示的是类的成员,而非类的对象。
类的静态成员不属于任何对象,因此无须特殊的指向静态成员的指针,指向静态成员的指针与普通指针没有什么区别。

数据成员指针

class MyClass {
public:
    int data;
    void Print() {
        std::cout << "Data: " << data << std::endl;
    }
};

int main() {
    int MyClass::*memberPtr = &MyClass::data;

    MyClass obj;
    obj.*memberPtr = 42;
    std::cout << obj.data << std::endl;

    return 0;
}

//`定义了一个指向 MyClass 类的 data 数据成员的类成员指针 int MyClass::*memberPtr。
//然后,创建了一个 MyClass 对象 obj,并使用类成员指针来访问和修改 data 数据成员。`

成员函数指针

class MyClass {
public:
    void PrintHello() {
        std::cout << "Hello from MyClass" << std::endl;
    }
};

int main() {
    void (MyClass::*memberFunctionPtr)() = &MyClass::PrintHello;

    MyClass obj;
    (obj.*memberFunctionPtr)();

    return 0;
}

//定义了一个指向 MyClass 类的 PrintHello 成员函数的类成员指针 void (MyClass::*memberFunctionPtr)()。
//然后,创建了一个 MyClass 对象 obj,并使用类成员指针来调用 PrintHello 成员函数。

类成员指针的语法可以看起来比较复杂,但它们提供了一种强大的机制,用于在运行时动态选择要调用的成员函数或访问的数据成员,这在某些高级编程场景中非常有用。需要注意的是,类成员指针的类型与要指向的成员的类型有关,因此需要确保它们的类型匹配。

嵌套类

一个类可以定义在另一个类的内部,称之为嵌套类。
嵌套类可以访问外部类的私有成员,并且通常用于实现一种封装和组织的方式,以将相关联的类放在一起。嵌套类的一个常见用途是作为外部类的辅助类,用于实现某些功能。

嵌套类是一个独立的类,与外层类基本没什么关系。特别是,外层类的对象和嵌套类的对象是相互独立的。在嵌套类的对象中不包含任何外层类定义的成员:类似的,在外层类的对象中也不包含任何嵌套类定义的成员。

嵌套类的名字在外层类作用域中是可见的,在外层类作用域之外不可见。和其他嵌套的名字一样,嵌套类的名字不会和别的作用域中的同一个名字冲突。

#include <iostream>

class OuterClass {
public:
    // 外部类的构造函数
    OuterClass(int value) : data(value) {}

    // 嵌套类的定义
    class NestedClass {
    public:
        NestedClass(int nestedValue) : nestedData(nestedValue) {}

        void Display() {
            std::cout << "Nested Data: " << nestedData << std::endl;
        }
    private:
        int nestedData;
    };

    void AccessNestedClass() {
        NestedClass nested(42);
        nested.Display();
    }

private:
    int data;
};

int main() {
    OuterClass outer(10);
    outer.AccessNestedClass();

    return 0;
}

//定义了一个外部类 OuterClass 和一个嵌套类 NestedClass。嵌套类 NestedClass 可以访问外部类 OuterClass 的私有成员,例如 data。在 AccessNestedClass 成员函数中,创建了一个 NestedClass 的对象并访问了它的成员函数。

在嵌套类在其外层类之外完成真正的定义之前,它都是一个不完全类型.

union:一种节省空间的类

联合(union)是一种特殊的类。一个union可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。
1.union 不能含有引用类型的成员
2.union 不能含有虚函数。因为,union既不能继承自其他类,也不能作为基类使用。
在C++11新标准中,含有构造函数或析构函数的类类型也可以作为union的成员类型。union可以为其成员指定publicprotectedprivate等保护标记。默认情况下,union 的成员都是公有的,与struct相同。

定义 union 类型

union 用来定义一组类型不同的互斥值。

// MyUnion 类型的对象只有一个成员,该成员的类型可能是下列类型中的任意一种
union MyUnion {
    int intValue;
    float floatValue;
    char charValue;
};

使用 union 类型

默认情况下union 是未初始化的。
可以使用花括号内的初始值来初始化一个 union,像聚合类一样。
提供的初始值被用于初始化 union 的第一个成员。

使用通用的成员访问运算符来访问一个 union 对象的成员。
union 的一个数据成员赋值会令其他成员变为未定义状态。

MyUnion u;
u.intValue = 42;

std::cout << "intValue: " << u.intValue << std::endl;
std::cout << "floatValue: " << u.floatValue << std::endl; // 未定义行为
std::cout << "charValue: " << u.charValue << std::endl;   // 未定义行为

匿名 union

匿名union 是一个未命名的union,并且在右花括号和分号之间没有任何声明。
在匿名union的定义所在的作用域内该union的成员都是可以直接访问的,因此匿名union不能包含受保护的成员或私有成员,也不能定义成员函数。

含有类类型成员的 union

C++ 允许在联合(union)中包含类类型的成员。这样的联合可以存储不同类类型的对象,但要注意管理它们的生命周期以避免资源泄漏。

#include <iostream>

class A {
public:
    A(int value) : data(value) {}
    void Print() {
        std::cout << "A: " << data << std::endl;
    }
private:
    int data;
};

class B {
public:
    B(float value) : data(value) {}
    void Print() {
        std::cout << "B: " << data << std::endl;
    }
private:
    float data;
};

union MyUnion {
    A aObj;
    B bObj;
};

int main() {
    MyUnion u;

    u.aObj = A(42);
    u.aObj.Print();

    u.bObj = B(3.14f);
    u.bObj.Print();

    // 在联合中切换到不同的类对象类型
    u.aObj = A(100);
    u.aObj.Print();

    return 0;
}

//定义了两个类 A 和 B,它们分别具有不同的数据成员和成员函数。
//然后,创建了一个联合 MyUnion,它包含两个类类型的成员 aObj 和 bObj。

//首先将 aObj 设置为 A 类对象,并调用其 Print 成员函数来显示数据。
//然后将 bObj 设置为 B 类对象,并调用其 Print 成员函数。
//请注意,每次切换联合的成员对象时,上一个成员对象的析构函数会被调用,因此需要小心管理对象的生命周期。

使用类类型的成员的联合通常要更加谨慎,因为需要确保在切换成员对象时正确管理资源和对象的状态。联合通常用于特定的底层编程场景,如需要在不同的类对象之间共享内存或进行位操作时。在一般情况下,最好使用结构体或类来更安全地组织数据。

局部类

定义在某个函数内部的类称为局部类。局部类定义的类型只在定义它的作用域内可见。

局部类的所有成员(包括函数在内)都必须完整定义在类的内部。在局部类中也不允许声明静态数据成员。

局部类不能使用函数作用域中的变量。
局部类只能访问外层作用域定义的类型名、静态变量以及枚举成员。如果局部类定义在某个函数内部,则该函数的普通局部变量不能被该局部类使用。

外层函数对局部类的私有成员没有任何访问特权。当然,局部类可以将外层函数声明为友元;或者更常见的情况是局部类将其成员声明成公有的。

嵌套的局部类
局部类中可以再嵌套一个类,此时嵌套类的定义可以出现在局部类之外,不过必须定义在与局部类相同的作用域中。
局部类中的嵌套类也是一个局部类,必须遵循局部类的各种规定。

固有的不可移植的特性

不可移植的特性是指因机器而异的特性。

位域

类可以将其(非静态)数据成员定义成位域,在一个位域中含有一定数量的二进制位。当一个程序需要向其他程序或硬件设备传递二进制数据时,通常会用到位域。
位域在内存中的布局是与机器相关的。

位域的类型必须是整型或枚举类型。因为带符号位域的行为是由具体实现确定的,所以在通常情况下我们使用无符号类型保存一个位域。
位域的声明形式是在成员名字之后紧跟一个冒号以及一个常量表达式,该表达式用于指定成员所占的二进制位数:

#include <iostream>

struct Flags {
    unsigned int flag1 : 1;  // 1位
    unsigned int flag2 : 2;  // 2位
    unsigned int flag3 : 3;  // 3位
};

int main() {
    Flags flags;

    flags.flag1 = 1;   // 设置第1位为1
    flags.flag2 = 2;   // 设置第2、3位为10
    flags.flag3 = 5;   // 设置第4、5、6位为101

    std::cout << "flag1: " << flags.flag1 << std::endl;
    std::cout << "flag2: " << flags.flag2 << std::endl;
    std::cout << "flag3: " << flags.flag3 << std::endl;

    return 0;
}

//定义了一个结构体 Flags,它包含了三个位域成员 flag1、flag2 和 flag3,它们分别具有1位、2位和3位。
//可以使用赋值操作符来设置每个位域的值,并使用位域名称来访问它们。
  1. 位域成员的位数通常不能超过基础整数类型(如 int)的位数。例如,在大多数平台上,int 是32位,所以位域成员的位数应小于或等于32。
  2. 位域成员的符号(有符号或无符号)由编译器决定。如果你需要确切的符号性,可以使用 signed 或 unsigned 修饰符。
  3. 不同编译器的位域布局规则可能不同,所以在跨平台开发时要小心使用位域。
  4. 位域的可移植性较差,因为它们受限于底层硬件和编译器的实现。如果需要高度可移植的代码,最好避免使用位域。
  5. 位域通常用于表示标志或状态位,以节省内存。但要小心位域的复杂性和可移植性,确保在需要时使用位域。
  6. 取地址运算符(&)不能作用于位域,因此任何指针都无法指向类的位域。

volatile 限定符

关键字 volatile 告诉编译器不要对它所修饰的对象进行优化。

volatile int display_register;	//该int值可能发生改变
volatile Task *curr_task;		//curr_task指向一个volatile对象
volatile int iax[max_size];		//iax的每个元素都是volatile
volatile screen bitmapBuf;		//bitmapBuf的每个成员都是volatile

constvolatile限定符互相没什么影响,某种类型可能既是 const的也是volatile的,此时它同时具有二者的属性。
也可以将成员函数定义为 volatile,只有 volatile 的成员函数才能被 volatile 的对象调用。

可以声明volatile指针、指向volatile对象的指针以及指向volatile对象的volatile指针:

volatile int v;		//v是一个volatile int
int *volatile vip;		//vip是一个volatile指针,它指向int
volatile int *ivp;		//ivp是一个指针,它指向一个volatile int
volatile int *volatile vivp; //vivp是一个volatile指针,它指向一个volatile int

int *ip = &v;

合成的拷贝对 volatile 对象无效

不能使用合成的拷贝/移动构造函数及赋值运算符初始化 volatile 对象或从 volatile 对象赋值。
因为合成的成员接受的形参类型是(非volatile)常量引用,不能把一个非 volatile引用绑定到一个volatile对象上。

如果一个类希望拷贝、移动或赋值它的 volatile对象,则该类必须自定义拷贝或移动操作。

class Foo {
public:
    Foo(const volatile Foo&);						// 从一个 volatile 对象进行拷贝
    Foo& operator=(const volatile Foo&);			// 将一个 volatile 对象赋值给一个非 volatile 对象
    Foo& operator=(const volatile Foo&) volatile;	// 将一个 volatile 对象赋值给一个 volatile 对象
}

链接指示:extern “C”

其他语言中的函数名字也必须在C++中进行声明,并且该声明必须指定返回类型和形参列表。对于其他语言编写的函数来说,编译器检查其调用的方式与处理普通C++函数的方式相同,但是生成的代码有所区别。
C++使用链接指示指出任意非C++函数所用的语言。

要想把C++代码和其他语言(包括C语言)编写的代码放在一起使用,要求我们必须有权访问该语言的编译器,并且这个编译器与当前的C++编译器是兼容的。

声明非 C++ 函数

链接指示可以有两种形式:单个的或复合的。链接指示不能出现在类定义或函数定义的内部。同样的链接指示必须在函数的每个声明中都出现。

//可能出现在C++头文件<cstring>中的链接指示
//单语句链接指示
extern "C" size_t strlen (const char *);
//复合语句链接指示
extern "C"{
	int strcmp (const char*, const char*);
	char *strcat (char* , const char*);
}

链接指示的第一种形式包含一个关键字extern,后面是一个字符串字面值常量以及一个“普通的”函数声明。

链接指示与头文件

可以令链接指示后面跟上花括号括起来的若干函数的声明,从而一次性建立多个链接。花括号的作用是将适用于该链接指示的多个声明聚合在一起,否则花括号就会被忽略,花括号中声明的函数名字就是可见的,就好像在花括号之外声明的一样。
多重声明的形式可以应用于整个头文件。

//复合语句链接指示
extern "C"{
	#include <string.h>	//操作C风格字符串的C函数
}

当一个#include指示被放置在复合链接指示的花括号中时,头文件中的所有普通函数声明都被认为是由链接指示的语言编写的。链接指示可以嵌套,因此如果头文件包含带自带链接指示的函数,则该函数的链接不受影响。

重要术语

匿名union 未命名的union,不能用于定义对象。匿名union的成员也是外层作用域的成员。匿名union不能包含成员函数,也不能包含私有成员或受保护的成员。

位域 特殊的类成员,该成员含有一个整型值以指定为其分配的二进制位数。如果可能的话,在类中连续定义的位域将被压缩在一个普通的整数值当中。

dynamic_cast 是一个运算符,执行从基类向派生类的带检查的强制类型转换。当基类中至少含有一个虚函数时,该运算符负责检查指针或引用所绑定的对象的动态类型。如果对象类型与目标类型(或其派生类)一致,则类型转换完成。否则,指针转换将返回一个值为0的指针;引用转换将抛出一个异常。

链接指示 支持C++程序调用其他语言编写的函数的一种机制。所有编译器都应支持调用C++和C函数,至于是否支持其他语言则由编译器决定。

不可移植 固有的与机器有关的特性,当程序转移到其他机器或编译器上时需要修改代码。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

飞大圣

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值