C++学习笔记7 作用域与编译链接

7 作用域与编译链接

7.1 代码块

代码块语句(block statement)是零个或多个语句组成的一组复合语句,编译器将其视为单个语句。

代码块以 {符号开始,以}符号结束,要执行的语句放在两者之间。代码块可以在允许单个语句的任何地方使用。末尾不需要分号。

代码块嵌套

函数不能嵌套在其他函数中,但代码块可以嵌套在其他块中。代码块的最常见用例之一是与if语句结合使用。默认情况下,如果条件表达式的计算结果为true,则if语句执行单个语句。如果希望在这时执行多个语句,则可以用语句块替换这条语句。

代码块可以多层嵌套,函数中代码块的最大嵌套深度,是从最外围到最内层的代码块的级数。C++标准规定C++编译器应该支持256级嵌套——然而,并非所有的编译器都支持。最好将嵌套级别保持在3或更少。

7.2 自定义命名空间和作用域解析操作符

当两个相同的标识符被引入同一作用域时,就会发生命名冲突,编译器无法决定使用哪个标识符。随着程序变得越来愈大,标识符的数量增加,这反过来导致发生命名冲突的概率显著增加。由于给定范围中的每个名称都可能与同一范围中的其他名称发生潜在冲突,因此标识符的线性增加将导致潜在冲突的指数增加!这是在尽可能小的范围内定义标识符的关键原因之一。

7.2.1 定义自己的命名空间

C++允许我们通过namespace关键字定义自己的命名空间。在程序中创建的命名空间称为用户定义命名空间。

命名空间的语法如下:

namespace NamespaceIdentifier
{
    // content of namespace here
}

从namespace关键字开始,后面是名称空间的标识符(即命名空间的名称),然后是大括号,其中包含名称空间中的内容。

这里建议以大写字母开始命名空间名称。然而,任何一种风格都应被视为可接受。首选以大写字母开头的命名空间名称的一些原因:

  1. 通常以大写字母开头命名用户自定义的类型。在使用限定名(如Foo::x, 其中Foo可以是命名空间或class名)时,命名空间与用户自定义类型一致。
  2. 有助于防止与系统提供的或库提供的小写名称发生命名冲突。
  3. C++20标准文档使用这种样式。
  4. C++核心指南文档使用这种风格。

命名空间必须在全局范围内或在另一个命名空间内定义。与函数中内容缩进类似,命名空间的内容通常缩进一级。有时,您可能会看到在命名空间的右大括号后面放置了可选的分号。

7.2.2 使用域解析操作符( :: )访问命名空间

在特定命名空间中查找标识符的最佳方法是使用域解析操作符( :: )。域解析操作符告诉编译器,应该在左侧操作数的范围内查找右侧操作数指定的标识符。

7.2.3 使用无名称前缀的域解析操作符

域解析操作符也可以在标识符之前使用,而不提供命名空间名称(例如 ::doSomething)。在这种情况下,在全局命名空间中查找标识符。

如果使用命名空间内的标识符,并且没有提供域解析,编译器将首先尝试在同一命名空间中查找匹配的声明。如果没有找到匹配的标识符,编译器将依次检查外围每个层级的命名空间,以查看是否找到匹配,直到检查全局命名空间。

7.2.4 命名空间中内容的前向声明

对于命名空间内的标识符,前向声明也需要在同一命名空间内:

add.h

#ifndef ADD_H
#define ADD_H

namespace BasicMath
{
    // function add() is part of namespace BasicMath
    int add(int x, int y);
}

#endif

add.cpp

#include "add.h"

namespace BasicMath
{
    // define the function add() inside namespace BasicMath
    int add(int x, int y)
    {
        return x + y;
    }
}

main.cpp:

#include "add.h" // for BasicMath::add()

#include <iostream>

int main()
{
    std::cout << BasicMath::add(4, 3) << '\n';

    return 0;
}

如果 add() 的前向声明没有放在命名空间BasicMath中,则 add() 将改为在全局命名空间中定义,编译器将告警,没有看到对 BasicMath::add(4, 3) 的调用函数的声明。如果函数 add() 的定义不在命名空间BasicMath内,则链接器将告警,找不到用于调用 BasicMath::add(4, 3) 的匹配定义。

7.2.5 单个命名空间可以存在多个文件中

在多个位置(跨多个文件或同一文件中的多个位置)声明命名空间块是合法的。命名空间中的所有声明都被视为命名空间的一部分。

请注意,此功能还意味着您可以将自己的定义添加到std命名空间。这样做在大多数情况下都会导致未定义的行为,因为std命名空间有一个特殊的规则,禁止从用户代码进行扩展。

7.2.6 嵌套命名空间

命名空间可以嵌套在其他命名空间中。例如:

#include <iostream>

namespace Foo
{
    namespace Goo // Goo is a namespace inside the Foo namespace
    {
        int add(int x, int y)
        {
            return x + y;
        }
    }
}

int main()
{
    std::cout << Foo::Goo::add(1, 2) << '\n';
    return 0;
}

请注意,因为名称空间Goo在名称空间Foo内,所以访问add需要写为 Foo::Goo::add。

7.2.7 命名空间别名

由于在嵌套命名空间中键入变量或函数的限定名可能会很痛苦,C++允许您创建命名空间别名,这允许我们暂时将一长串命名空间缩短为较短的名称空间:

#include <iostream>

namespace Foo::Goo
{
    int add(int x, int y)
    {
        return x + y;
    }
}

int main()
{
    namespace Active = Foo::Goo; // active now refers to Foo::Goo

    std::cout << Active::add(1, 2) << '\n'; // This is really Foo::Goo::add()

    return 0;
} // The Active alias ends here

名称空间别名的一个很好的优点:如果您想要将 Foo::Goo 中的功能移动到不同的位置,您可以只更新 Active 这个别名以指代新的目标,而不必查找/替换Foo:∶Goo的每个实例。

7.3 局部变量

7.3.1 局部变量

局部变量是在函数内定义的变量(包括函数参数)。

在之前,我们还介绍了作用域的概念。当一个标识符可以被访问时,我们说它在作用域内。当标识符不能被访问时,我们说它超出作用域。作用域是编译时属性,当标识符超出作用域时,尝试使用它将导致编译错误。

7.3.2 局部变量具有代码块作用域

局部变量具有代码块作用域,这意味着从定义点到定义它们的代码块的末尾都在作用域内。

尽管函数参数没有在函数体内定义,但对于一般函数,它们可以被视为函数体内作用域的一部分。

7.3.3 作用域内的所有变量名都必须唯一

变量名在给定作用域内必须唯一。

7.3.4 局部变量具有自动存储期

变量的存储期(storage duration)规则,决定了何时以及如何创建和销毁变量。在大多数情况下,变量的存储期规则直接决定其生命周期。

例如,局部变量具有自动存储期,这意味着它们在定义点创建,在定义它们的代码块末尾销毁。

因此,局部变量有时称为自动变量。

7.3.5 嵌套代码块中的局部变量

局部变量可以在嵌套块内定义。这与函数体块中的局部变量相同:

int main() // outer block
{
    int x { 5 }; // x enters scope and is created here

    { // nested block
        int y { 7 }; // y enters scope and is created here
    } // y goes out of scope and is destroyed here

    // y can not be used here because it is out of scope in this block

    return 0;
} // x goes out of scope and is destroyed here

7.3.6 局部变量没有链接属性

标识符具有另一个名为链接(linkage)的属性。标识符的链接属性决定了该名称的其他声明是否引用同一对象。

局部变量没有链接属性,这意味着每个声明都是唯一的对象。例如:

int main()
{
    int x { 2 }; // local variable, no linkage

    {
        int x { 3 }; // this declaration of x refers to a different object than the previous x
    }

    return 0;
}

作用域和链接性似乎有些相似。然而,作用域定义了单个声明的可见和使用位置。链接性定义多个声明是否引用同一对象。

变量应在最有限的现有作用域内定义。

7.4 全局变量

7.4.1 声明全局变量

在C++中,变量也可以在函数外部声明。这样的变量称为全局变量。

按照惯例,全局变量在全局命名空间中的文件顶部、include的下方声明。

7.4.2 全局变量的作用域

在全局命名空间中声明的标识符具有全局作用域,这意味着它们从声明点到声明它们的文件末尾都是可见的。

一旦声明,全局变量就可以在文件中的任何位置使用。

全局变量也可以在用户定义的命名空间内定义,但自定义命名空间中的全局变量必须通过域解析操作符(例如Foo::g_x)来访问。

7.4.3 全局变量具有静态存储期

全局变量在程序启动时创建,在程序结束时销毁。这称为静态存储期。具有静态存储期的变量有时称为静态变量。

7.4.4 命名全局变量

按照惯例,一些开发人员在非常量的全局变量标识符前面加上“g”或“g_”,以表示它们是全局的。该前缀有几种用途:

  1. 它有助于避免与全局命名空间中的其他标识符发生命名冲突。
  2. 它有助于防止无意中的命名遮挡(name shadowing)。
  3. 它有助于指示变量存在于函数范围之外,因此,对它们所做的任何更改也将持续存在。

在用户定义的命名空间内定义的全局变量通常省略前缀(因为在这种情况下,上面列表中的前两点不是问题,并且当我们看到前缀的命名空间名称时,我们可以推断变量是全局的)。

7.4.5 全局变量初始化

与局部变量(默认情况下未初始化)不同,具有静态存储期的变量默认为被零初始化。

也可以选择初始化非常量的全局变量。

7.4.6 常量全局变量

就像局部变量一样,全局变量也可以是常量。与所有常量一样,必须初始化常量全局变量。

7.5 变量名称遮挡(Variable shadowing)

每个代码块有自己的变量命名的空间。那么,当我们在嵌套的内部块中有一个变量,该变量与外部块中的变量同名时,会发生什么呢?当这种情况发生时,嵌套块内的变量会“遮挡”外部变量。

在外部块中声明一个名为apples的局部变量,并在嵌套块中声明一个同名变量,该同名变量将原局部变量遮挡直至嵌套块末尾被销毁。

类似于嵌套块中的变量会隐藏外部块中的同名变量,与全局变量同名的局部变量将遮挡全局变量,无论局部变量在作用域中的何处。

通常应避免局部变量的命名遮挡,因为在使用或修改错误的变量时,会导致意外错误。当变量被遮挡时,某些编译器将发出警告。

7.6 内部链接

全局变量和函数标识符可以具有内部链接或外部链接属性。

具有内部链接的标识符可以在单个翻译单元内看到并使用,但它不能从其他翻译单元访问(即,它不向链接器公开)。这意味着,如果两个源文件存在具有内部链接的同名标识符,则这些标识符将被视为独立的(并且不会因具有重复定义而导致ODR冲突)。

7.6.1 具有内部链接的全局变量

具有内部链接的全局变量有时称为内部变量。

为了使非常量的全局变量成为内部变量,使用static关键字进行声明。

默认情况下,Const和constexpr全局变量具有内部链接属性(因此不需要static关键字——如果使用static关键字也无额外作用)。

上面static关键字的使用是存储类说明符( storage class specifier)的一个示例,它设置名称的链接属性及其存储期。最常用的存储类说明符是static、extern和mutable的。

7.6.2 具有内部链接的函数

由于链接是标识符的属性(不是变量的属性),函数标识符也具有链接属性。函数默认为外部链接,若一个文件中定义了一个函数,则其他文件可以通过前向声明来调用该函数。

函数默认为外部链接,但可以通过static关键字设置为内部链接。即函数被声明为static后,只能在该文件中被使用,即使其他文件中有该函数的前向声明也不能访问。

7.6.3 单定义规则和内部链接

根据单定义规则,我们注意到,对象或函数不能在文件或程序中具有多个定义。

然而,值得注意的是,在不同文件中定义的内部对象(和函数)被认为是独立的实体(即使它们的名称和类型相同),因此不会违反单定义规则。每个文件内部对象只有一个定义。

7.6.4 static 对比未命名的命名空间

在现代C++中,使用static关键字为标识符提供内部链接越来越不受欢迎。未命名的名称空间可以为更广泛的标识符(例如,类型标识符)提供内部链接,并且它们更适合为多个标识符提供内部链接。

7.6.5 为什么为标识符提供内部链接

让标识符具有内部链接通常有两个原因:

  1. 有一个标识符,我们要确保其他文件无法访问。这可能是一个我们不想弄乱的全局变量,或者是一个不想调用的辅助函数。
  2. 避免命名冲突。由于具有内部链接的标识符不会向链接器公开,因此它们只能与同一翻译单元中的名称发生冲突,而不会在整个程序中发生冲突。

当您有明确的理由不允许从其他文件访问时,将标识符设置为内部链接。最好将您不希望其他文件访问的所有标识符设置为内部链接(使用未命名的命名空间)。

7.7 外部链接和变量前向声明

具有外部链接的标识符既可以从定义它的文件中看到,也可以从其他代码文件中使用(通过前向声明)。

7.7.1 函数默认具有外部链接

为了调用在另一个文件中定义的函数,必须在使用该函数的任何其他文件中放置该函数的前向声明。前向声明将函数的存在告知编译器,链接器将函数调用连接到实际的函数定义。

7.7.2 具有外部链接的全局变量

具有外部链接的全局变量有时称为外部变量。要将全局变量设置为外部变量(因此可由其他文件访问),可以使用extern关键字执行此操作。

默认情况下,非常量的全局变量是外部链接(添加extern关键字也会被忽略)。

7.7.3 通过extern关键字进行变量前向声明

要实际使用在另一个文件中定义的外部全局变量,还必须在希望使用该变量的任何其他文件中放置全局变量的前向声明。对于变量,也可以通过extern关键字(没有初始化值)创建前向声明。

如果要定义未初始化的非常量全局变量,请不要使用extern关键字,否则C++会认为您正在尝试对该变量进行前向声明。

7.8 在多个文件中共享全局常量(使用内联变量)

在某些应用程序中,可能需要在整个代码中使用某些符号常量(而不仅仅是在一个位置)。例如不改变的物理或数学常数(例如,pi或阿伏伽德罗数),或特定于应用的“系数”值(例如,摩擦系数或重力系数)。与其在每个需要它们的文件中重新定义这些常量),不如在中心位置声明它们一次,并在需要的地方使用它们。

7.8.1 作为内部变量的全局常数

在C++17之前,以下是最简单、最常见的解决方案:

  1. 创建一个头文件来放置这些值
  2. 在创建的头文件中定义一个命名空间
  3. 使用constexpr来定义这些变量
  4. 使用#include,将这个头文件包含在需要使用它的地方

7.8.2 作为外部链接属性的全局常量

上述方法有一些潜在的缺点。

虽然方法很简单(对于较小的程序来说也很好),但每次constants.h被包含在不同的代码文件中时,这些变量每个都会复制到对应的代码文件中。如果constants.h被包含在20个不同的代码文件中,则每个变量都重复20次。头文件保护不会阻止这种情况的发生,因为它们只会防止头文件被多次包含到单个文件中,而不是被一次包含到多个不同的代码文件中。这带来了两个挑战:

  1. 改变一个常量,需要重编引用到对应头文件的所有代码文件,在大型项目中,会导致较长的重新编译时间。
  2. 如果常量很大,不能被优化掉,那么会消耗大量的内存。

避免这些问题的一种方法是将这些常量转换为外部变量,因为可以有一个在所有文件中共享的单个变量(初始化一次)。在这种方法中,我们将在.cpp文件中定义常量(以确保定义仅存在于一个位置),并在头中进行声明(它将包含在其他文件中)。

然而,这种方法有几个缺点。首先,这些常量现在仅在它们实际定义的文件(constants.cpp)中被视为编译时常量。在其他文件中,编译器将只看到前向声明,该声明不定义常量值(并且必须由链接器解析)。这意味着在其他文件中,它们被视为运行时常量值,而不是编译时常量。因此,在constants.cpp之外,不能在需要编译时常量的任何地方使用这些变量。其次,由于编译时常量通常可以比运行时常量更容易优化,编译器可能无法对这些常量进行足够的优化。因此,constexpr变量不能分为头文件和源文件,它们必须在头文件中定义。

鉴于上述缺点,最好在头文件中定义常量。如果您发现常量的值经常变动(例如,因为您正在调整程序),导致编译时间过长,则可以根据需要将有问题的常量移到.cpp文件中。

在该方法中,我们使用const而不是constexpr,因为constexper变量不能被前向声明,即使它们具有外部链接。这是因为编译器需要在编译时知道变量的值,而前向声明不提供此信息。

7.9 静态局部变量

局部变量在默认情况下具有自动存储期,这意味着它们在定义点创建,并在退出代码块时销毁。

对局部变量使用static关键字会将其存储期从自动存储期更改为静态存储期。这意味着现在在程序开始时创建变量,并在程序结束时销毁(就像全局变量一样)。因此,即使静态变量超出范围,它也将保留其值!

静态存储期局部变量最常见的用途之一是用于唯一ID生成器:

int generateID()
{
    static int s_itemID{ 0 };
    return s_itemID++; // 制作 s_itemID的拷贝, s_itemID加一, 返回拷贝的值
}

第一次调用此函数时,它返回0。第二次,它返回1。每次调用它时,它都会返回比上次调用它时高一的数字。可以将这些编号指定为对象的唯一ID。由于s_itemID是局部变量,因此它不能被其他函数“篡改”。

静态局部常数

静态局部变量可以设置为const(或constexpr)。常量静态局部变量的一个很好的用途是当您有一个需要使用常量值的函数,但创建或初始化对象的成本很高(例如,您需要从数据库中读取值)。如果使用普通局部变量,则每次执行函数时都会创建和初始化该变量。使用const/constexpr静态局部变量,可以创建并初始化昂贵的对象一次,然后在调用函数时重用它。

7.10 作用域、存储期和链接

7.10.1 作用域摘要

标识符的作用域确定可以在源代码中访问标识符的位置。

  1. 具有块(局部)作用域的变量只能从声明点访问,直到声明它们的块(包括嵌套块)的末尾。这包括:
    • 局部变量
    • 函数参数
    • 代码块中类型定义(例如enum和class)
  2. 具有全局作用域的变量和函数可以从声明点访问,直到文件结束。这包括:
    • 全局变量
    • 函数
    • 命名空间(全局或非全局)中类型定义(例如enum和class)

7.10.2 存储期摘要

变量的存储期决定了它的创建和销毁时间。

  1. 具有自动存储期的变量在定义点创建,并在退出它们所属的块时销毁。这包括:
    • 局部变量
    • 函数参数
  2. 具有静态存储期的变量在程序开始时创建,在程序结束时销毁。这包括:
    • 全局变量
    • 静态局部变量
  3. 具有动态存储期的变量由程序员请求创建和销毁。这包括:
    • 动态创建的变量

7.10.3 链接摘要

标识符的链接属性确定标识符的多个声明是否引用同一实体(对象、函数、引用等)。

  1. 没有链接的标识符意味着该标识符仅引用其自身。这包括:
    • 局部变量
    • 代码块中类型定义(例如enum和class)
  2. 具有内部链接的标识符可以在其声明的文件中的任何位置访问。这包括:
    • Static 全局变量
    • Static 函数
    • Const 全局变量
    • 在未命名的命名空间中声明的函数
    • 在未命名的命名空间中类型定义(例如enum和class)
  3. 具有外部链接的标识符可以在其声明的文件或其他文件中的任何位置访问(通过前向声明)。这包括:
    • 函数
    • 非常量全局变量
    • Extern const 全局变量
    • Inline const 全局变量

如果将定义编译到多个.cpp文件中,则具有外部链接的标识符通常会导致重复定义的链接器错误(由于违反了单定义规则)。这个规则有一些例外(对于类型、模板和内联函数和变量)——在对应的课程中有相应的说明。

另请注意,默认情况下,函数具有外部链接。可以使用static关键字将它们设置为内部链接。

7.11 using声明和using指令

7.11.1 限定与未限定的名称

名称可以是限定(qualified)的,也可以是不限定(unqualified.)的。

限定名是包含关联域的名称。通常,使用域解析操作符(::)用命名空间限定名称。

非限定名称是不包括域限定符的名称。例如,cout和x是非限定名称,因为它们不包括关联的作用域。

名称也可以由类名限定,或者使用成员选择操作符(.或->)由类对象限定。

7.11.2 using声明

减少反复键入std::的一种方法是使用using声明语句。using声明允许我们使用非限定名称(没有域名称)作为限定名称的别名。

使用 using std::cout;告诉编译器我们将使用std命名空间中的对象cout。因此,每当它看到cout时,它都会假设我们是指std::cout。

using声明从声明点到对应作用域结束都是有效的。

7.11.3 using指令

另一种简化方法是使用using指令。稍微简化了一点,using指令将命名空间中的所有标识符导入using指令的作用域。

using namespace std; 告诉编译器将std命名空间中的所有名称导入到当前作用域(在本例中,是函数main() 的内部)。然后,当我们使用不含前缀的cout时,它将解析为std::cout。

7.11.4 using声明和using指令的作用范围

如果在块中使用using声明或using指令,则名称仅适用于该块(它遵循正常的块作用域规则)。这是一件好事,因为它减少了在该块内发生命名冲突的机会。

如果在全局命名空间中使用using声明或using指令,则名称适用于文件的整个其余部分(它们具有文件范围)。

7.12 未命名与内联的命名空间

7.12.1 未命名(匿名)命名空间

未命名命名空间(也称为匿名命名空间)是定义时没有名称的命名空间,如下所示:

#include <iostream>

namespace // 未命名的命名空间
{
    void doSomething() // 只能在本文件中访问
    {
        std::cout << "v1\n";
    }
}

int main()
{
    doSomething(); // 使用doSomething(),可以不用带命名空间限定符

    return 0;
}

在未命名命名空间中声明的所有内容都被视为父命名空间的一部分。因此,即使函数doSomething() 在未命名的命名空间中定义,函数本身也可以在父命名空间(在本例中是全局命名空间)被访问,这就是为什么我们可以从main() 调用doSometing() ,而不需要任何限定符。

这可能会使未命名的命名空间看起来毫无用处。但未命名命名空间的另一个影响是,未命名命名空间内的所有标识符都被视为具有内部链接,这意味着在定义未命名命名空间所在的文件外部看不到未命名名称空间的内容。

对于函数,这实际上与将未命名命名空间中的所有函数定义为static函数相同。

7.12.2 内联命名空间

内联命名空间是通常用于版本内容的命名空间。与未命名命名空间很相似,在内联命名空间内声明的任何内容都被视为父命名空间的一部分。然而,与未命名名称空间不同,内联名称空间不影响链接。

要定义内联命名空间,我们使用inline关键字:

#include <iostream>

inline namespace V1 // 定义内联命名空间 V1
{
    void doSomething()
    {
        std::cout << "V1\n";
    }
}

namespace V2 // 定义普通命名空间 V2
{
    void doSomething()
    {
        std::cout << "V2\n";
    }
}

int main()
{
    V1::doSomething(); // 调用v1版 doSomething()
    V2::doSomething(); // 调用v2版 doSomething()

    doSomething(); // 调用内联版本 doSomething() (V1)
 
    return 0;
}

在上面的示例中,doSomething()的调用获得V1(内联版本)。想要使用较新版本的调用可以显式调用V2::doSomething()。这保留了现有程序的功能,同时允许较新的程序利用较新/更好的变体。

7.12.3 混合内联命名空间和未命名命名空间

命名空间可以是内联的,也可以是未命名的:

#include <iostream>

namespace V1 // 定义普通命名空间 V1
{
    void doSomething()
    {
        std::cout << "V1\n";
    }
}

inline namespace // 定义一个内联的未命名的命名空间
{
    void doSomething() // 内部链接
    {
        std::cout << "V2\n";
    }
}

int main()
{
    V1::doSomething(); // 调用v1版 doSomething()

    doSomething(); // 调用内联版本 doSomething() (未命名版本)

    return 0;
}

然而,在这种情况下,最好将匿名命名空间嵌套在内联命名空间中。这具有相同的效果(默认情况下,匿名命名空间内的所有函数都具有内部链接),但仍然为您提供了一个可以使用的显式命名空间名称:

#include <iostream>

namespace V1 // 定义普通命名空间 V1
{
    void doSomething()
    {
        std::cout << "V1\n";
    }
}

inline namespace V2 // 定义一个内联的命名空间 V2
{
    namespace // 匿名的命名空间
    {
        void doSomething() // 内部链接
        {
            std::cout << "V2\n";
        }

    }
}

int main()
{
    V1::doSomething(); // 调用v1版 doSomething()
    V2::doSomething(); // 调用v2版 doSomething()

    ::doSomething(); // 调用内联版本 doSomething() (V2)

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值