我所理解的 C++ 反射机制

1.前言

在实际的项目中,听师兄说项目中用到了 C++ 反射,出于好奇,就查阅相关资料,发现强大的 C++ 本身并不支持反射,反而 Java 支持反射机制。当我得知这个事实时,唯 C++ 马首是瞻的我,心中暗自落泪,悲叹不已。但是,C++ 的拥趸们别难受,强大的 C++ 本身不支持,但却可以让我们手动实现,真的是曲径通幽处,禅房花木深。C++ 是不会辜负我们对它至死不渝的追逐。

但是,说到 Java 的反射机制或者 C++ 用到了反射,如果没有真正的在项目中使用过,我们对它会感觉到陌生和不解。我们会问反射是什么,反射的作用是什么,反射的应用场景又是什么?在你查看了很多资料,可能还是会有疑惑,要不就是被那些官方的术语忽悠的晕头转向,不知东南西北,要不就是被博文作者拙劣的文字和陌生的应用场景弄的丈二和尚摸不着头脑,不知所云。

下面我提一个简单的应用场景,以此作为讲解 C++ 反射机制的切入点。遇到问题,才去探索问题的解决方法。解决问题之后,我们就学到了新的知识。这就是我们常用的学习模式,符合常人的学习求知习惯。如果,我们不知道反射能解决什么问题,或者说我们在工作实践中遇到的问题无需反射来解决,那么我们千辛万苦,煞费苦心去学习这个不常用的东西,意义何在呢?

所以,这里抛出一个问题:如何通过类名称字符串来生成类的对象。比如有一个类 ClassA,那么如何通过类名称字符串 “ClassA” 创建类的对象呢?

在 Java 编程中,会经常用到反射,但是我想很多使用 C++ 的人至今都没有想过这个问题。C++ 是不支持通过类名称字符串 “ClassXX” 来生成对象的,也就是说我们可以使用ClassXX* object =new ClassXX; 来生成对象,但是不能通过ClassXX* object=new "ClassXX"; 来生成对象。

那么我们如何解决这个问题呢?我们就可以通过反射来解决这个问题。找到了问题所在,找到了反射能够解决实际的问题,是不是觉得充满了动力和期待去学习 C++ 如何实现反射来解决这个问题呢?

好了,我们知道了反射能够解决这个问题,但什么是反射呢?我们先看下维基百科的定义:反射是指计算机程序在运行时可以访问、检测和修改它本身状态或行为的一种能力。 有点抽象,实际上反射就是程序在运行时动态获取对象信息以及调用对象方法的能力。

通过类名称创建类对象是反射的功能之一,下面我就慢慢讲解 C++ 中实现反射来解决上面的问题。

2.设计与实现

2.1 设计思路

我的设计思路大致是这样的。
(1)为需要反射的类中定义一个创建该类对象的一个回调函数;
(2)设计一个工厂类,类中有一个 std::map,用于保存类名和创建实例的回调函数。通过类工厂来动态创建类对象;
(3)程序开始运行时,将回调函数存入 std::map 里面,类名字做为 map 的 key 值。

实现流程如下图所示:
这里写图片描述

2.2 具体实现

下面我来一步一步的讲解具体的实现方法。

第一步: 定义一个函数指针类型,用于指向创建类实例的回调函数。

typedef void* (*PTRCreateObject)(void);

第二步: 定义和实现一个工厂类,用于保存保存类名和创建类实例的回调函数。工厂类的作用仅仅是用来保存类名与创建类实例的回调函数,所以程序的整个证明周期内无需多个工厂类的实例,所以这里采用单例模式来涉及工厂类。

// 工厂类的定义
class ClassFactory {
private:  
    map<string, PTRCreateObject> m_classMap ;  
	ClassFactory(){}; //构造函数私有化
	
public:   
    void* getClassByName(string className);  
    void registClass(string name, PTRCreateObject method);
    static ClassFactory& getInstance();
};

// 工厂类的实现

// brief: 获取工厂类的单个实例对象  
ClassFactory& ClassFactory::getInstance(){
    static ClassFactory sLo_factory;  
    return sLo_factory;
}

// brief: 通过类名称字符串获取类的实例
void* ClassFactory::getClassByName(string className){
    map<string, PTRCreateObject>::const_iterator iter;  
    iter = m_classMap.find(className);  
    if (iter == m_classMap.end())  
        return NULL ;  
    else
        return iter->second();  
}  

// brief: 将给定的类名称字符串和对应的创建类对象的函数保存到 map 中
void ClassFactory::registClass(string name, PTRCreateObject method){
    m_classMap.insert(pair<string, PTRCreateObject>(name, method)) ;  
}

第三步: 这一步比较重要,也是最值得深究的一步,更是容易犯迷糊的地方。仔细看,将定义的类注册到工厂类中。也就是说将类名称字符串和创建类实例的回调函数保存到工厂类的 map 中。这里我们又需要完成两个工作,第一个是定义一个创建类实例的回调函数,第二个就是将类名称字符串和我们定义的回调函数保存到工厂类的 map 中。假设我们定义了一个 TestClassA。

// test class A
class TestClassA {
public:
	void m_print() {
		cout << "hello TestClassA" << endl;
	};
};

// 创建类实例的回调函数。
TestClassA* createTestClassA() {
	return new TestClassA;
}

好了,我们完了第一个工作,定义了一个创建类实例的回调函数。下面我们要思考一下如何将这个回调函数和对应的类名称字符串保存到工厂类的 map 中。我这里的一个做法是创建一个全局变量,在创建这个全局变量时,调用的构造函数内将回调函数和对应的类名称字符串保存到工厂类的 map 中。在这里,这个全局变量的类型我们定义为 RegisterAction。

// 注册动作类
class RegisterAction {
public:
	RegisterAction(string className, PTRCreateObject ptrCreateFn) {
		ClassFactory::getInstance().registClass(className, ptrCreateFn);
	}
};

有这个注册动作类,我们在每个类定义完成之后,我们就创建一个全局的注册动作类的对象,通过注册动作类的构造函数将我们定义的类名称和回调函数注册到工厂类的 map 中。可以在程序的任何一个源文件中创建注册动作类的对象,但是在这里,我们放在回调函数后面创建。后面你就知道为什么这么做了。创建一个注册动作类的对象如下:

RegisterAction g_creatorRegisterTestClassA("TestClassA", (PTRCreateObject)createTestClassA);   

到这里,我们就完成将类名称和创建类实例的回调函数注册到工厂类的 map。下面再以另外一个类 TestClassB 为例,重温一下上面的步骤:

// test class B
class TestClassB {
public:
	void m_print() {
		cout << "hello TestClassB" << endl;
	};
};

// 创建类实例的回调函数
TestClassB* createTestClassB() {
	return new TestClassB;
}

// 注册动作类的全局实例
RegisterAction g_creatorRegisterTestClassB("TestClassB",(PTRCreateObject)createTestClassB);

聪明的你有没有发现,如果我们再定义一个类 C、D…,我们重复的在写大量相似度极高的代码。那么如何偷懒让代码变得简洁,提高编码效率呢。

有时我们就应该偷懒,不是说这个世界是懒人们创造的么,当然这些懒人们都很聪明。那么我们如何偷懒呢,如果你想到了宏,恭喜,答对了。其实仔细一看,包括回调函数的定义和注册动作类变量的定义,每个类的代码除了类名外其它都是一模一样的,那么我们就可以用下面的宏来替代这些重复代码。

#define REGISTER(className) 											\
	className* objectCreator##className(){     							\
        return new className;                                         	\
    }                                                                  	\
    RegisterAction g_creatorRegister##className(                        \
		#className,(PTRCreateObject)objectCreator##className)

有了上面的宏,我们就可以在每个类后面简单的写一个 REGISTER(ClassName) 就完成了注册的功能,是不是很方便快捷呢!!!

2.3 测试

至此,我们就完成了 C++ 反射的部分功能,为什么是部分功能,后面再另外说明。急不可耐,我们先来测试一下,是否解决了上面我们提到的问题:如何通过类的名称字符串来生成类的对象。测试代码如下:

#include <map>
#include <iostream>
#include <string>
using namespace std;

// test class
class TestClass {
public:
	void m_print() {
		cout<<"hello TestClass"<<endl;
	};
};
REGISTER(TestClass);

int main(int argc,char* argv[]) {
	TestClass* ptrObj = (TestClass*)ClassFactory::getInstance().getClassByName("TestClass");
	ptrObj->m_print();
}

程序编译运行输出:
这里写图片描述

2.4 可能存在的疑问

看了上面的测试代码,大家可能会唏嘘不已,我们在通过类名称字符串创建类实例的时候,我们还是需要用到类名进行强制类型转换,有了类名称,我们何必还要处心积虑实现反射的功能呢,直接用类名创建实例不就行了么?

其实,上面实现的反射只是解决了本文最初提出的问题。那么在实际的项目中,还有一种应用场景就是我们定义好了基类,给客户继承,但是我们并不知道客户继承基类后的子类名称。不过我们可以通过配置文件说明客户实现的具体子类名称,这样我们就可以通过类名称字符串来创建客户自定义类的实例了。

3.还有其它注册方法吗?

上面具体讲解了通过实现 C++ 的反射来达到通过类名称字符串创建类的实例。其中,在对需要反射的类进行注册的时候,我们用到了一个注册动作类的全局变量,来辅助我们达到注册的功能。除了这个方法,还有没有别的方法呢?大家可以想一想。

仔细一想,我们通过全局对象的构造函数将类创建实例的函数注册到工厂类中,因为全局对象的构造函数是在程序进入 main() 函数之前执行的,这个问题可以抽象为 C/C++ 中如何在 main() 函数之前执行一条语句。

主要有以下几种方法:

(1)全局变量的构造函数。
也就是上面介绍的通过全局对象的构造函数来实现在main函数之前执行想要的操作。但是很明显的副作用就是定义了一个不从使用的全局变量,从出生,完成使命,就被我们无情的抛弃。

(2)全局变量的赋值函数。
跟上面的方法有异曲同工之妙,但也同样有着上面的副作用。参考如下代码:

#include <iostream>
using namespace std;

int foo(void);
int i = foo();

int foo(void) {
	cout << "before main" << endl;
	return 0;
}

int main(void) {
	cout << "I'm main" << endl;
}

(3)使用 GCC 的话,可以通过 attribute 关键字声明 constructor 和 destructor 分别规定函数在 main 函数之前执行和之后执行。

#include <stdio.h>

__attribute((constructor)) void before_main() {
    printf("%s/n",__FUNCTION__);
}  

__attribute((destructor)) void after_main() {
    printf("%s/n",__FUNCTION__);  
}

int main( int argc, char ** argv) {
    printf("%s/n",__FUNCTION__);
    return 0;  
}

(4)指定入口点,入口点中调用原来的入口点。
在使用 gcc 编译 C 程序时,我们可以使用 linker 指定入口,使用编译选项 -e 指明程序入口函数。

// test.c
#include<stdio.h>

int main(int argc, char **argv) {
	printf("main\n");
	return 0;
}

int xiao(int argc, char **argv) {
	printf("xiao\n");
	return main(argc, argv);
}

编译语句可以为:gcc -e xiao test.c

上面是知乎用户提出的方法,但是当我在测试的时候,运行到 main 函数中,总是会出现段错误。C++ 程序时,使用 g++ 如法炮制,编译时记得给新的入口函数添加 extern “C” 说明,以防 g++ 编译时改变了函数签名。虽然编译可以通过,但也是执行到 main 函数时抛出Segmentation fault (core dumped)

有兴趣的读者可以尝试一下,如果解决了,请留言告知。

(5)可以用 main 调用 main 实现在 main 前执行一段代码,如下:

#include<stdio.h>
#include<stdbool.h>

int main(int argc, char **argv) {
	static _Bool firstTime = true;
	if(firstTime) {
		firstTime = false;
		printf("BEFORE MAIN\n");
		return main(argc, argv);
	}

	printf("main\n");
	return 0;
}

4.小结

这里先解释一下上文中 2.3 节中提出的问题,为什么只是完成了 C++ 反射的部分功能,因为我们在上面并没有完整地实现 C++ 的反射机制,只能实现了反射机制中的一个小功能而已,即通过类名称字符串创建类的实例。除此之外,据我所知,编程语言的反射机制所能实现的功能还有通过类名称字符串获取类中属性和方法,修改属性和方法的访问权限等。

我们为什么需要反射机制。由于在 Java 和 .NET 的成功应用,反射技术以其明确分离描述系统自身结构、行为的信息与系统所处理的信息,建立可动态操纵的因果关联以动态调整系统行为的良好特征,已经从理论和技术研究走向实用化,使得动态获取和调整系统行为具备了坚实的基础。当需要编写扩展性较强的代码、处理在程序设计时并不确定的对象时,反射机制会展示其威力,这样的场合主要有:

  • 序列化(Serialization)和数据绑定(Data Binding);
  • 远程方法调用(Remote Method Invocation RMI);
  • 对象/关系数据映射(O/R Mapping)。

当前许多流行的框架和工具,例如 Castor(基于 Java 的数据绑定工具)、Hibernate(基于 Java 的对象/关系映射框架)等,其核心都使用了反射机制来动态获得类型信息。因此,能够动态获取并操纵类型信息,已经成为现代软件的标志之一。

反射机制如此复杂,C++ 尚不支持,岂是我这种三教九流之辈的只言片语和几个代码片段所能够勾勒描绘的。

下面附上本文用到的完整代码,均写在一个源文件中,大家可以根据实际应用,将不同功能的代码写在不同的文件中。也可以在此基础上,进行功能扩充和改良。

#include <map>
#include <iostream>
#include <string>
using namespace std;

typedef void* (*PTRCreateObject)(void);  

class ClassFactory {
private:  
    map<string, PTRCreateObject> m_classMap ;  
	ClassFactory(){}; //构造函数私有化
	
public:   
    void* getClassByName(string className);  
    void registClass(string name, PTRCreateObject method) ;  
    static ClassFactory& getInstance() ;  
};

void* ClassFactory::getClassByName(string className){  
    map<string, PTRCreateObject>::const_iterator iter;  
    iter = m_classMap.find(className) ;  
    if ( iter == m_classMap.end() )  
        return NULL ;  
    else  
        return iter->second() ;  
}  
   
void ClassFactory::registClass(string name, PTRCreateObject method){  
    m_classMap.insert(pair<string, PTRCreateObject>(name, method)) ;  
}  
   
ClassFactory& ClassFactory::getInstance(){
    static ClassFactory sLo_factory;  
    return sLo_factory ;  
}  

class RegisterAction{
public:
	RegisterAction(string className,PTRCreateObject ptrCreateFn){
		ClassFactory::getInstance().registClass(className,ptrCreateFn);
	}
};

#define REGISTER(className) 											\
	className* objectCreator##className(){     							\
        return new className;                                         	\
    }                                                                  	\
    RegisterAction g_creatorRegister##className(                        \
		#className,(PTRCreateObject)objectCreator##className)

// test class
class TestClass{
public:
	void m_print(){
		cout<<"hello TestClass"<<endl;
	};
};
REGISTER(TestClass);

int main(int argc,char* argv[]) {
	TestClass* ptrObj=(TestClass*)ClassFactory::getInstance().getClassByName("TestClass");
	ptrObj->m_print();
}

参考文献

C++反射机制的实现 - CSDN
C++反射机制的一种简单实现.[J].鲍亮, 陈平.计算机工程, 2006, 32(16):95-96
C/C++中如何在main()函数之前执行一条语句?
C/C++中如何在main()函数之前执行一条语句?- 知乎
谈谈C++如何实现反射机制 - 知乎

  • 29
    点赞
  • 133
    收藏
    觉得还不错? 一键收藏
  • 29
    评论
现在流行的Windows下的编程语言实在不少,所以在BBS上常常有人会问:我应该使用什么编程语言呢?其中,有一个大家认可的答案:真正的程序员使用Visual C++。 的确,Visual C++是一个功能强大、灵活、方便的编程工具,可以完成其他编程语言所无法完成的任务,可以让程序员方便地实现自己的设计,尽情的发挥自己地创造性。 Visual C++的强大无比的功能除了得益于C++的特性之外,更重要的是它具有体系完整、机制灵活、功能丰富的MFC类库。 所以,要讲Visual C++,必须讲MFC类库。 MFC的类库可以分两个层次,首先是实现MFC编程框架体系的核心MFC类库,然后是建立在核心MFC类库基础之上的扩展类库,例如,支持COM的类库,实现网络功能的类库,等等。随着Visual C++的不断升级,MFC类库的功能越来越丰富,越来越强大,但是,MFC核心类库是相对稳定的,特别是从Visual C++ 4.2开始到现在的Visual C++6.0。 本书的中心就是深入浅出地解析MFC类库,分析怎么使用MFC类库以及MFC类库的内部实现,揭开MFC复杂、深奥的面纱,让读者对MFC有一个全面、透彻、清晰的理解。关于MFC的核心实现,主要有以下几个方面。 首先,MFC采用C++的面向对象的特征封装了Windows的对象和Win32函数,一定程度上隐蔽了底层Win32的复杂性。 其次,MFC采用消息映射的方法来处理Windows消息和事件,隐藏了Windows窗口的窗口过程,简化了消息处理的复杂性和烦琐性。 还有,MFC提供了一个以文档-视为中心的编程模式,并实现了以文档-视为中心的编程框架,简化了数据处理的过程。 而且,MFC提出了模块状态、线程状态、模块线程状态来支持多线程的编程设计和DLL的编程。 本书分别从使用MFC的角度和MFC内部设计及实现的角度讨论了上述内容,分析了MFC核心的设计和实现;然后,在此基础上,进一步讨论了MFC对一些常用类的实现。有关章节的内容如下: 第一章,MFC概述。 第二章,解释MFC对Win32 API和Windows对象的封装,讨论各类MFC对象的使用,分析MFC对象和Windows对象的关系。 第三章,讨论CObject的特性及其实现,包括动态类信息、动态创建、序列化的实现等内容。 第四章,讨论MFC的消息映射机制,分析MFC对各类消息的处理,例如对Windows消息、控制通知消息、命令消息、状态更新消息、反射消息的处理等;并揭示了MFC通过消息映射手段实现C++虚拟函数机制的原理。 第五章和第六章,分析MFC编程框架启动和关闭一个应用程序的过程,揭示MFC框架的内幕,剖析以文档模板为核心创建基于文档-视的应用程序的过程,展示MFC框架处理消息和调用虚拟函数的时机和位置。 第七、八、九章,介绍MFC的动态链接库、进程、线程等概念,以及MFC动态链接库的种类和使用,讨论MFC下多线程编程的问题。并且进一步阐述MFC的核心概念之一:状态(模块状态、线程状态、模块线程状态),揭示MFC对多线程的支持机制,MFC实现规则DLL和扩展DLL的内幕。 第十章,阐述MFC下的调试手段。 第十一章,讨论CFile类,主要分析了CFile的使用和它对Win32文件函数的封装。 第十二章,讨论模式和无模式对话框,分析MFC如何设计和实现这两种对话框的功能,分析CDialog和CFormView为实现有关功能而设计的虚拟函数、消息处理函数等。 第十三章,讨论MFC工具栏和状态栏的设计及其实现,分析MFC是如何以CControlBar为基础,派生出CStatusBar、CToolBar、CDialogBar等子类,实现MFC工具栏和状态栏标准处理。 第十四章,讨论MFC的Socket类。 第一章到第十章介绍了MFC的核心概念以及实现。在此基础上,第十一章到第十四章讨论了MFC一些常用类的实现。 本书的内容对MFC的初学者(最好对Visual C++和Windows有所了解)和提高者都是很有帮助的。 如果您是一个初学者,可以读第一至第六章。主要目的是建立对MFC的全面理解,了解MFC框架是如何支持程序员编程的。如果有读不懂的地方,可以跳过,直接阅读有关分析的结论。特别是第五章和第六章,可以重点阅读,了解MFC是怎样来处理有关消息、调用有关虚拟函数的。 然后,还可以读第十章,第十一至第十四章。特别第十二章,可以重点阅读,它是MFC从CWnd或者CView派生出特定的类实现特定功能的例子,可以帮助您进一步理解MFC,并且学习如何设计和实现一个特定的类。 如果您对MFC有一定的掌握,可以进一步阅读第八和第九章,了解MFC处理DLL和线程的知识。对于第一至第六章、第十至第十四
Java是一种广泛使用的面向对象的编程语言,由Sun Microsystems公司于1995年5月正式发布。它的设计目标是“一次编写,到处运行(Write Once, Run Anywhere)”,这意味着开发者可以使用Java编写应用程序,并在支持Java的任何平台上无需重新编译即可运行,这得益于其独特的跨平台性,通过Java虚拟机(JVM)实现不同操作系统上的兼容。 Java的特点包括: 面向对象:Java全面支持面向对象的特性,如封装、继承和多态,使得代码更易于维护和扩展。 安全:Java提供了丰富的安全特性,如禁止指针运算、自动内存管理和异常处理机制,以减少程序错误和恶意攻击的可能性。 可移植性:Java字节码可以在所有安装了JVM的设备上执行,从服务器到嵌入式系统,再到移动设备和桌面应用。 健壮性与高性能:Java通过垃圾回收机制确保内存的有效管理,同时也能通过JIT编译器优化来提升运行时性能。 标准库丰富:Java拥有庞大的类库,如Java SE(Java Standard Edition)包含基础API,用于开发通用应用程序;Java EE(Java Enterprise Edition)提供企业级服务,如Web服务、EJB等;而Java ME(Java Micro Edition)则针对小型设备和嵌入式系统。 社区活跃:Java有着全球范围内庞大的开发者社区和开源项目,持续推动技术进步和创新。 多线程支持:Java内建对多线程编程的支持,使并发编程变得更加简单直接。 动态性:Java可以通过反射、注解等机制实现在运行时动态加载类和修改行为,增加了程序的灵活性。 综上所述,Java凭借其强大的特性和广泛的适用范围,在企业级应用、互联网服务、移动开发等领域均扮演着举足轻重的角色,是现代软件开发不可或缺的重要工具之一。
反射机制是一种编程语言的特性,它允许程序在运行时获取类的信息并在运行时动态地创建对象、调用方法等。在C++中,反射机制需要通过元编程技术实现,下面是一份简单的C++反射机制代码示例: ```c++ #include <iostream> #include <string> #include <map> #include <functional> // 定义一个基类 class Base { public: virtual void print() const { std::cout << "Base" << std::endl; } }; // 定义一个派生类 class Derived : public Base { public: void print() const override { std::cout << "Derived" << std::endl; } }; // 定义一个反射类 template <typename T> class Reflector { public: // 注册类型信息 static void registerType(const std::string& name) { typeMap()[name] = []() -> Base* { return new T(); }; } // 创建对象 static Base* create(const std::string& name) { auto it = typeMap().find(name); if (it != typeMap().end()) { return it->second(); } else { return nullptr; } } private: // 类型名称和构造函数的映射表 static std::map<std::string, std::function<Base*()>>& typeMap() { static std::map<std::string, std::function<Base*()>> s_typeMap; return s_typeMap; } }; // 注册类型信息 static int dummy = (Reflector<Base>::registerType("Base"), Reflector<Derived>::registerType("Derived"), 0); int main() { // 创建对象 Base* b = Reflector<Base>::create("Base"); if (b) { b->print(); delete b; } Base* d = Reflector<Base>::create("Derived"); if (d) { d->print(); delete d; } return 0; } ``` 在上述示例中,我们定义了一个基类Base和一个派生类Derived,然后定义了一个Reflector反射类,用于注册类型信息和创建对象。具体步骤如下: 1. 在Reflector类中定义一个静态成员函数registerType,用于注册类型信息。我们使用std::map来维护类型名称和构造函数的映射关系,将构造函数封装成std::function类型,方便调用。 2. 在Reflector类中定义一个静态成员函数create,用于创建对象。我们在typeMap中查找类型名称对应的构造函数,并调用它来创建对象。 3. 在主函数中,我们先使用Reflector<Base>::create来创建一个Base对象,并调用它的print方法。然后使用Reflector<Base>::create来创建一个Derived对象,并调用它的print方法。 4. 在程序中其他地方,我们可以通过Reflector类来注册更多的类型信息,并动态地创建对象。 需要注意的是,在实际的编程中,我们还需要考虑类型的安全性和可扩展性等问题,上述示例仅仅是一个简单的反射机制实现。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值