【C++基础】第5章:语句

语句

1. 语句基础

1.1 语句的常见类别

1.1.1 表达式语句:表达式后加分号,对表达式求值后丢弃,可能产生副作用

在这里插入图片描述

1.1.2 空语句:仅包含一个分号的语句,可能与循环一起工作

在这里插入图片描述

1.1.3 复合语句(语句体):由大括号组成,无需在结尾加分号,形成独立的域(语句域(下图橙色))

  1. 某些地方要求只能出现一条语句,但只写一条语句可能会存在没法实现程序所需要的功能,这时可以引入复合语句,在复合语句里面可以写多条子语句,通过这种方法,引入更加复杂的逻辑。
    在这里插入图片描述
  2. 复合语句(语句体)无需在结尾加分号,因为通过大括号,已经能让编译器知道这是一条语句了,会对大括号内的语句进行单独划分处理。
  3. 复合语句(语句体)形成独立的域

c++程序中包含很多域,最基本的是包含全局域。我们写一个函数,函数会包含函数域;写一个名字空间,名字空间里面的东西会组成域。

域:域当中可以定义变量或对象,防止名字冲突。同时有些时候域也说明了变量会在什么时候被构造出来,什么时候被销毁。

复合语句(语句体)形成独立的域:
在这里插入图片描述
上图代码合法。虽然定义了两个不同的x,但这俩x属于不同的域,int x = 2;中的x属于main函数域,int x = 3;的x属于复合语句域,故不会出现名字冲突。

如果把复合语句的{ }去掉,则报错:
在这里插入图片描述
另外,构成语句域还有个好处:我们能更加精确的控制对象的生存周期。如下图,我们构造了语句域,对于第7行的x来讲,会在我调用int x = 3;时产生,在程序执行完第10行,跳出语句域之后,x就被消亡了,在后续代码中就不能使用上图语句域中的x(第7行)

在这里插入图片描述

1.2 顺序语句与非顺序语句

1.2.1 顺序语句

  1. 从语义上按照先后顺序执行(从上到下执行语句)
    在这里插入图片描述
  2. 实际的执行顺序可能产生变化(调整顺序后可能利于编译器优化、硬件乱序执行)

下面两图的执行顺序是等价(单线程情况下):
在这里插入图片描述
在这里插入图片描述
与硬件流水线紧密结合,执行效率较高。

1.2.2 非顺序语句

非顺序语句:在执行过程中引入跳转,从而产生复杂的变化。

与顺序语句相比,非顺序语句执行效率相对较低。随着硬件、编译器的发展,我们也引入了更多的技术尽大可能地提高程序的执行速度,如在硬件中引入分支预测。
在这里插入图片描述
如上图,执行完分支branch后,再判断执行statement2还是3还是4语句。分支预测是在执行branch的同时,预测接下来会执行statement2还是3还是4语句,如预测分支执行完毕会执行statement2,那么在执行分支的同时,直接就执行statement2了。但在某些时候,可能预测错误,分支预测错误可能导致执行性能降低。

1.2.2.1 最基本的非顺序语句: goto
  1. 通过标签指定跳转到的位置

第6行,如果(x)为true,则直接跳到第8行。(汇编中的jne、jmp指的是跳转)
在这里插入图片描述

1.2.2.2 goto具有若干限制
  1. 不能跨函数跳转
    在这里插入图片描述
    上图代码不合法。goto语句是在fun函数里面的,label是在main函数里面的,goto不能实现跨函数跳转。

  2. 向前跳转(从低行号跳转到高行号,即为向前跳转)时不能越过对象初始化语句
    在这里插入图片描述
    上图代码不合法。goto跳转到label,label前面有初始化语句,但goto语句不能越过对象初始化语句(跳过了y的初始化,直接执行第9行,那么y应为啥值?)

  3. 向后跳转(从高行号跳转到低行号,即为向后跳转)可能会导致对象销毁与重新初始化
    在这里插入图片描述
    上图由goto跳转到label后,执行int x = 3;语句时,会把对象x销毁再重新初始化(重新构造x)。

1.2.2.3 goto 本质上对应了汇编语言中的跳转指令,它有缺陷,除特殊情况外,应避免使用
  1. 缺乏结构性的含义

为什么要有goto label就直接跳到label?为什么会产生跳转?

  1. 容易造成逻辑混乱

如果函数包含大量goto和label,会导致执行时跳来跳去,造成逻辑混乱。

2. 分支语句

2.1 分支语句—— if

2.1.1 语法: https://zh.cppreference.com/w/cpp/language/if

在这里插入图片描述

2.1.2 使用语句块表示复杂的分支逻辑

语句块用{ }表示,里面可以写多条语句。
在这里插入图片描述

2.1.3 从 if 到 if-else

  1. 实现多重分支
    在这里插入图片描述
    在这里插入图片描述
    上两图指同一个意思。即加不加{ }无所谓,因为整个if…else…指代一条语句,一条语句可以不使用语句块。上图也可改成:
    在这里插入图片描述
  2. else 会与最近的 if 匹配

12行的else和10行的if相匹配。
在这里插入图片描述

  1. 使用大括号改变匹配规则

如何避免上述if和else就近匹配的麻烦?我们可以使用大括号改变匹配规则。如下图:14行的else会和9行的if匹配,不会和11行的if去匹配。
在这里插入图片描述
又:我们本意是:输入一个分数,判断是否大于60分,小于60分则输出fail,大于60分则继续判断是否为excellent还是not bad。
在这里插入图片描述
在这里插入图片描述
但上面的代码写法,显然与我们的设想不太一样。我们应该加上{ }:
在这里插入图片描述

2.1.4 if V.S. constexpr if—— 运行期与编译期分支

在这里插入图片描述
(条件)里的条件必须是常量表达式(不能修改、编译器可以确定),如下图的constexpr int grade = 80
在这里插入图片描述
使用constexpr的好处:constexpr int grade = 80在编译期确定,相应地,到底执行哪个分支也即能在编译期确定,即可以选择相应的分支,同时把没用到的分支屏蔽掉,优化为:
在这里插入图片描述
同理,如果grade为59,代码优化为:
在这里插入图片描述

2.1.5 带初始化语句的 if

在这里插入图片描述
在这里插入图片描述
上图:7行,我们定义了一个变量y,在8~15行对y引入相应的分支,从第17行开始,y也还是可见的。但在某些情况下,我们希望y在8~15行用了之后,就被删除,后面的代码中我们重新定义新的y去使用。

但下图代码是非法的:定义了两个y(重复定义)
在这里插入图片描述
c++17以前,我们通过为8~15行代码加{ },引入了一个语句块,相当于定义了一个域。在一个域里面定义变量y,在域结束的时候,y会被销毁:
在这里插入图片描述
但代码比较凌乱,所以引入带初始化语句的if:

相当于在条件里面申明一个变量,int y = x * 3;的y的作用域是if…else…的结束。故在16行再定义y,是合法的。
在这里插入图片描述

2.2 分支语句—— switch

2.2.1 语法: https://zh.cppreference.com/w/cpp/language/switch

在这里插入图片描述

2.2.2 条件部分应当能够隐式转换为整形或枚举类型,可以包含初始化的语句

2.2.3 case/default 标签

在这里插入图片描述
使用初始化语句(行为和上图一致):
在这里插入图片描述

  1. case 后面跟常量表达式 , 用于匹配 switch 中的条件,匹配时执行后续的代码

执行完第9行代码,又执行第11行代码(fall through)
在这里插入图片描述
但有时我们只希望执行case 3,故引入break。

  1. 可以使用 break 跳出当前的 switch 执行

break关键字一般用在swith和循环语句。
在这里插入图片描述

  1. default 用于定义缺省情况下的逻辑

如下图,输入3,打印Hello,输入4,打印World,其他情况,打印China。
在这里插入图片描述
又:
在这里插入图片描述
上图,如果输入2,那么在执行完default之后,会继续执行case3,case4,因为没有break。如果不想执行case3,case4,那么:
在这里插入图片描述

  1. 在 case/default 中定义对象要加大括号

如下图,case 3里面定义了一个对象y,报错:case 3定义了y,使得程序不能跳转到case4和case5,

在这里插入图片描述
我们可以使用{ },在复合语句{ }内部定义一个变量y,y在执行完16行代码时被销毁,故在case4,case5中,y不可见。这样是合法的。
在这里插入图片描述
又,如果我们希望在x输入为3时打印Hello,4、5时打印World,其他情况打印China:

在这里插入图片描述
下图这么写会警告fall through
在这里插入图片描述

2.2.4 [[fall through]] 属性

fall through:
在这里插入图片描述
执行完case 3,继续执行case 4。

再如下图,如果确实需要执行完9行,又执行11行,且不希望出现警告:
在这里插入图片描述
引入[[fall through]] 属性(c++17)
在这里插入图片描述
在这里插入图片描述

2.2.5 与 if 相比的优劣

  1. 分支描述能力较弱(swich能干的,if都能干;但if能干的,swich不一定能干
    在这里插入图片描述
    等价于:
    在这里插入图片描述
    但if能干的,swich不一定能干:
    在这里插入图片描述
    x>5时,换成switch的话,需要写无数个case。

  2. switch在一些情况下能引入更好的优化

3. 循环语句

3.1 循环语句——while

3.1.1 语法: https://zh.cppreference.com/w/cpp/language/while

在这里插入图片描述
如下图,将int x = 3的x转换成布尔值true。
在这里插入图片描述

3.1.2 处理逻辑

  1. 判断条件是否满足,如果不满足则跳出循环

  2. 如果条件满足则执行循环体

  3. 执行完循环体后转向步骤 1
    在这里插入图片描述
    在这里插入图片描述

3.1.3 注意:在 while 的条件部分不包含额外的初始化内容(if和swicth语句有额外带初始化语句)

如果想额外带初始化语句,我们完全可以使用for循环语句。

3.2 循环语句——do-while

3.2.1 语法: https://zh.cppreference.com/w/cpp/language/do

在这里插入图片描述

3.2.2 处理逻辑

  1. 执行循环体(与while不太一样的是,do while语句无论是否满足条件,都要先执行一次循环体(先do后while判断条件))
    在这里插入图片描述
    又如:
    在这里插入图片描述
  2. 判断条件是否满足,如果不满足则跳出循环
  3. 如果条件满足则转向步骤 1

3.2.3 与while的区别

除了上述3.2.2.1的区别外,还有一下区别:

在这里插入图片描述
在这里插入图片描述

如上图,while的条件可以是带花括号或等号初始化器的单个变量(如初始化变量x为0)的声明;我们在声明x之后,会把x用到循环体里进行一系列操作。但do while的条件()里并不能初始化变量:
在这里插入图片描述
在这里插入图片描述
上图,int x = 0这个条件是在循环体之后才被执行(求值),求得的初始化后的变量x并不能返回到循环体里面去使用(因为循环体已经执行完了),故do while循环中,在条件里声明一个变量并没有意义。

  1. do while用号作为结尾,但while后跟语句(语句一般使用{ },有了{ }就不需要),而不是直接跟号。
    在这里插入图片描述
    在这里插入图片描述

3.2 循环语句——for

3.2.1 语法: https://zh.cppreference.com/w/cpp/language/for

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.2.2 处理逻辑

  1. 初始化语句会被首先执行
    在这里插入图片描述
  2. 条件部分会被执行,执行结果如果为 false ,则终止循环
    在这里插入图片描述
  3. 否则执行循环体
    在这里插入图片描述
  4. 迭代表达式会被求值,之后转向 2
    在这里插入图片描述

3.2.3 在初始化语句中声明多个名字

在这里插入图片描述
上图,for语句带有初始化语句的好处是:

  1. 使得变量x只在for循环这个语句域内作用,一旦执行完for循环,变量x就被销毁,不影响后续代码重新使用x(防止名字冲突);
  2. 通过变量x来控制循环的次数,如上图,声明x=0,则循环5次;若声明x=1,则循环4次。

初始化语句中可以声明多个名字:一个变量的类型是int,一个是int指针(其值指向i的地址)

在这里插入图片描述
以下代码不合法:只能声明多个拥有相同的声明说明符序列,即只能声明int或i的引用、int指针,但不能同时声明int变量,又声明double变量。
在这里插入图片描述
实际上,在变量初始化时,可以如下这么写(不建议):(声明了变量x,同时又声明变量x的指针、引用等)
在这里插入图片描述

3.2.4 初始化语句、条件、迭代表达式可以为空

语句体不能为空,初始化语句、条件、迭代表达式可以为空。

  1. 初始化语句为空:系统会执行一条空语句
  2. 条件语句为空:系统自动判断为true
  3. 迭代表达式为空:不进行任何实质性操作

3.2.5 for 的更多示例

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

其中:

  1. 第2个for循环语句:*p:对p指针进行解引用,获取p所指向的那块内存的初值。
  2. 第4个for循环:用auto来声明,那么iter会推导出v.begin()所返回的类型退化之后的类型,相应地,iter对应的是vector的指针:
    在这里插入图片描述
  3. 第5个循环:条件语句中:
    在这里插入图片描述
    std::cout输出正常的时候,返回的是true。

循环体:
在这里插入图片描述

3.3 循环语句——基于范围的 for 循环

3.3.1 语法: https://zh.cppreference.com/w/cpp/language/range-for

在这里插入图片描述
在这里插入图片描述

3.3.2 本质:语法糖,编译器会转换为 for 循环的调用方式

3.3.3 转换形式的衍化: C++11 / C++17 / C++20

  1. c++11:(int v : arr指v依次访问arr里面的每个元素)
    在这里插入图片描述
    在这里插入图片描述
    上图,对范围表达式求值,把求值结果放到__range里面,左边的&&对应右值引用,但前面跟一个auto,则auto &&不是右值引用,而是万能引用,即编译器会根据范围表达式和万能引用auto &&推导出是左值引用还是右值引用(在后面泛型编程和元编程中会讨论这部分内容)。auto &&推导出来的类型是下图蓝色部分:左值引用
    在这里插入图片描述
    简单点的例子:
    在这里插入图片描述
    但:
    在这里插入图片描述
    上图蓝色,一条语句定义两个变量,这两个变量的基本类型必须一样,但某些时候,首表达式和尾表达式可能会返回不同类型,如果是这种情况,那么上图蓝色的代码写法显然是错的。所以在c++17时,将上图蓝色的首表达式和尾表达式分开定义。
  2. c++17:
    在这里插入图片描述
    在这里插入图片描述
  3. c++20:
    在这里插入图片描述
    在这里插入图片描述
    上图代码,c++17和c++20没啥区别。但c++20相比c++17,是多了初始化语句
    在这里插入图片描述
    相比于c++11和17,c++20的for循环允许加初始化语句了。
    在这里插入图片描述
    为什么要引入初始化语句?

for循环有个临时范围表达式的概念:如果我们在c++写下图这个代码(c++11之后,下面的代码都是可以通过编译,但是在c++11~17里,写下面代码,其执行行为是未定义的)。
,t_70,g_se,x_16)
foo().items()指范围表达式,将其放入下图范围表达式里面:即对范围表达式求值,求值结果作为一个引用(可能是左值引用也可能是右值引用),把求值结果存到__range里面:在这里插入图片描述
但是会有个问题,foo().items()相当于调用了函数foo,然后foo函数的返回值,我们使用成员访问操作符.来调用items,如果foo返回的是右值(临时的值,假设存储在tem里)(foo返回左值还是右值,由auto &&决定),那么我们在对上图蓝色区域的范围表达式求值之后,求得的临时值保存在tem里,接下来我们调用tem.items(),返回的值作为引用会被绑定到__range,但在上图蓝色语句执行完,tem是临时值,会被销毁,那么items返回的数据元素也会被销毁(临时变量tem被销毁,由tem调用的items返回的数据自然也被销毁),那么range所绑定的东西就失效了。那么上图蓝色语句后的所有代码基本上是未定义行为。

上述问题,在c++20里面,可以使用初始化语句来解决。foo函数返回右值,我们用thing把右值保留下来:
在这里插入图片描述
那么对应的逻辑就变成下图:
在这里插入图片描述
我们在初始化语句定义thing,thing的生命周期是从初始化语句被调用开始构造出来,到{ }结束被销毁。换句话说thing在系统中一直存在。那么就不会出现上述的由于foo产生的未定义行为。

3.3.4 使用常量左值引用读元素;使用万能引用( universal reference )修改元素

  1. 使用常量左值引用读元素
    在这里插入图片描述
    上图是读取元素。
    又如:
    在这里插入图片描述
    在这里插入图片描述
    但上图这么写,非常耗时:
    在这里插入图片描述
    我们可使用&修改:(同时,为了表明我们不会对v进行修改,我们使用了const &)
    在这里插入图片描述
    其中,下图构造了常量左值引用:
    在这里插入图片描述
    实际上,我们还可以对上述代码简写:(系统自动推导v是常量左值,auto会被替换成std::string)
    在这里插入图片描述
  2. 使用万能引用( universal reference )修改元素

假设我们需要对arr里面的元素进行修改,可以将上图改成下图:(使用非常量左值引用)
在这里插入图片描述
在这里插入图片描述
但使用非常量左值引用去修改元素,有时候会出bug,如下图,我们将上图代码的int改成bool,希望将arr里面的元素全部输出为false:
在这里插入图片描述
报错。这是因为涉及vector内部的逻辑,通过auto& v : arr获取到的是迭代器,vector的迭代器的解引用不是bool类型,而是中间类型。对于这种情况,我们应该使用auto&&(万能引用):
在这里插入图片描述

3.4 循环语句——break / continue

3.4.1 含义(转自 cpp reference )

  1. break: 导致外围的 for 、范围 for 、 while 或 do-while 循环或 switch 语句终止
    在这里插入图片描述
    在这里插入图片描述
    上图break只会影响蓝色。

  2. continue: 用于跳过整个 for 、 while 或 do-while 循环体的剩余部分
    在这里插入图片描述
    在这里插入图片描述
    又:i != 5时,会通过continue跳过9行语句。
    在这里插入图片描述
    再如:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

3.4.2 注意这二者均不能用于多重嵌套循环,多重嵌套循环的跳转可考虑 goto 语句

使用goto…label跳出多重循环:(橙色:label一定要跟一个语句,橙色为空语句)
在这里插入图片描述
在这里插入图片描述

使用break只会跳出当前内层循环,外围循环还会继续:
在这里插入图片描述

4. 语句的综合应用——达夫设备

4.1 使用循环展开提升系统性能

假设有一个vector,名为buffer,buffer里面包含一系列元素,buffer_count表示buffer里面包含多少个元素(如10000个元素)。
在这里插入图片描述
第10行:为buffer里面的每个元素设置一个值(如第i个元素设置为i),接下来我们要求buffer里面最大的那个值,我们可以使用循环来解决这个问题:
在这里插入图片描述
在这里插入图片描述
但上图代码也还有可以提升的地方:

for循环处理起来耗费时间:
在这里插入图片描述
我们可以使用循环展开来解决上述for循环耗费资源的问题:(每次迭代处理8个语句,然后每隔8个数才迭代一次)
在这里插入图片描述
上图,循环体内部的逻辑变复杂了,处理了8次才调用一次迭代表达式,才会进行比较,即增加了循环处理的工作量,减少了迭代表达式和条件的工作量。

4.2 处理无法整除的情形

但是如果buffer_count为10001,输出结果乱了(输出随机值):
在这里插入图片描述
在这里插入图片描述
i = 10000时,17行能合理执行,但18~24无法合理处理,因为18行返回的是buffer[10001],已经是第10002个元素了,超出buffer_count的范围了,19行~24行同理(内存访问越界)。

那么如何避免内存访问越界问题?

如上图,迭代表达式为i += 8,那么我们应该要求buffer_count为8的倍数。

4.2.1 额外增加一个循环语句

但这样不人性化,我们还可以:
在这里插入图片描述
在这里插入图片描述
上图,当i是9992时,执行条件语句(下图)没问题。(10000<10001)
在这里插入图片描述
接下来,当i =10000时,10000+8不小于buffer_count(10001),那么for循环跳出。相当于我们处理了前面10000个数据(i从0取到9999,共10000个数据),后面还有一个数据(还剩下i=10000没进行对比是否为最大值)(一共10001个数据)我们使用26行的for循环处理:
即单独使用一个for循环来处理i=10000(最多会处理7个元素(即buffer_count = 10007时)):
在这里插入图片描述
简单来说,分为两个for循环进行处理。第一个for循环处理能被8整除的i,第二个for循环处理不能被8整除的i。
但这样并不算好,因为这俩for循环的逻辑一样,相当于重复了。

还可以继续简化:

  1. 用指针处理能被8整除的i

我们可以使用17~24行循环语句用指针ptr统一起来:ptr指向buffer中第一个元素(begin)
在这里插入图片描述

  1. 用switch处理不能让被8整除的i

即如果buffer_count为10007(10007除以8,余7),则需要处理7次循环语句;buffer_count为10006(10006除以8,余6),则需要处理6次循环语句,以此类推 在这里插入图片描述
加fallthrough属性避免warning:
在这里插入图片描述
以上代码的逻辑为:数组中有一堆元素,8个8个分堆,一次处理8个;然后使用switch处理最后剩下的几个i。

4.2.2 将 switch —— 与循环结合 达夫设备

我们把上述逻辑反过来,先让switch处理数组中前面的一堆元素,然后我希望switch处理完之后,数组中剩余元素是8的整数倍,然后再使用for循环处理。

注意case 0一定要放在case 7前面,什么意思呢?

首先假设数组当中一定包含一个或一个以上元素(即数组不为空。如果数组为空,我们需要引入一些额外的逻辑来处理),数组不为空的话,buffer_count % 8即8中情况,要么为0,要么为1。。。要么为7。

buffer_count % 8为0,说明数组中包含8的整数倍的元素(8个,16个,24个。。。)

假设数组中包含24个元素,即buffer_count = 24,则switch语句中处理了8个i,还有24-8=16个i在for循环中被处理。

假设数组中包含31个元素,即buffer_count = 31,则switch语句中处理了7个i(31%8=7),还有31-7=24(一定是8的整数倍)个i在for循环中被处理。
在这里插入图片描述
在这里插入图片描述
上图,假设buffer_count = 24(buffer_count - 1) / 8 = 2,即执行两次for循环。

接下来再做修改:switch内再套for循环(达夫设备)
在这里插入图片描述
对上图做两个调整:

  1. 在switch…case里面声明定义一个变量,需要加大括号{ },因为switch会产生一个跳转,我们希望这种跳转不会跳转过变量的定义,否则就无法在后续代码使用这个变量。如果加了{ },那么这个变量的生存周期就被限制在一条复合语句里,这样跳转就没问题。与之类似,我们定义了一个i = 0(下图橙色部分):
    在这里插入图片描述
    在产生跳转时,我们不希望跳过i的定义,故把上图变量i的定义移到switch上面:
    在这里插入图片描述
  2. (buffer_count - 1) / 8改为(buffer_count + 7) / 8
    在这里插入图片描述
    总结:首先执行switch,先判断buffer_coubt % 8的值,根据其值跳到相应的case语句,执行完case语句,会执行for的逻辑,进行迭代表达式求值,再对条件进行判断 ,判断完之后再去执行for循环。本质上和先执行完switch再执行for是一样的逻辑,但只不过由于上面的代码中switch和for语句中的代码大部分一样,所以就合到一起写了。

那么为什么将(buffer_count - 1) / 8改为(buffer_count + 7) / 8

这是由于for循环本身的执行逻辑所决定的。如果我们只是简简单单写一个for循环,那么会先执行初始化表达式,再进行条件判断,如果条件满足,那么执行循环体内的语句,最后再进行迭代表达式求值。

但如果按上图那样代码写法,那么是经switch之后,直接执行for循环里面的case语句了,然后再++i,再执行条件语句判断。实际上相当于多对i增加了1,此时如果想保证循环体执行的次数跟原先是一样的,那么我们就要将(buffer_count - 1) / 8改为(buffer_count + 7) / 8,把多执行的那一次给找回来。

处理fallthrough的问题:for语句后也要加[[fallthrough]]
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

cashapxxx

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

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

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

打赏作者

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

抵扣说明:

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

余额充值