CPP 学习 - 知识点 - 标准库中 string 的底层实现方式以及手动模拟 COW

标准库中 string 的底层实现方式

三种基本实现方式

Eager Copy - 深拷贝

为 Ubuntu 1804 中 string 的实现方法

tip:指针大小为 32

测试代码

// Ubuntu 1804 中
string s1 = "1234";
cout << sizeof(s1) << endl;
// 输出结果为:32 - 指针的大小

实现图例

在这里插入图片描述



COW(Copy On Write) - 写时复制

具体实现方式:浅拷贝 + 引用计数 - 为 Ubuntu 1404 中 string 的实现方法

tip:指针大小为 8

测试代码

// Ubuntu 1404 中
string s1 = "1234";
cout << sizeof(s1) << endl;
// 输出结果为:8 - 指针的大小

实现图例

在这里插入图片描述

实现代码

// 自定义类型 String 实现浅拷贝
// 具体思路:创建数组时,在头部额外增加 4 个字节,用来存放引用计数
// 			具体内容从第五个字节开始存储


SSO(Short String Optimization) - 短字符串优化

tips

  1. 当字符串的长度小于 15 字节的时候,存在栈上

  2. 当字符串的长度大于 15 字节的时候,存在堆上。

  3. 指针大小为 32

测试代码

#include <iostream>
#include <string>

using std::string;
using std::cout;
using std::endl;

int main()
{
    string s1 = "123";
    string s2 = "qwertyuiopasdfghjkzxcvbnm";
    
    printf("&s1:%p\n", s1.c_str());
    printf("&s2:%p\n", s2.c_str());
    
    cout << sizeof(s1) << endl;
    
    return 0;
}

// 运行结果:
// 0x7ffcbb249380 - 栈
// 0x559942d02e70 - 堆
// 32 - 指针的大小

实现图例

在这里插入图片描述

COW 的实现 - 引用计数

参考的为《More Effective C++》

简易版本

重载使用下标访问符时,忽略判断读写区别

使用代理类可进行判断

实现代码

String_Refcount.h

#ifndef __STRING_REFCOUNT_H
#define __STRING_REFCOUNT_H

// 引用计数
// 1. 引用计数允许多个有相同值的对象共享这个值的实现。
//      节省内存,而且使得程序运行更快,因为不需要构造和析构这个值的拷贝。
// 2. 使用引用计数后,对象自己拥有自己,当没人再使用它是,会自动销毁自己。
//      因此,引用计数是个简单的垃圾回收体系。

#include <cstring>

// 实现引用计数
class String
{
public:
    String(const char *initValue = "");     // 构造函数
    String(const String& rhs);              // 拷贝构造函数
    String& operator=(const String& rhs);   // 赋值运算符
    ~String();                              // 析构函数

    // 下标访问符 - 有待改进:区分读写
    const char& operator[](size_t idx) const;
    char& operator[](size_t idx);

private:
    // 保存引用计数及其跟踪的值
    struct StringValue
    {
        StringValue(const char *initValue);
        ~StringValue();

        int refCount;
        char *data;
        // 当有复数个对象指向同一内存块时,将指针直接指向内存块时,将会对所有指向该内存块的对象值进行修改
        // 解决方案:
        //      增加标志 shareable 指出是否可共享。
        //          1. 创建时/可共享时,将标志打开
        //          2. 在非 const 的 operator[] 被调用时将它关闭,之后将永远保持这个状态
        bool shareable;
    };

    StringValue *value;
};

#endif  // __STRING_REFCOUNT_H

String_Refcount.cc

#include <cstring>

#include "String_Refcount.h"

String::StringValue::StringValue(const char *initValue)
    : refCount(1)
    , shareable(true)
{
    data = new char[strlen(initValue) + 1];
    strcpy(data, initValue);
}

String::StringValue::~StringValue()
{
    delete[] data;
    data = nullptr;
}

// 用构造函数创建对象时,都是各自独立的
// String s1("More Effective C++")
// [s1] ----> [1] ----> [More Effective C++]
// String s2("More Effective C++")
// [s2] ----> [1] ----> [More Effective C++]
// 可以改进,即创建时,跟踪已存在的 StringValue 对象,只有不同串时才创建新的对象
// 有缘在写!
String::String(const char *initValue)
    : value(new StringValue(initValue))
{}

// 实现拷贝构造函数
// String s1("More Effective C++")
// String s2 = s1;
// [s1] --
//        --> [2] ----> [More Effective C++]
// [s2] --
String::String(const String& rhs)
{
    if(rhs.value->shareable)
    {
        value = rhs.value;
        ++value->refCount;
    }
    else
        value = new StringValue(rhs.value->data);
}

// 重载赋值运算符
// String s1("More Effective C++")
// String s2("Nothing");
// s1 = s2;
String& String::operator=(const String& rhs)
{
    if(value == rhs.value)
        return *this;

    if(0 == --value->refCount)
    {
        delete value;
        value = nullptr;
    }

    if(rhs.value->shareable)
    {
        value = rhs.value;
        ++value->refCount;
    }
    else
        value = new StringValue(rhs.value->data);

    return *this;
}

// 实现析构函数
String::~String()
{
    if(0 == --value->refCount)
    {
        delete value;
        value = nullptr;
    }
}

// 由于是 const 版本,不允许更改其值,所以直接返回即可
const char& String::operator[](size_t idx) const
{
    return value->data[idx];
}

// 由于无法直接区分读写,所以非 const 的下标访问符重载时,只要调用就默认认为要更改其值
// 可再由一个内部类来实现区分读写
char& String::operator[](size_t idx)
{
    // 如果该对象有复数个引用,则新建一个对象
    if(1 != value->refCount)
    {
        --value->refCount;
        value = new StringValue(value->data);
    }
    value->shareable = false;

    return value->data[idx];
}


简易版本的改进

设置基类 RCObject,任何需要引用计数的类都必须从它继承,String 再继承 RCObject。

再创建灵巧指针模板 RCPtr,实现指针自己检测拷贝指针、给指针赋值和销毁指针等工作

实现代码

RCPtr.h

#ifndef __RCPTR_H
#define __RCPTR_H

// 实现自动的引用计数处理
template<class T>
class RCPtr
{
public:
    RCPtr(T *realPtr = 0);
    RCPtr(const RCPtr& rhs);
    RCPtr& operator=(const RCPtr& rhs);
    ~RCPtr();

    T *operator->() const;
    T& operator*() const;

private:
    T *pointee;

    void init();
};

#endif  // __RCPTR_H

RCPtr.cc

#include "RCPtr.h"

template<class T>
RCPtr<T>::RCPtr(T *realPtr)
    : pointee(realPtr)
{
    init();
}

template<class T>
RCPtr<T>::RCPtr(const RCPtr& rhs)
    : pointee(rhs.pointee)
{
    init();
}

template<class T>
void RCPtr<T>::init()
{
    if(0 == pointee)
        return;
    if(false == pointee->isShareable())
        pointee = new T(*pointee);

    pointee->addReference();
}

template<class T>
RCPtr<T>& RCPtr<T>::operator=(const RCPtr& rhs)
{ 
    if(rhs.pointee != pointee)
    {
        if(pointee)
            pointee->removeReference();

        pointee = rhs.pointee;
        init();
    }

    return *this;
}

template<class T>
RCPtr<T>::~RCPtr()
{
    if(pointee)
        pointee->removeReference();
}

template<class T>
T *RCPtr<T>::operator->() const
{
    return pointee;
}

template<class T>
T& RCPtr<T>::operator*() const
{
    return *pointee;
}

RCObject.h

#ifndef __RCOBJECT_H
#define __RCOBJECT_H

#include <iostream>

#include "RCPtr.h"

class RCObject
{
protected:
    RCObject();
    RCObject(const RCObject& rhs);
    RCObject& operator=(const RCObject& rhs);
    // 虚函数 - 详情见 theVirtual.cc
    virtual ~RCObject() = 0;

public:
    void addReference();
    void removeReference();
    void markUnshareable();

    bool isShareable() const;
    bool isShared() const;

private:
    int refCount;
    bool shareable;
};

#endif  // __RCOBJECT_H

RCObject.cc

#include "RCObject.h"

RCObject::RCObject()
    : refCount(0)   // 初始化列表中设置为 0,在构造后,将构造它的对象简单地将 refCount 设置为 1 即可
    , shareable(true)
{}

RCObject::RCObject(const RCObject&)
    : refCount(0)   // 同上,构造者负责将 refCount 设为正确的值
    , shareable(true)
{}

// 不期望调用该函数,所以没有做任何事情
// RCObject 是基于引用计数来共享的值对象的积累,不该从一个赋给另外一个,
//      而应该是拥有这个值的对象被从一个赋给另外一个
// 有一说一,这理由没懂!
RCObject& RCObject::operator=(const RCObject&)
{
    return *this;
}

RCObject::~RCObject() {}

void RCObject::addReference()
{
    ++refCount;
}

void RCObject::removeReference()
{
    if(0 == --refCount)
        delete this;
}

void RCObject::markUnshareable()
{
    shareable = false;
}

bool RCObject::isShareable() const
{
    return shareable;
}

bool RCObject::isShared() const
{
    return refCount > 1;
}

String.h

#ifndef __STRING_REFCOUNT_H
#define __STRING_REFCOUNT_H

// 引用计数
// 1. 引用计数允许多个有相同值的对象共享这个值的实现。
//      节省内存,而且使得程序运行更快,因为不需要构造和析构这个值的拷贝。
// 2. 使用引用计数后,对象自己拥有自己,当没人再使用它是,会自动销毁自己。
//      因此,引用计数是个简单的垃圾回收体系。

#include <cstring>

#include "RCObject.h"

// 实现引用计数
class String
{
public:
    String(const char *initValue = "");     // 构造函数
    /* String(const String& rhs);              // 拷贝构造函数 */
    /* String& operator=(const String& rhs);   // 赋值运算符 */
    /* ~String();                              // 析构函数 */

    // 下标访问符 - 有待改进:区分读写
    const char& operator[](size_t idx) const;
    char& operator[](size_t idx);

private:
    // 保存引用计数及其跟踪的值
    struct StringValue
    : public RCObject
    {
        StringValue(const char *initValue);
        // 所有含有指针的类都应当提供拷贝构造函数 和 赋值运算
        // 可以设置虚拷贝构造函数,实现当 pointee 指向 T 的派生类时,即
        //      struct SpecialStringValue: public StringValue { ... }
        //          RCPtr<String Value> tmp = new SpecialStringValue;
        //      希望 pointee = new T(*pointee); // 生成一个 SpecialStringValue 的指针
        // 但对于该 String 类,不期望生成 StringValue 的派生子类,所以忽略该问题
        StringValue(const StringValue& rhs);
        ~StringValue();

        char *data;

        void init(const char *initValue);
    };

    /* StringValue *value; */
    RCPtr<StringValue> value;
};

#endif  // __STRING_REFCOUNT_H

String.cc

#include <cstring>

#include "String_Refcount.h"

void String::StringValue::init(const char *initValue)
{
    data = new char[strlen(initValue) + 1];
    strcpy(data, initValue);
}

String::StringValue::StringValue(const char *initValue)
    /* : refCount(1) */
    /* , shareable(true) */
{
    /* data = new char[strlen(initValue) + 1]; */
    /* strcpy(data, initValue); */
    init(initValue);
}

String::StringValue::~StringValue()
{
    delete[] data;
    data = nullptr;
}

String::StringValue::StringValue(const StringValue& rhs)
{
    /* data = new char[strlen(rhs.data) + 1]; */
    /* strcpy(data, rhs.data); */
    init(rhs.data);
}

// 用构造函数创建对象时,都是各自独立的
// String s1("More Effective C++")
// [s1] ----> [1] ----> [More Effective C++]
// String s2("More Effective C++")
// [s2] ----> [1] ----> [More Effective C++]
// 可以改进,即创建时,跟踪已存在的 StringValue 对象,只有不同串时才创建新的对象
// 有缘在写!
String::String(const char *initValue)
    : value(new StringValue(initValue))
{}

/* // 实现拷贝构造函数 */
/* // String s1("More Effective C++") */
/* // String s2 = s1; */
/* // [s1] -- */
/* //        --> [2] ----> [More Effective C++] */
/* // [s2] -- */
/* String::String(const String& rhs) */
/* { */
/*     if(rhs.value->shareable) */
/*     { */
/*         value = rhs.value; */
/*         ++value->refCount; */
/*     } */
/*     else */
/*         value = new StringValue(rhs.value->data); */
/* } */

/* // 重载赋值运算符 */
/* // String s1("More Effective C++") */
/* // String s2("Nothing"); */
/* // s1 = s2; */
/* String& String::operator=(const String& rhs) */
/* { */
/*     if(value == rhs.value) */
/*         return *this; */

/*     if(0 == --value->refCount) */
/*     { */
/*         delete value; */
/*         value = nullptr; */
/*     } */

/*     if(rhs.value->shareable) */
/*     { */
/*         value = rhs.value; */
/*         ++value->refCount; */
/*     } */
/*     else */
/*         value = new StringValue(rhs.value->data); */

/*     return *this; */
/* } */

/* // 实现析构函数 */
/* String::~String() */
/* { */
/*     if(0 == --value->refCount) */
/*     { */
/*         delete value; */
/*         value = nullptr; */
/*     } */
/* } */

// 由于是 const 版本,不允许更改其值,所以直接返回即可
const char& String::operator[](size_t idx) const
{
    return value->data[idx];
}

/* // 由于无法直接区分读写,所以非 const 的下标访问符重载时,只要调用就默认认为要更改其值 */
/* // 可再由一个内部类来实现区分读写 */
/* char& String::operator[](size_t idx) */
/* { */
/*     // 如果该对象有复数个引用,则新建一个对象 */
/*     if(1 != value->refCount) */
/*     { */
/*         --value->refCount; */
/*         value = new StringValue(value->data); */
/*     } */
/*     value->shareable = false; */

/*     return value->data[idx]; */
/* } */
char& String::operator[](size_t idx)
{
    if(value->isShared())
        value = new StringValue(value->data);

    value->markUnshareable();

    return value->data[idx];
}

自己写的版本,且实现了取下标时判断读写 - 代理类

通过代理类来帮助区分通过 operaotr[] 进行的是读操作还是写操作

核心思想

  1. 无法在 operator[]内判断进行的是读还是写操作,这是确定的。所以,返回类型更改为一个代理类 - proxy 类,将判断读还是写的行为推迟到我们知道 operator[] 的结果被怎么使用。 - lazy 原则 - 《More Effective C++》 ( Item M17 )

    其中 proxy 所做的事

    1. 创建它,也就是指定它扮演哪个字符。
    2. 将它作为赋值操作的目标,在这种情况下可以将赋值真正作用在它扮演的字符上。当这样被使用时,proxy 类扮演的是左值。
    3. 其它方式使用它。这时,proxy 类扮演的是右值。
  2. 通过proxy类所做的事,在配合隐式转换,从而达到自动右值的扮演。

    具体流程

    1. 对于 String 的 取下标操作,返回了 CharProxy 对象,而没有为这样的对象定义输出流操作,编译器自动地寻找一个隐式的类型转换以使得 operat<<调用成功。
    2. CharProxy类内部申明了一个隐式转换到cahr的操作。
    3. 当取下标后作为右值时,都故意以这种隐式转换的方式去执行调用。
  3. CharProxy 类中重写 operator=(char);,使得从 String 返回 CharProxy 类型的返回值后直接作为左值进行赋值。

  4. const 版本的本就不可更改,故不存在写操作。

    主要实现代码

    // String 类中的修改
    class String
    {
    	// ...
    public:
    	class CharProxy
    	{
    	public:
    		CharProxy(String&, size_t);					// creation
    		CharProxy& operator=(const CharProxy&);		// lvalue
    		CharProxy& operator=(char);				// uses
    		// 由自定义类型向其他类型转换,详情见 - 运算符重载
    		operator char() const;						// rvalue
    
    		// 改进项 - 无缝替换
    		// 1. 由于 String::operator[] 返回类型为 CharProxy
    		// 所以当执行
    		// 		String s1 = "Hello";
    		//		char *p = &s1[1];		// error
    		// 因为没有 CharProxy * 到 char * 的转换函数
    		// 故,重载 CharProxy 类的取地址运算
    		char *operator&();
    		const char *operator&() const;
    
    		// 2. 还有一些较为复杂的左值操作函数需要一一重载
    		// 如:operator++, operator+= 等等
    
    		// 3. 有当形参类型为 char& 时也会出现异常
    		// void swap(char&, char&);
    		// String s = "C++";
    		// swap(s[0], s[1]);
    		// 因为 String::operator[] 返回的是一个 CharProxy 对象
    		// 		CharProxy 对象可以隐式转换为一个 char,但是并没有转换为 char& 的转换函数
    		// 		而转换而成的 char 是一个临时变量,不可也不引导作为非 const 的引用的的形参
    
    		// 4. 一些未定义的隐式转换,如 int 等
    		// 解决方案:申明需要隐式转换而生成的对象的类的构造函数为 explicit
    		// 		以使得第一次调用构造函数的行为在编译期间直接失败
    		// 		详情见 - 《More Effective C++》 ( Item M5 )
    
    	private:
    		String& theString;
    		int charIndex;
    	};
    
    	// 重写 String 类的 operator[]
    	const CharProxy operator[](std::size_t idx) const;
    	CharProxy operator[](std::size_t idx);
    
    	// ...
    
    	// 由于 CharProxy::operator=(char) 中要使用 String 的私有成员,所以设置为友元类
    	friend class CharProxy;
    
    private:
    	// 智慧指针,非必须 - 实现代码中未使用智慧指针
    	RCPtr<StringValue> value;
    };
    
    // 具体实现
    // String 类的重写
    const String::CharProxy String::operator[](size_t idx) const
    { return CharProxy(const_cast<String&>(*this), idx) }
    
    String::CharProxy String::operator[](size_t idx)
    {return CharProxy(*this, idx) }
    
    // String::CharProxy 的实现
    String::CharProxy::CharProxy(String& str, size_t idx)
    	:theString(str), charIndex(idx) {}
    
    // rvalue
    String::CharProxy::operator char() const
    { return theString.value->data[charIndex] }
    
    // lvalue
    String::CharProxy& String::CharProxy::operator=(const CharProxy& rhs)
    {
    	if(theString.value->isShared())
    		theString.value = new StringValue(theString.value->data);
    
    	theString.value->data[charIndex] = rhs.theString.value->data[rhs.charIndex];
    
    	return *this;
    }
    
    String::CharProxy& String::CharProxy::operator=(char c)
    {
    	if(theString.value->isShared())
    		theString.value = new StringValue(theString.value->data);
    
    	theString.value->data[charIndex] = c;
    	return *this;
    }
    
    const char *String::CharProxy::operator&() const
    { return &(theString.value->data[charIndex]) }
    
    char *String::CharProxy::operator&()
    {
    	if(theString.value->isShared())
    		theString.value = new StringValue(theString.value->data);
    
    	theString.value->markUnshareable();
    	return &(theString.value->data[charIndex]);
    }
    

实现代码

手动判断指针是否该删除

string_cow.cc

#include <iostream>
#include <cstring>

using std::cout;
using std::endl;

class String
{
    friend std::ostream &operator<<(std::ostream &os, const String &rhs);
    friend class CharProxy;

public:
    class CharProxy
    {
    public:
        CharProxy(String &str, int index);
        CharProxy &operator=(const CharProxy &rhs);
        CharProxy &operator=(char c);
        operator char() const;

        const char* operator&() const;
        char* operator&();

    private:
        String &theString;
        int charIndex;
    };

    const CharProxy operator[](int) const;
    CharProxy operator[](int);

    String()
        : _pstr(new char[5]() + 4)
    {
        /* cout << "String" << endl; */

        // operator top 4 Byte
        // Because Integer is 4 Byte
        initRefcount();
    }

    // String s1 = "Hello";
    String(const char *pstr)
        : _pstr(new char[strlen(pstr) + 5] + 4)
    {
        /* cout << "String(const char *)" << endl; */

        strcpy(_pstr, pstr);
        initRefcount();
    }

    // String s2 = s1;
    String(const String &rhs)
        : _pstr(rhs._pstr)  // shallow copy
    {
        /* cout <<"String(const String &)" << endl; */

        increaseRefcount();
    }

    // s3 = s1;
    String &operator=(const String &rhs)
    {
        /* cout << "String &operator=(const String &)" << endl; */

        if(this != &rhs)    // copy itself
        {
            decreaseRefcount(); // release the Refcouont

            _pstr = rhs._pstr;  // shallow copy
            increaseRefcount();
        }

        return *this;
    }

    ~String()
    {
        /* cout << "~String" << endl; */

        decreaseRefcount();
    }

    /* char &operator[](size_t idx) */
    /* { */
    /*     if(idx < size()) */
    /*     { */
    /*         // 由于取下标后,可能会对该位置进行赋值操作,所以需要进行深拷贝 */ 
    /*         if(getRefcount() != 1) */
    /*         { */
    /*             char *ptmp = new char[size() + 5]() + 4; */
    /*             strcpy(ptmp, _pstr); */
    /*             decreaseRefcount(); */

    /*             _pstr = ptmp; */
    /*             initRefcount(); */
    /*         } */

    /*         return _pstr[idx]; */
    /*     } */
    /*     else */
    /*     { */
    /*         static char nullchar = '\0'; */
    /*         return nullchar; */
    /*     } */
    /* } */

    int getRefcount()
    {
        return *(int *)(_pstr- 4);
    }

    const char *c_str() const
    {
        return _pstr;
    }

    size_t size() const
    {
        return strlen(_pstr);
    }
    
private:
    void initRefcount()
    {
        *(int *)(_pstr - 4) = 1;
    }

    void increaseRefcount()
    {
        ++*(int *)(_pstr - 4);
    }

    void decreaseRefcount()
    {
        --*(int *)(_pstr - 4);
        if(0 == getRefcount())
            delete[] (_pstr - 4);
    }

    char *_pstr;
};

const String::CharProxy String::operator[](int idx) const
{
    return CharProxy(const_cast<String&>(*this), idx);
}

String::CharProxy String::operator[](int idx)
{
    return CharProxy(*this, idx);
}

String::CharProxy::CharProxy(String& str, int idx)
    : theString(str)
    , charIndex(idx)
{}

String::CharProxy::operator char() const
{
    return theString._pstr[charIndex];
}

String::CharProxy& String::CharProxy::operator=(const CharProxy& rhs)
{
    if(theString.getRefcount() != 1)
    {
        char *pString = theString._pstr;
        char *ptmp = new char[strlen(pString) + 5]() + 4;

        strcpy(ptmp, pString);
        theString.decreaseRefcount();

        theString._pstr = ptmp;
        theString.initRefcount();
    }
}

String::CharProxy& String::CharProxy::operator=(char c)
{
    if(theString.getRefcount() != 1)
    {
        char *pString = theString._pstr;
        char *ptmp = new char[strlen(pString) + 5]() + 4;

        strcpy(ptmp, pString);
        theString.decreaseRefcount();

        theString._pstr = ptmp;
        theString.initRefcount();
    }

    theString._pstr[charIndex] = c;
    return *this;
}

const char* String::CharProxy::operator&() const
{
    return &(theString._pstr[charIndex]);
}

char* String::CharProxy::operator&()
{
    if(theString.getRefcount() != 1)
    {
        char *pString = theString._pstr;
        char *ptmp = new char[strlen(pString) + 5]() + 4;

        strcpy(ptmp, pString);
        theString.decreaseRefcount();

        theString._pstr = ptmp;
        theString.initRefcount();
    }

    return &(theString._pstr[charIndex]);
}

std::ostream &operator<<(std::ostream &os, const String &rhs)
{
    if(rhs._pstr)
        os << rhs._pstr << endl;

    return os;
}

int main()
{
    cout << "First step:" << endl;
    String s1 = "123";
    String s2 = "321";
    String s3 = s1;
    printf("s1:%p\n", s1.c_str());
    printf("s2:%p\n", s2.c_str());
    printf("s3:%p\n", s3.c_str());

    cout << endl << "Second step:" << endl;
    s2 = s1;
    s3[0];
    s1[0] = '3';
    printf("s1:%p\n", s1.c_str());
    printf("s2:%p\n", s2.c_str());
    printf("s3:%p\n", s3.c_str());

    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值