C++名字查找

C++名字查找有两个方法:
一个是OL(ordinary name lookup) 普通查找规则
一个是ADL(argument-depentment lookup)依赖于实参的名字查找


一、简单引入

这两个查找规则就是C++的查找规则,如果经过这两个规则还是没有找过的话,那编译器就会报出没有匹配函数这样的错误。OL这个规则是从相邻的作用域开始进行查找,如果没有找过的话,那就到更加大的一个作用域去进行查找,OL有这样的一个规则(OL terminates as soon as the name is found),也就是说当编译器在这个作用域中找到了与要找的函数名相同的时候,这就不再会到更加大的作用域中去寻找。也就说停止在这个地方了。如果存在有重载的问题,那编译器会在找到的这个作用域内的进行考虑,到底哪个函数才是最为匹配的,但是它不会往外层进行查找了。这就有可能会形成错误。

ADL规则的意思就是和字面意思差不多,编译器根据实参的类型,去包含着这些类型的名字空间中去查找我们所要的函数定义或者名字。如果是类的话,那就可能包含了它的本身还是他所有基类的名字空间,如果是模板类的话,那就是定义原型模板的名字空间和所有模板实参的名字空间。

#include <iostream>
#include <string>
#include <vector>
#include <iterator>
#include <algorithm>
namespace test
{
    class A
    {
	public:
	    A():str_("")
	    {
	    }
	    A(std::string str):str_(str)
	    {
	    }
	    void setstr(std::string const &str)
	    {
		this->str_ = str;
	    }
	    
	    std::string getstr() const
	    {
		return this->str_;
	    }
	private:
	    std::string str_;
    };
}
std::istream& operator>>(std::istream& in,test::A& thiz)
{
    std::string str_tmp;
    if(in >> str_tmp)
    {
	thiz.setstr(str_tmp);	
    }
    else
    {
	thiz.setstr("wrong");
    }
    return in;
}
std::ostream& operator<<(std::ostream& out,test::A const& thiz)
{
    out<<thiz.getstr()<<std::endl;
    return out;
}
int main()
{ 
    using namespace test;
    using namespace std;
    vector<A> token;
    copy(istream_iterator<A>(cin),istream_iterator<A>(),back_inserter(token));
    copy(token.begin(),token.end(),ostream_iterator<A>(cout," "));
    return 0;
}
这个程序的class A这个类定义在namespace test中,而<<  >>这两个操作符则定义在全局作用域中,运行结果发生编译提出了
很多莫名其妙的错误,最为主要的就是这句:
“error: no match for 'operator>>' in '*((std::istream_iterator<test::A>*)this)->std::istream_iterator<test::A>::_M_stream >> ((std::istream_iterator<test::A>*)this)->std::istream_iterator<test::A>::_M_value'”
大概可以理解为没有匹配的>>这个函数的意思吧。这就是疑问所在了,为什么我定义的流操作符在不能识别,却说没有找到匹配的函数呢。

分析上述程序的名字查找的过程,copy1(copy第一次调用)会调用>>这个操作符,编译器会把当作istream的成员函数,然后去istream的类中去寻找这个函数的名字,但由于istream只是对内定的数据类型是重载的,所有不是最佳的匹配,但是编译器还是在类的作用域中找到了这个名字,所以他不会去全局中去找。

然而编译器使用ADL规则也只是找到了名字空间std和test两个,但是我们自定义的这个流操作符却是定义在全局作用域中,所以最后会报错。
如果将自己定义的<< >>包含在test中的话,那就可以通过编译运行的。
所以将相关操作符的声明和主要类型放在同一个名字空间中非常重要的。不然编译器找不到他们的....


二、ADL详解

ADL,参数相关查找,也称作为Koenig查找(以Andrew Koenig的名字命名,有兴趣可以看Scott Meyer的文章The Most Important C++ People...Ever),是指在编译器对无限定域的函数调用进行名字查找时,所应用的一种查找规则。

f(x, y, z); // unqualified
N::f(x, y, z); // qualified

上面的函数调用,第一个f就是无限定域的函数调用,第二个则限定了在名字空间N里面,也是说使用了完全限定名。
 
我们首先来看一个函数所在的域的分类: 
1:类域(函数作为某个类的成员函数(静态或非静态))
2:名字空间域
3:全局域
 
而Koenig查找,它的规则就是当编译器对无限定域的函数调用进行名字查找时,除了当前名字空间域以外,也会把函数参数类型所处的名字空间加入查找的范围。
 
Herb提供的解释(Exceptional C++, Item 31)
Koenig Lookup(simplified): If you supply a function argument of class type (here x, of type A::X), then to look up the correct function name the compiler considers matching names in the namespace (here A) containing the argument's type.
 
请看下面的例程:

#include <iostream>
using namespace std;
 
namespace Koenig
{
    class KoenigArg
    {
    public:
         ostream& print(ostream& out) const
         {
                 out<<member_<<endl;
         }
 
         KoenigArg(int member = 5) : member_(member){}

    private:
         int member_;
    };
 

    inline ostream& operator<<(ostream& out, const KoenigArg& kArg)
    {
         return kArg.print(out);
    }
}
 
int main()
{
    Koenig::KoenigArg karg(10);
    cout<<karg;
 
    char c;cin>>c;

    return 0;
}

我们通常都会写如上的代码,使用operator<<打印对象的状态,但是ostream& operator<<(ostream& out, const KoenigArg& kArg) 的定义是处于名字空间Koenig,为什么编译器在解析main函数(全局域)里面的operator<<调用时,它能够正确定位到Koenig名字空间里面的operator<<?这是因为根据Koenig查找规则,编译器需要把参数类型KoenigArg所在的名字空间Koenig也加入对operator<<调用的名字查找范围中。
 
如果没有Koenig查找规则,我们就无法直接写cout<<karg;,而是需要写类似Koenig::operator<<(std::cout, karg); 这样的代码(使用完全限定名)。嗯,即不直观也不方便是吗?更重要的是如果我们写的是模版代码,在模版参数还没有实例化之前,我们根本就不知道参数所处的名字空间,比如:

template<typename T> void print(const T& value)
{
    std::cout<<value;
}
 
print(karg);
很显然,你的模版代码根本无法确认T是来自那个名字空间,直到编译器对模版实例化(print(karg); 被调用)。
 
对Koenig查找规则的一个异议是,由于Koenig查找规则的存在,处于某个名字空间的函数调用的重载决议会受到另外一个名字空间的自由函数所影响,仅仅是由于它使用了另外一个名字空间的类型作为参数。在这样的规则下,名字空间看起来不像我们一般所想象的那样是完全封闭和独立的。
我们应该怎么解释这样的异议呢?这样隐讳的影响或者依赖性是合理的吗?Herb认为,如果我们把另外一个名字空间的自由函数(非类成员函数)也看作是它所涉及的类型的接口的一部分,很显然,它应该参与这样的重载决议,这样的跨越名字空间的影响是合理的。从而导出了Herb在传统类定义之上的一个更详细和完整的解释(请参考Exceptional C++, Item 32)。
传统的类定义:
A class describes a set of data, along with the functions that operate on that data.
一个类描述了数据的集合以及操作这些数据的函数。

Herb的类定义,称之为接口准则(Interface Principle):
For a class X, all functions, including free functions, that both
"Mention" X
Are "supplied with" X
are logically part of X, because they form part of the interface of X.
 
对应类X来说,所有函数,包括自由函数,只要它们
         提及X(跟X有关)
         与X一起提供
都在逻辑上被认为是X的一部分,因为它们是X的接口的一部分。
 

关于Koenig查找,我们该说的都说了吗?其实未然,之前所描述的只是Koenig查找一般可能发生的状况,当Koenig查找规则和C++原来的Ordinal Lookup(OL,顺序查找规则)混合在一起的时候,它们之间的组合所产生的状况要比之前的例子复杂的多……


三、ADL和OL比较

C++名字查找有两个方法:
一个是OL(ordinary name lookup) 普通查找规则
一个是ADL(argument-depentment lookup)依赖于实参的名字查找

在说明顺序查找和Koenig查找如何共同作用的之前,先解释一下顺序查找,所谓顺序查找,就是从函数调用所处的域开始(如果函数调用处于一个成员函数中,初始域就是类域,如果处于自由函数中,初始域就是名字空间域或者全局域),依次由内到外到各个域进行名字查找,如果在某个域找到该名字的函数,就停止查找,将所有找到的重载函数进行重载决议,如果没有合适的候选者或者有多个合适的候选者而导致歧义,编译器则报错。如果一直找到全局域也没有找到任何该名字函数,编译器也报错。
 
例如:

namespace KL
{
    namespace KL_Inside
    {
         class KoenigLookup
         {
         public:
                 void koenigLookup()
                 {
                          KoenigLookupMethod();
                 }
         };
    }
}

KoenigLookupMethod的查找顺序依次是类KoenigLookup,名字空间KL::KL_Inside,名字空间KL,最后是全局域。
应该说,OL是名字查找的主要规则,只是在OL应用的某些阶段中KL也起作用,并将其作用附加在OL之上。

在继续阐述这一点之前,首先确认一个原则,类域比名字空间域(包括全局域)有更高的优先级,KL规则的作用范围是名字空间域里的自由函数,当OL应用于类域的成员函数的时候,KL是不起作用的。或者按照Herb的话说,成员函数与类之间的关系要比非成员函数更紧密(虽然都可以认为是类接口的一部分),当进行名字查找的时候,成员函数绝对不会跟非成员函数一起进行重载决议。


下面的例程说明了整个名字查找规则可能发生的各种各样的状况:
#include <iostream>
using namespace std;
 
namespace KL_ARG_2
{
    class KoenigLookupArg2;
}
 
namespace KL_ARG_1
{
    class KoenigLookupArg
    {
    };
 

    //Overload method in namespace KL_ARG_1, same with KoenigLookupArg
    void KoenigLookupMethod(KL_ARG_1::KoenigLookupArg&,
         KL_ARG_2::KoenigLookupArg2&)
    {

         cout<<"Namespace KL_ARG_1::KoenigLookupMethod ";
    }
}
 
namespace KL_ARG_2
{
    class KoenigLookupArg2
    {
    };
 

    //Overload method in namespace KL_ARG_2, same with KoenigLookupArg2
    void KoenigLookupMethod(KL_ARG_1::KoenigLookupArg&,
         KL_ARG_2::KoenigLookupArg2&)
    {

         cout<<"Namespace KL_ARG_2::KoenigLookupMethod ";
    }
}
 

//Overload method is Global
void KoenigLookupMethod(KL_ARG_1::KoenigLookupArg&,
                                            KL_ARG_2::KoenigLookupArg2&)
{

    cout<<"Global KoenigLookupMethod ";
}
 
namespace KL
{

    //Overload method in namespace KL
    void KoenigLookupMethod(KL_ARG_1::KoenigLookupArg&,
         KL_ARG_2::KoenigLookupArg2&)
    {

         cout<<"Namespace KL::KoenigLookupMethod ";
    }
 
    namespace KL_Inside
    {
         //Overload method in namespace KL::KL_Inside
         void KoenigLookupMethod(KL_ARG_1::KoenigLookupArg&,
                 KL_ARG_2::KoenigLookupArg2&)
         {

                 cout<<"Namespace KL::KL_Inside::KoenigLookupMethod ";
         }
 
         //Call overload method in the scope of namespace KL::KL_Inside
         void KL_KoenigLookup()
         {
                 KL_ARG_1::KoenigLookupArg klArg;
                 KL_ARG_2::KoenigLookupArg2 klArg2;
                 KoenigLookupMethod(klArg, klArg2);
         }
 
         class KoenigLookup
         {
         public:
                 //Call overload method in the scope of class KoenigLookup
                 //Non-Static member function
                 void koenigLookup()
                 {
                          KL_ARG_1::KoenigLookupArg klArg;
                          KL_ARG_2::KoenigLookupArg2 klArg2;
                          KoenigLookupMethod(klArg, klArg2);
                 }
 
                 //Call lookup method in the scope of class KoenigLookup
                 //Static Member function
                 static void staticKoenigLookup()
                 {
                          KL_ARG_1::KoenigLookupArg klArg;
                          KL_ARG_2::KoenigLookupArg2 klArg2;
                          KoenigLookupMethod(klArg, klArg2);
                 }
 
         private:

                 //Overload method in class KoenigLookup(Non-Static member function)
                 void KoenigLookupMethod(KL_ARG_1::KoenigLookupArg&,
                          KL_ARG_2::KoenigLookupArg2&)
                 {

                          cout<<"Non-Static Member KL::KL_Inside::KoenigLookup::"
                                   "KoenigLookupMethod ";
                 }
 

                 //Overload method in class KoenigLookup(Static member function)
                 static  void KoenigLookupMethod(KL_ARG_1::KoenigLookupArg&,
                          KL_ARG_2::KoenigLookupArg2&)
                 {

                          cout<<"Static Member KL::KL_Inside::KoenigLookup::"
                                   "KoenigLookupMethod ";
                 }
         };
    }
}

int main()
{

    //1, Call overload method in the scope of class KoenigLookup(namespace KL)
    //   Non-Static member function
    KL::KL_Inside::KoenigLookup kl;
    kl.koenigLookup();
 

    //2, Call overload method in the scope of class KoenigLookup(namespace KL)
    //   Static member function
    KL::KL_Inside::KoenigLookup::staticKoenigLookup();
 

    //3, Call overload method in the scope of namespace KL
    KL::KL_Inside::KL_KoenigLookup();
 

    //4, Call overload method in the scope of global
    KL_ARG_1::KoenigLookupArg klArg;
    KL_ARG_2::KoenigLookupArg2 klArg2;
    KoenigLookupMethod(klArg, klArg2);
 
 
    char c;cin>>c;
    return 0;
}

当然,上面的程序是不会被编译通过的,它是各种可能的组合镜像,我们用它删节之后的子版本来说明各种状况。


A:首先,我们来看重载函数KoenigLookupMethod的调用发生在类KoenigLookup的成员函数koenigLookup(非静态)的情况。

#include <iostream>
using namespace std;
 
namespace KL_ARG_2
{
    class KoenigLookupArg2;
}
 
namespace KL_ARG_1
{
    class KoenigLookupArg
    {
    };
 

    //Overload method in namespace KL_ARG_1, same with KoenigLookupArg
    void KoenigLookupMethod(KL_ARG_1::KoenigLookupArg&,
         KL_ARG_2::KoenigLookupArg2&)
    {

         cout<<"Namespace KL_ARG_1::KoenigLookupMethod ";
    }
}
 
namespace KL_ARG_2
{
    class KoenigLookupArg2
    {
    };
 

    //Overload method in namespace KL_ARG_2, same with KoenigLookupArg2
    void KoenigLookupMethod(KL_ARG_1::KoenigLookupArg&,
         KL_ARG_2::KoenigLookupArg2&)
    {

         cout<<"Namespace KL_ARG_2::KoenigLookupMethod ";
    }
}
 

//Overload method is Global
void KoenigLookupMethod(KL_ARG_1::KoenigLookupArg&,
                                            KL_ARG_2::KoenigLookupArg2&)
{

    cout<<"Global KoenigLookupMethod ";
}
 
namespace KL
{

    //Overload method in namespace KL
    void KoenigLookupMethod(KL_ARG_1::KoenigLookupArg&,
         KL_ARG_2::KoenigLookupArg2&)
    {

         cout<<"Namespace KL::KoenigLookupMethod ";
    }
 
    namespace KL_Inside
    {
         //Overload method in namespace KL::KL_Inside
         void KoenigLookupMethod(KL_ARG_1::KoenigLookupArg&,
                 KL_ARG_2::KoenigLookupArg2&)
         {

                 cout<<"Namespace KL::KL_Inside::KoenigLookupMethod ";
         }
 
         class KoenigLookup
         {
         public:
                 //Call overload method in the scope of class KoenigLookup
                 //Non-Static member function
                 void koenigLookup()
                 {
                          KL_ARG_1::KoenigLookupArg klArg;
                          KL_ARG_2::KoenigLookupArg2 klArg2;
                          KoenigLookupMethod(klArg, klArg2);
                 }
 
         private:

                 //Overload method in class KoenigLookup(Non-Static member function)
                 void KoenigLookupMethod(KL_ARG_1::KoenigLookupArg&,
                          KL_ARG_2::KoenigLookupArg2&)
                 {

                          cout<<"Non-Static Member KL::KL_Inside::KoenigLookup::"
                                   "KoenigLookupMethod ";
                 }
         };
    }
}
int main()
{

    //1, Call overload method in the scope of class KoenigLookup(namespace KL)
    //   Non-Static member function
    KL::KL_Inside::KoenigLookup kl;
    kl.koenigLookup();
 
    char c;cin>>c;
    return 0;
}
上述程序是可以编译通过并运行的,输出结果是“Non-Static Member KL::KL_Inside::KoenigLookup::KoenigLookupMethod”,被调用的是类KoenigLookup的成员函数KoenigLookupMethod(非静态)。整个过程中KL规则并没有起作用,因为OL开始作用于类域,找到符合名字的成员函数之后就停止了查找,经过重载决议后得到最后调用的版本,类域中KL是不起作用的,我们把上面程序的调用函数和重载函数换作类的静态函数,结果也一样:

class KoenigLookup
         {
         public:
                 //Call lookup method in the scope of class KoenigLookup
                 //Static Member function
                 static void staticKoenigLookup()
                 {
                          KL_ARG_1::KoenigLookupArg klArg;
                          KL_ARG_2::KoenigLookupArg2 klArg2;
                          KoenigLookupMethod(klArg, klArg2);
                 }
 


                 //Overload method in class KoenigLookup(Static member function)
                 static  void KoenigLookupMethod(KL_ARG_1::KoenigLookupArg&,
                          KL_ARG_2::KoenigLookupArg2&)
                 {


                          cout<<"Static Member KL::KL_Inside::KoenigLookup::"
                                   "KoenigLookupMethod ";
                 }
         };
 
int main()
{


    //2, Call overload method in the scope of class KoenigLookup(namespace KL)
    //   Static member function
    KL::KL_Inside::KoenigLookup::staticKoenigLookup();
 
    char c;cin>>c;
    return 0;
}

输出变成“Static Member KL::KL_Inside::KoenigLookup:: KoenigLookupMethod”。
如果成员函数KoenigLookupMethod不合适怎么办,比如它的签名被修改如下:
static void KoenigLookupMethod(KL_ARG_1::KoenigLookupArg&, KL_ARG_2::KoenigLookupArg2&, int)
编译器会直接报错说签名不吻合,并不会到后续的域继续进行查找,这也是所谓的name hiding名字隐藏。


B:继续考查KoenigLookupMethod调用发生在类KoenigLookup的成员函数中,但是在类KoenigLookup中没有找到任何候选者的情况,此时,根据OL,查找的域步进到了内层名字空间KL::KL_Inside中,如果在这个域找到了候选者,那么编译器此时就会附加KL规则,试图从KoenigLookupMethod的参数相关的域KL_ARG_1和KL_ARG_2中查找更多候选者参加重载决议,例如:

#include <iostream>
using namespace std;
 
namespace KL_ARG_2
{
    class KoenigLookupArg2;
}
 
namespace KL_ARG_1
{
    class KoenigLookupArg
    {
    };
 

    //Overload method in namespace KL_ARG_1, same with KoenigLookupArg
    void KoenigLookupMethod(KL_ARG_1::KoenigLookupArg&,
         KL_ARG_2::KoenigLookupArg2&)
    {

         cout<<"Namespace KL_ARG_1::KoenigLookupMethod ";
    }
}
 
namespace KL_ARG_2
{
    class KoenigLookupArg2
    {
    };
 

    //Overload method in namespace KL_ARG_2, same with KoenigLookupArg2
    void KoenigLookupMethod(KL_ARG_1::KoenigLookupArg&,
         KL_ARG_2::KoenigLookupArg2&)
    {

         cout<<"Namespace KL_ARG_2::KoenigLookupMethod ";
    }
}
//Overload method is Global
void KoenigLookupMethod(KL_ARG_1::KoenigLookupArg&,
                                            KL_ARG_2::KoenigLookupArg2&)
{

    cout<<"Global KoenigLookupMethod ";
}
 
namespace KL
{

    //Overload method in namespace KL
    void KoenigLookupMethod(KL_ARG_1::KoenigLookupArg&,
         KL_ARG_2::KoenigLookupArg2&)
    {

         cout<<"Namespace KL::KoenigLookupMethod ";
    }
 
    namespace KL_Inside
    {
         //Overload method in namespace KL::KL_Inside
         void KoenigLookupMethod(KL_ARG_1::KoenigLookupArg&,
                 KL_ARG_2::KoenigLookupArg2&)
         {

                 cout<<"Namespace KL::KL_Inside::KoenigLookupMethod ";
         }
 
         class KoenigLookup
         {
         public:
                 //Call lookup method in the scope of class KoenigLookup
                 //Static Member function
                 static void staticKoenigLookup()
                 {
                          KL_ARG_1::KoenigLookupArg klArg;
                          KL_ARG_2::KoenigLookupArg2 klArg2;
                          KoenigLookupMethod(klArg, klArg2);
                 }
         };
    }
}
 
int main()
{

    //2, Call overload method in the scope of class KoenigLookup(namespace KL)
    //   Static member function
    KL::KL_Inside::KoenigLookup::staticKoenigLookup();
 
    char c;cin>>c;
    return 0;
}
上面的程序会使编译器报错说对重载函数的调用不明确(VC 8.0),有三个可能性:


正在编译...

main.cpp
d:/devtest/learning/koeniglookup2/main.cpp(71) : error C2668: “KL::KL_Inside::KoenigLookupMethod”: 对重载函数的调用不明确
        d:/devtest/learning/koeniglookup2/main.cpp(56): 可能是“void KL::KL_Inside::KoenigLookupMethod(KL_ARG_1::KoenigLookupArg &,KL_ARG_2::KoenigLookupArg2 &)”
        d:/devtest/learning/koeniglookup2/main.cpp(30): 或“void KL_ARG_2::KoenigLookupMethod(KL_ARG_1::KoenigLookupArg &,KL_ARG_2::KoenigLookupArg2 &)”[使用参数相关的查找找到]
        d:/devtest/learning/koeniglookup2/main.cpp(16): 或“void KL_ARG_1::KoenigLookupMethod(KL_ARG_1::KoenigLookupArg &,KL_ARG_2::KoenigLookupArg2 &)”[使用参数相关的查找找到]
        试图匹配参数列表“(KL_ARG_1::KoenigLookupArg, KL_ARG_2::KoenigLookupArg2)”时
 
        值得注意的是,全局域和名字空间KL的重载函数KoenigLookupMethod并没有被编译器抱怨说是导致歧义的版本,因为这两个域此时根本不在查找的范围内。


C:假设在名字空间KL_Inside里面找不到,那么查找的域继续步进到外层名字空间域KL,如果在该名字空间找到候选者,编译器一样附加KL规则从参数相关域KL_ARG_1和KL_ARG_2中查找更多候选者参与重载决议。


D:如果KL还是找不到,那么查找的域最后来到了全局域,因为OL不会再继续步进,所以编译器直接使用KL规则,在全局域和参数相关域KL_ARG_1和KL_ARG_2中进行联合查找,将找到的候选者参与重载决议。

整个名字查找过程至此就结束了,如果还没有找到的话,编译器就会告诉你没有KoenigLookupMethod这个标识符。
最后看一下调用发生在非成员函数里面的情况:
       如果KoenigLookupMethod调用发生在名字空间KL_Inside的自由函数中,情况与前述的B类似;
       如果KoenigLookupMethod调用发生在名字空间KL的自由函数中,情况与前述的C类似;
       如果KoenigLookupMethod调用发生在全局域的自由函数中,情况则与前述的D类似。
至此,函数的名字查找规则(包括OL和KL)应该都解析的比较清楚了,当然,我们在编程中一般遇到的状况不会像上面几个例子那样那么复杂,实际上也不应该去搞得这么复杂,过于复杂的事物即使不会使我们犯错,也会使未来的我们代码的维护者犯错。但是,有时我们在维护他人代码或者使用一些模版库,碰到KL规则带来的副作用的时候,我们应该懂得如何去识别和解决它。
 
PS:

示例代码在VC 8.0 和 Gnu C++ 3.4.2中编译验证通过。


参考资料:http://blog.csdn.net/ccjjnn19890720/article/details/6538775
http://blog.csdn.net/rogeryi/article/details/1448606   \    http://blog.csdn.net/rogeryi/article/details/1449220

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值