领悟设计模式--Template Method / Visitor

与大虾对话 领悟设计模式 立即下载
 

[译者按] 本文根据发表在CUJ Expert Forum上的两篇文章编译而成。C/C++ User's Journal是目前最出色的C/C++语言专业杂志,特别是在C++ Report闭刊之后,CUJ的地位更加突出。CUJ Expert Forum是CUJ主办的网上技术专栏,汇集2000年10月以来C++社群中顶尖专家的技术短文,并免费公开发布,精彩纷呈,是每一个C/C++学习者不可错过的资料。由Jim Hyslop和Herb Sutter主持的Conversation系列,是CUJ Expert Forum每期必备的精品专栏,以风趣幽默的对话形式讲解C++高级技术,在C++社群内得到广泛赞誉。译者特别挑选两篇设计模式方面的文章,介绍给大家。设计模式方面的经典著作是GoF的Design Patterns。但是那本书有一个缺点,不好懂。从风格上讲,该书与其说是为学习者而写作的教程范本,还不如说是给学术界人士看的学术报告,严谨有余,生动不足。这一点包括该书作者和象Bjarne Stroustrup这样的大师都从不讳言。实际上Design Pattern并非一定是晦涩难懂的,通过生动的例子,一个中等水平的C++学习者完全可以掌握基本用法,在自己的编程实践中使用,得到立竿见影的功效。这两篇文章就是很好的例证。本文翻译在保证技术完整性的前提下作了不少删节和修改,以便使文章显得更紧凑。

----------------------------------------------------------

人物介绍:

我 --- 一个追求上进的C++程序员,尚在试用期,聪明但是经验不足。

Wendy --- 公司里的技术大拿,就坐在我旁边的隔间里,C++大虾,最了不起的是,她是个女的!她什么都好,就是有点刻薄,

我对她真是又崇拜又嫉妒。

----------------------------------------------------------

I. Virtually Yours -- Template Method模式

我在研究Wendy写的一个类。那是她为这个项目写的一个抽象基类,而我的工作就是从中派生出一个具象类(concrete class)。这个类的public部分是这样的:

class Mountie {
public: 
void read( std::istream & ); 
void write( std::ostream & ) const; 
virtual ~Mountie(); 
很正常,virtual destructor表明这个类打算被继承。那么再看看其protected部分:

protected: 
virtual void do_read( std::istream & ); 
virtual void do_write( std::ostream & ) const;

也不过就是一会儿的功夫,我识破了Wendy的把戏:她在使用template method模式。public成员函数read和write是非虚拟的,它们肯定是调用protected部分do_read/do_write虚拟成员函数来完成实际的工作。啊,我简直为自己的进步而飘飘然了!哈,Wendy,这回你可难不住我,还有什么招数?尽管放马过来... 突然,笑容在我脸上凝固,因为我看到了其private部分:

private: 
virtual std::string classID() const = 0;

这是什么?一个private纯序函数,能工作么?我站了起来,

“Wendy,你的Mountie类好像不能工作耶,它有一个private virtual function。”

“你试过了?”她连头都不抬。

“嗯,那倒是没有啦,可是想想也不行啊?我的派生类怎么能override你的private函数呢?” 我嘟囔着。

“嗬,你倒是很确定啊!”Wendy的声音很轻柔,“你怎么老是这也不行,那也不行的,这几个月跟着我你就没学到什么东西吗?小菜鸟。”

真是可恶啊...

“小菜鸟,你全都忘了,访问控制级别跟一个函数是不是虚拟的根本没关系。判断一个函数是动态绑定还是静态绑定是函数调用解析的最后一个步骤。好好读读标准的3.4和5.2.2节吧。”

我完全处于下风,只好采取干扰战术。“好吧,就算你说的不错,我也还是不明白,何必把它设为private?”

“我且问你,倘若你不想让一个类中的成员函数被其他的类调用,应当如何处理?”

“当然是把它设置为private的,” 我回答道。

“那么你去看看我的Mountie类实现,特别是write()函数的实现。”

我正巴不得逃开Wendy那刺人的目光,便转过头去在我的屏幕上搜索,很快,我找到了:

void Mountie::write(std::ostream &Dudley) const 
{
Dudley << classID() << std::endl; 
do_write(Dudley); 
} 
嗨,最近卡通片真是看得太多了,居然犯这样的低级失误。还是老是承认吧:“好了,我明白了。classID()是一个实现细节,用来在保存对象时指示具象类的类型,派生类必须覆盖它,所以必须是纯虚的。但是既然是实现细节,就应该设为private的。”

“这还差不多,小菜鸟。”大虾点了点头,“现在给我解释一下为什么do_read()和do_write()是protected的?”

这个问题并不难,我组织了一下就回答:“因为派生类对象需要调用这两个函数的实现来读写其中的基类对象。”

“很好很好,”大虾差不多满意了,“不过,你再解释解释为什么我不把它们设为public的?”

现在我感觉好多了:“因为调用它们的时候必须以一种特定的方式进行。比如do_write()函数,必须先把类型信息写入,再把对象信息写入,这样读取的时候,负责生成对象的模块首先能够知道要读出来的对象是什么类型的,然后才能正确地从流中读取对象信息。”

“聪明啊,我的小菜鸟!”Wendy停顿了一下,“就跟学习外国口语一样,学习C++也不光是掌握语法而已,还必须要掌握大量的惯用法。”

“是啊是啊,我正打算读Coplien的书...”

[译者注:就是James Coplien 1992年的经典著作Advanced C++ Programming Style and Idioms]

大虾挥了挥她的手,“冷静,小菜鸟,我不是指先知Coplien的那本书,我是指某种结构背后隐含的惯用法。比如一个类有virtual destructor,相当于告诉你说:‘嗨,我是一个多态基类,来继承我吧!’ 而如果一个类的destructor不是虚拟的,则相当于是在说:‘我不能作为多态基类,看在老天的份上,别继承我。’”

“同样的,virtual函数的访问控制级别也具有隐含的意义。一个protected virtual function告诉你:‘你写的派生类应该,哦,可是说是必须调用我的实现。’而一个private virtual function是在说:‘派生类可以覆盖,也可以不覆盖我,随你的便。但是你不可以调用我的实现。’”

我点点头,告诉她我懂了,然后追问道:“那么public virtual function呢?”

“尽可能不要使用public virtual function。”她拿起一支笔写下了以下代码:

class HardToExtend 
{
public: 
virtual void f(); 
}; 
void HardToExtend::f() 
{
// Perform a specific action 
} 
“假设你发布了这个类。在写第二版时,需求有所变化,你必须改用Template Method。可是这根本不可能,你知道为什么?”

“呃,这个...,不知道。”

“由两种可能的办法。其一,将f()的实现代码转移到一个新的函数中,然后将f()本身设为non-virtual的:

class HardToExtend 
{
// possibly protected 
virtual void do_f(); 
public: 
void f(); 
}; 
void HardToExtend::f() 
{
// pre-processing 
do_f(); 
// post-processing 
} 
void HardToExtend::do_f() 
{
// Perform a specific action 
}

然而你原来写的派生类都是企图override函数f()而不是do_f()的,你必须改变所有的派生类实现,只要你错过了一个类,你的类层次就会染上先知Meyers所说的‘精神分裂的行径’。” [译者注:参见Scott Meyers,Effective C++, Item 37,绝对不要重新定义继承而来的非虚拟函数]

“另一种办法是将f()移到private区域,引入一个新的non-virtual函数:”

class HardToExtend 
{
// possibly protected 
virtual void f(); 
public: 
void call_f(); 
}; 
“这会导致无数令人头痛的问题。首先,所有的客户都企图调用f()而不是call_f(),现在它们的代码都不能编译了。更有甚者,大部分派生类都回把f()放在public区域中,这样直接使用派生类的用户可以访问到你本来想保护的细节。”

“对待虚函数要象对待数据成员一样,把它们设为private的,直到设计上要求使用更宽松的访问控制再来调整。要知道由private入public易,由public入private难啊!”

[译者注:这篇文章所表达的思想具有一定的颠覆性,因为我们太容易在基类中设置public virtual function了,Java中甚至专门为这种做法建立了interface机制,现在竟然说这不好!一时间真是接受不了。但是仔细体会作者的意思,他并不是一般地反对public virtual function,只是在template method大背景下给出上述原则。虽然这个原则在一般的设计中也是值得考虑的,但是主要的应用领域还是在template method模式中。当然,template method是一种非常有用和常用的模式,因此也决定了本文提出的原则具有广泛的意义。]

----------------------------------------------------------------

II. Visitor模式

我正在为一个设计问题苦恼。试用期快结束了,我希望自己解决这个问题,来证明自己的进步。每个人都记得自己的第一份工作吧,也都应该知道在这个时候把活儿做好是多么的重要!我亲眼看到其他的新雇员没有过完试用期就被炒了鱿鱼,就是因为他们不懂得如何对付那个大虾...,别误会,我不是说她不好,她是我见过最棒的程序员,可就是有点刻薄古怪...。现在我拜她为师,不为别的,就是因为我十分希望能达到她那个高度。

我想在一个类层次(class hierarchy)中增加一个新的虚函数,但是这个类层次是由另外一帮人维护的,其他人碰都不能碰:

class Personnel 
{
public: 
virtual void Pay ( ) = 0; 
virtual void Promote( ) = 0; 
virtual void Accept ( PersonnelV& ) = 0; 
// ... other functions ... 
};

class Officer : public Personnel {}; 
class Captain : public Officer {}; 
class First : public Officer {}; 
我想要一个函数,如果对象是船长(Captain)就这么做,如果是大副(First Officer)就那么做。Virtual function正是解决之道,在Personnel或者Officer中声明它,而在Captain和First覆盖(override)它。

糟糕的是,我不能增加这么一个虚函数。我知道可以用RTTI给出一个解决方案:

void f( Officer &o ) 
{
if( dynamic_cast<Captain*>(&o) ) 

else if( dynamic_cast<First*>(&o) ) 

}

int main() 
{
Captain k; 
First s; 
f( k ); 
f( s ); 
} 
但是我知道使用RTTI是公司编码标准所排斥的行为,我对自己说:“是的,虽然我以前不喜欢RTTI,但是这回我得改变对它的看法了。很显然,除了使用RTTI,别无它法。”

“任何问题都可以通过增加间接层次的方法解决。”

我噌地一下跳起来,那是大虾的声音,她不知道什么时候跑到我背后,“啊哟,您吓了我一跳...您刚才说什么?”

“任何问...”

“是的,我听清楚了,”我也不知道哪来的勇气,居然敢打断她,“我只是不知道您从哪冒出来的。”其实这话只不过是掩饰我内心的慌张。

“哈,算了吧,小菜鸟,”大虾斜着眼看着我,“你以为我不知道你心里想什么!”她把声音提高了八度,直盯着我,“那些可怜的C语言门徒才会使用switch语句处理不同的对象类型。你看:”


void f(struct someStruct *s) 
{
switch(s->type) {
case APPLE: 

break; 
case ORANGE: 

break; 

} 
} 
“这些人学习Stroustrup教主的C++语言时,最重要的事情就是学习如何设计好的类层次。”

“没错,”我又一次打断她,迫不及待地想让Wendy明白,我还是有两下子的,“他们应该设计一个Fruit基类,派生出Apple和Orange,用virtual function来作具体的事情。

“很好,小菜鸟。C语言门徒通常老习惯改不掉。但是,你应该知道,通过使用virtual function,你增加了一个间接层次。”她放下笔,“你所需要的不就是一个新的虚函数吗?”

“是的。可是我没有权力这么干。”

“因为你无权修改类层次,对吧!”

“您终于了解了情况,我们没法动它。也不知道这个该死的类层次是哪个家伙设计的...” 我嘀嘀咕咕着。

“是我设计的。”

“啊...,真的?!这个,嘿嘿...”,我极为尴尬。

“这个类层次必须非常稳定,因为有跨平台的问题。但是它的设计允许你增加新的virtual function,而不必烦劳RTTI。你可以通过增加一个间接层次的办法解决这个问题。请问,Personnel::Accept是什么”

”嗯,这个...”

“这个类实现了一个模式,可惜这个模式的名字起得不太好,是个PNP,叫Visitor模式。”

[译者注:PNP,Poor-Named Pattern, 没起好名字的模式]

“啊,我刚刚读过Visitor模式。但是那只不过是允许若干对象之间相互迭代访问的模式,不是吗?”

她叹了一口气,“这是流行的错误理解。那个V,我觉得毋宁说是Visitor,还不如说是Virtual更好。这个PNP最重要的用途是允许在不改变类层次的前提下,向已经存在的类层次中增加新的虚函数。首先来看看Personnel及其派生类的Accept实现细节。”她拿起笔写下:

void Personnel::Accept( PersonnelV& v ) 
{v.Visit( *this );}

void Officer::Accept ( PersonnelV& v ) 
{v.Visit( *this );}

void Captain::Accept ( PersonnelV& v ) 
{v.Visit( *this );}

void First::Accept ( PersonnelV& v ) 
{v.Visit( *this );} 
“Visitor的基类如下:”

class PersonnelV 
{
public: 
virtual void Visit( Personnel& ) = 0; 
virtual void Visit( Officer& ) = 0; 
virtual void Visit( Captain& ) = 0; 
virtual void Visit( First& ) = 0; 
}; 
“啊,我记起来了。当我要利用Personnel类层次的多态性时,我只要调用Personnel::Accept(myVisitorObject)。由于Accept是虚函数,我的myVisitorObject.Visit()会针对正确的对象类型调用,根据重载法则,编译器会挑选最贴切的那个Visit来调用。这不相当于增加了一个新的虚拟函数了吗?”

“没错,小菜鸟。只要类层次支持Accept,我们就可以在不改动类层次的情况下增加新的虚函数了。”

“好了,我现在知道该怎么办了”,我写道:

class DoSomething : public PersonnelV 
{
public: 
virtual void Visit( Personnel& ); 
virtual void Visit( Officer& ); 
virtual void Visit( Captain& ); 
virtual void Visit( First& ); 
};

void DoSomething::Visit( Captain& c ) 
{
if( femaleGuestStarIsPresent ) 
c.TurnOnCharm(); 
else 
c.StartFight(); 
}

void DoSomething::Visit( First& f ) 
{
f.RaiseEyebrowAtCaptainsBehavior(); 
} 
void f( Personnel& p ) 
{
p.Accept( DoSomething() ); // 相当于 p.DoSomething() 
}

int main() 
{
Captain k; 
First s;

f( k ); 
f( s ); 
} 
大虾满意地笑了,“也许这个模式换一个名字会更好理解,可惜世事往往不遂人意...”。

ArcGIS Pro2.5深度学习使用注意事项 ArcGIS Pro2.5深度学习使用中的注意事项1、ArcGIS Pro2.5深度学习使用中的注意事项1.1、Label深度学习样本工具参数介绍1.2、准备数据参数介绍1.3、训练模型介绍1.4、ArcGIS Pro2.5深度学习推理工具和模型对应关系介绍1.4.1、Classify Objects Using Deep Learning1.4.2、Classify Pixels Using D... 阅读详情

相关推荐

小米手机安装Burp证书失败原因与DER格式改造指南

CA证书是HTTPS抓包和移动安全测试的基础组件,其本质是遵循X.509标准的数字凭证,依赖ASN.1/DER编码格式与完整证书链实现可信验证。Android系统默认支持PEM和DER两种编码,但MIUI自14版本起强化安全策略,仅识别DER二进制格式,并强制校验Subject Alternative Name(SAN)、Basic Constraints及证书链完整性。这种企业级加固逻辑导致Burp Suite导出的PEM格式.cer文件在小米设备上‘安装成功但无效’。本文聚焦证书格式转换、SAN字段注入

weixin_33646989的博客 625

C++失去竞争力了吗?《C/C++ User's Journal》停刊有感

转一个朋友文章片断:这是真的,多年来一直被C++界视为珍宝的《C/C++ Users Journal》停刊了。最开始在comp.lang.c++新闻组上读到这条消息时,我还以为是哪一位反C++的老兄在不怀好意地开玩笑,他心灾乐祸地宣称:“C和C++正在走向消亡”。不过,CUJ的主编P.J. Plauger很快就出来予以证实——这一切是真的,已经出版20多年汇集了众多C++顶尖专家

书 人 事 9726

OpenResty无损升级内嵌nginx版本0DAY漏洞

OpenResty无损升级内嵌nginx版本,nginx

qq1071643762的博客 6904

与大虾对话: 领悟设计模式

 与大虾对话: 领悟设计模式--Template Method / Visitor[译者按] 本文根据发表在CUJ Expert Forum上的两篇文章编译而成。C/C++ Users Journal是目前最出色的C/C++语言专业杂志,特别是在C++ Report闭刊之后,CUJ的地位更加突出。CUJ Expert Forum是CUJ主办的网上技术专栏,汇集2000年10月以来C+

孟岩 7077

Deep Reinforcement Learning - 1. DDPG原理和算法

Deep Reinforcement Learning - 1. DDPG原理和算法 背景描述 DDPG的定义和应用场景 DDPG算法相关基本概念定义 DDPG实现框架和算法 DDPG对于DPG的关键改进 下一篇 以下用RL作为Reinforcement Learning 的简称。背景描述概括来说,RL要解决的问

kenneth_yu的博客 14万+

超级详细!!!新手使用AutoDL跑深度学习(MixVPR项目)的学习记录

新手第一次在AutoDL上跑深度学习的实验,遇到一些问题,简单记录一下。如果大家发现问题,欢迎和我讨论。

weixin_45152283的博客 5万+

近来维护一个项目,里面用的CUJ里的代码,想下来看看。。本来面目。找了半天。

 C++ user journal 代码及文章: http://download.chinaprj.cn/search/C.Users.Journal.CD-ROM.Release/

Hill的专栏 891

机器人轨迹规划——三次样条轨迹1(基于速度的求解方法)

三次样条轨迹是利用三次样条曲线来对给定的一系列点进行插值,从而得到一个光滑的轨迹。三次样条因为每段的多项式次数最高都是三阶的,所以可以得到2阶导数连续,也就是加速度连续的轨迹。假设给定了一系列的点q0q1q2qnq0​q1​q2​...qn​,以及每个点对应的参数t0t1t2tnt0​t1​t2​...tn​,计算一条三次样条曲线sts(t)st,该曲线通过给定的点q0q1q2qnq0​q1。

出门搔白首,若负平生志 1471

[推荐] C/C++程序员必知的一些国外站点

Free C/C++ Compilers     GCC http://gcc.gnu.org/install/binaries.html     MinGW http://www.mingw.org/     Cygwin (GNU + cygnus + Windows) http://cygwin.com/     Borland C++ Compiler 5.5 http://www

yuanhubilie的专栏 1355

【linux】linux 出现一个网卡绑定多个ip怎么处理

我的原因是我使用了dhcp,这个会自动分配ip, 将 BOOTPROTO 参数由 dhcp 改为 static,并移除或注释掉与动态分配地址 10.0.2.15 相关的配置行(如果存在)。执行后,您可以再次使用 ip -4 a 或 ifconfig 命令确认 IP 地址已被移除。输出可以看出,您的虚拟机(k8s003)中存在一个名为 enp0s3 的网卡绑定了多个 IPv4 地址。对于某些 Linux 发行版(如 Ubuntu、Debian),您可能需要编辑网络接口的配置文件来移除多余的 IP 地址。

九师兄 297

ollama如何在自己的服务器上跑Qwen3(小白必看)

在国内下载ollama最新安装包时,使用curl -fsSL https://ollama.com/install.sh | sh命令可能会遇到网络问题导致下载进度停滞。为了解决这一问题,可以通过国内的镜像源进行下载,例如使用modelscope社区提供的最新ollama镜像仓库。具体步骤包括:首先通过pip install modelscope安装modelscope,然后使用modelscope download命令下载ollama-linux镜像,接着进入下载目录并赋予安装脚本执行权限,最后运行安装脚

Ven%的博客 756

设计模式之 template method 与 visitor

<br />template method要注意一点的是在父类中定义了一个框架,而这个框架中有几个步骤可以由子类来重载或者扩充,这是template method和我最初想得不一样的地方,我开始把他当成了factory method的东西了。这个两个还是非常类似的。<br />visitor模式,有点像STL中的函数对象,尤其是我在copy,find的时候。但感觉比函数对象又要深刻一点。<br />visitor模式目的是把数据结构的组织和处理分开来,啥意思呢,假如我遍历一个列表,同时想对某个元素处理点东西

Innost的专栏 2776

petalinux2022.2启动文件编译配置

petalinux2021之前的版本里,导入目录components/yocto/下的“environment-setup-aarch64-xilinx-linux” 和 “layers/core/oe-init-build-env”的环境,能使用Yocto的devtool工具。(加入下面内容,先petalinux-build下载完成后才能有源码)制作 BOOT.BIN 启动文件:petalinux-package --boot --fsbl --fpga --u-boot --force。

wangjie36的博客 1391

Template Method / Visitor

侯捷注:本文承译者孟岩先生应允,转载於此以飨台湾读者,非常感谢。未得孟岩先生之同意,任何人请勿将此文再做转载。以下红色为译注,紫色为侯捷个人认为宜再斟酌之处。浅蓝色是侯捷个人阅读时的神秘标记。本文系由GB简码直接转为BIG5繁码,并未将大陆惯用术语转换为台湾惯用术语。由於字码直接转译,也许有些字在简体中是正确的,转为繁体後音同字异,或字同义异,或┅。某些紫色标记也许是这麽来的 :)读後感想:本文

DX 1018

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

领悟设计模式

   

bochunbo的专栏 578

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

C语言对话-31.与大虾对话 领悟设计模式

然而你原来写的派生类都是企图override函数f()而不是do_f()的,你必须改变所有的派生类实现,只要你错过了一个类,你的类层次就会染上先知Meyers所说的‘精神分裂的行径’。“没错,”我又一次打断她,迫不及待地想让Wendy明白,我还是有两下子的,“他们应该设计一个Fruit基类,派生出Apple和Orange,用virtual function来作具体的事情。但是我知道使用RTTI是公司编码标准所排斥的行为,我对自己说:“是的,虽然我以前不喜欢RTTI,但是这回我得改变对它的看法了。

qq_41237850的博客 755

ssm框架搭建

这个是一个在idea上成功跑起来的ssm框架项目,保存在这里方便以后使用直接下载

领悟设计模式(转 netwiser)

--Template Method / Visitor[译者按] 本文根据发表在CUJ Expert Forum上的两篇文章编译而成。C/C++ Users Journal是目前最出色的C/C++语言专业杂志,特别是在C++ Report闭刊之后,CUJ的地位更加突出。CUJ Expert Forum是CUJ主办的网上技术专栏,汇集2000年10月以来C++社群中顶尖专家的技术短文,并免费公开发

612

与大虾对话:领悟设计模式(czmagic转)

译者按] 本文根据发表在CUJ Expert Forum上的两篇文章编译而成。C/C++ Users Journal是目前最出色的C/C++语言专业杂志,特别是在C++ Report闭刊之后,CUJ的地位更加突出。CUJ Expert Forum是CUJ主办的网上技术专栏,汇集2000年10月以来C++社群中顶尖专家的技术短文,并免费公开发布,精彩纷呈,是每一个C/C++学习者不可错过的资料。由

天涯的历程 579

与大虾对话: 领悟设计模式 (转自http://blog.youkuaiyun.com/myan)

[译者按] 本文根据发表在CUJ Expert Forum上的两篇文章编译而成。C/C++ Users Journal是目前最出色的C/C++语言专业杂志,特别是在C++ Report闭刊之后,CUJ的地位更加突出。CUJ Expert Forum是CUJ主办的网上技术专栏,汇集2000年10月以来C++社群中顶尖专家的技术短文,并免费公开发布,精彩纷呈,是每一个C/C++学习者不可错过的资料。

1007
上一篇: [探讨]如何更有效地学习开源项目的代码?
下一篇: retain和release倒底怎么玩?
buffoon1
博客等级 码龄19年 5粉丝 9原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值