The serialization primer(序列化入门)

翻译:

The serialization primer(本文源自http://www.codeproject.com, 查看原文:http://www.codeproject.com/KB/cpp/serialization_primer1.aspx )

教程分为三部分,我就不再分直接放在一起。

l         第一部分 介绍序列化的基础。

l         第二部分 解释怎样处理读取无效数据和版本支持。

l         第三部分 介绍怎样对复杂的对象进行序列化。

 

第一部分

序列化是指将一个对象读取或者写入一个持久存储介质,比如硬盘文件。序列化一个对象需要三个成分:

l         一个代表数据文件的CFile对象

l         一个提供序列化上下文的CArchive对象

l         需要序列化的对象。

 

第一步:打开数据文件

将一个对象序列化到文件“foo.dat”,需要以合适的访问方式打开一个文件。在这个例子中,文件以单独读写的方式打开。

  // Open file "foo.dat"

  CFile* pFile = new CFile();

  ASSERT (pFile != NULL);

  if (!pFile->Open ("foo.dat", CFile::modeReadWrite | CFile::shareExclusive)) {

      // Handle error

      return;

  }

第二步:关联CArchive

接下来,将一个CArchive对象关联到文件。CArchive提供了一个有效的渠道来进行持久存储。你可以通过这个对象来序列化数据而不是直接读写文件,但是它需要知道你是用它来写入数据还是读取数据。在这个例子里,我们假定要写数据。

  // Create archive ...

  bool bReading = false;  // ... for writing

  CArchive* pArchive = NULL;

  try

  {

    pFile->SeekToBegin();

    UINT uMode = (bReading ? CArchive::load : CArchive::store);

    pArchive = new CArchive (pFile, uMode);

    ASSERT (pArchive != NULL);

  }

  catch (CException* pException)

  {

    // Handle error

    return;

  }

 

第三步:序列化对象

最终我们将调用对象的serialize()函数来序列化一个对象。serialize()函数只是我们自己写的一个函数,与MFC中的CObject::Serialize()没有关系。同时,你也不需要继承自CObject。这个serialize()函数使用CArchive的一个指针并返回一个整型类型的状态值。

  int CFoo::serialize

    (CArchive* pArchive)

  {

    int nStatus = SUCCESS;

 

    // Serialize the object ...

    ...

   

    return (nStatus);

  }

我们可以在一分钟之内得到一个实际的序列化过程。同时,让我们注意以下两个重点:

l         CFoo::serialize()函数用来将一个对象写入到持久存储介质或者从持久存储介质上读取数据

l         CFoo::serialize()函数不知道将要访问的数据文件的任何信息。

假设CFoo代表一个包含两个成员变量职工记录。

  class CFoo

  {

    // Construction/destruction

    public:

      CFoo::CFoo();

      virtual CFoo::~CFoo();

 

    // Methods

    public:

      int serialize (CArchive* pArchive);

 

    // Data members

    public:

      CString  m_strName;  // employee name

      int      m_nId;      // employee id

  };

我们使用CArchive的流操作符<<>>向存储介质上读写数据。CArchive知道怎样去序列化像int, float或者DWORD的数据类型,或者像CString的对象。同时它也知道它是在读还是在写。你可以用CArchive::IsStoring()函数来查询它的读写状态。CFoo的序列化函数可以写成下面的样子:

  int CFoo::serialize

    (CArchive* pArchive)

  {

    int nStatus = SUCCESS;

 

    // Serialize the object ...

    ASSERT (pArchive != NULL);

    try

    {

      if (pArchive->IsStoring()) {

         // Write employee name and id

         (*pArchive) << m_strName;

         (*pArchive) << m_nId;

      }

      else {

         // Read employee name and id

         (*pArchive) >> m_strName;

         (*pArchive) >> m_nId;

      }

    }

    catch (CException* pException)

    {

      nStatus = ERROR;

    }

    return (nStatus);

  }

第四步:清除资源

当结束序列化时,你需要关闭CArchive的对象和数据文件对象,并清理资源。

  pArchive->Close();

  delete pArchive;

  pFile->Close();

  delete pFile();

总结

到现在,我们有了一个非常小的序列化雏形。在第二部分,我们将看看怎样处理读取非法数据和支持对象的不同版本。在第三部分我们将序列化一个复杂的对象。

 

 

第二部分

在第一部分我们知道了怎样通过CArchive使用serilize()函数来序列化一个简单的对象:

  int CFoo::serialize

    (CArchive* pArchive)

  {

    int nStatus = SUCCESS;

 

    // Serialize the object ...

    ASSERT (pArchive != NULL);

    TRY

    {

      if (pArchive->IsStoring()) {

         // Write employee name and id

         (*pArchive) << m_strName;

         (*pArchive) << m_nId;

      }

      else {

         // Read employee name and id

         (*pArchive) >> m_strName;

         (*pArchive) >> m_nId;

      }

    }

    CATCH_ALL (pException)

    {

      nStatus = ERROR;

    }

    END_CATCH_ALL

 

    return (nStatus);

  }

这个代码存在一个问题。当我们读取的数据文件不存在我们期望的数据时怎么办?如果数据文件在int数据后没有一个CString对象,我们的serialize()函数将会返回ERROR。这样处理非常好,但是如果我们可以提前知晓这个状况并且返回一个更加详细的状态码INVALID_DATAFILE会更好。我们可以使用一个签名来检查是否正在读写一个合法的数据文件(例如,包含一个CFoo类)。

 

对象签名

一个对象签名就是一个表示一个对象的字符串(比如:“FooObject”)。我们修改类定义来给CFoo添加一个签名:

  class CFoo

  {

    ...

 

    // Methods

    public:

      ...

      CString getSignature();

 

    // Data members

      ...

    protected:

      static const CString  Signature;  // object signature

  };

这个签名在Foo.cpp文件中声明。

  // Static constants

  const CString CFoo::Signature = "FooObject";

接下来,我们修改serialize()函数,使它在存储两个成员变量前先存储签名。如果遇到一个非法的签名或者丢失了签名,那么就有可是是我们存取的数据存储介质不包含CFoo对象。这是读写逻辑就变成了下面的样子:

代码如下:

  int CFoo::serialize

    (CArchive* pArchive)

  {

    int nStatus = SUCCESS;

    bool bSignatureRead = false;

 

    // Serialize the object ...

    ASSERT (pArchive != NULL);

    TRY

    {

      if (pArchive->IsStoring()) {

         // Write signature

         (*pArchive) << getSignature();

 

         // Write employee name and id

         (*pArchive) << m_strName;

         (*pArchive) << m_nId;

      }

      else {

         // Read signature - complain if invalid

         CString strSignature;

         (*pArchive) >> strSignature;

         bSignatureRead = true;

         if (strSignature.Compare (getSignature()) != 0) {

            return (INVALID_DATAFILE);

         }

 

         // Read employee name and id

         (*pArchive) >> m_strName;

         (*pArchive) >> m_nId;

      }

    }

    CATCH_ALL (pException)

    {

      nStatus = bSignatureRead ? ERROR : INVALID_DATAFILE;

    }

    END_CATCH_ALL

 

    return (nStatus);

  }

你必须保证你的对象签名是唯一的。它的重要性仅次于签名本身。如果你开发了一个产品套间,这对于你注册公司产品签名非常有用。这样的话,开发人员不会在不同的对象中使用相同的签名。如果你想扭转组织数据文件的工程,你需要使用不关联对象名称的签名。

 

版本信息

当你在你的产品生命周期中升级时,你可以发现方便的添加删除CFoo结构的成员函数非常有必要。如果你发布一个新版本的CFoo,尝试独缺老版本存储的数据文件会失败。这样是不可以接受的。任何版本的CFoo都应该能够从老版本的序列化中恢复。换句话说,CFoo序列化的函数要能够向后兼容。在对象中使用版本信息很容易实现。就像我们添加一个签名一样,我们添加一个整型数来表示版本号。

  class CFoo

  {

    ...

 

    // Methods

    public:

      ...

      CString getSignature();

      int     getVersion();

 

    // Data members

      ...

    protected:

      static const CString  Signature;  // object signature

      static const int      Version;    // object version

  };

CFoo的版本1中有两个成员函数:一个CString类型的m_strName和一个int类型的m_nId。如果在版本2添加第三个成员(比如:int m_nDept),那么我们在读取老版本的对象时就需要决定什么时候初始化m_nDept。如果老版本的对象我们就将这个变量初始化为-1

  class CFoo

  {

    ...

    // Data members

    public:

      CString  m_strName;  // employee name

      int      m_nId;      // employee id

      int      m_nDept;    // department code (-1 = unknown)

  };

同样,我们需要在新版本中加入:

  const int CFoo::Version = 2;

最后,我们修改serialize()函数,当我们读到旧版本的数据文件时就将m_nDept赋值为-1。需要注意的是文件存储总是保存为最新的版本。

  int CFoo::serialize

    (CArchive* pArchive)

  {

    ...

    // Serialize the object ...

    ASSERT (pArchive != NULL);

    TRY

    {

      if (pArchive->IsStoring()) {

         ...

         // Write employee name, id and department code

         (*pArchive) << m_strName;

         (*pArchive) << m_nId;

         (*pArchive) << m_nDept;

      }

      else {

         ...

         // Read employee name and id

         (*pArchive) >> m_strName;

         (*pArchive) >> m_nId;

 

         // Read department code (new in version 2)

         if (nVersion >= 2) {

            (*pArchive) >> m_nDept;

         }

         else {

            m_nDept = -1; // unknown

         }

      }

    }

    CATCH_ALL (pException)

    {

      nStatus = bSignatureRead && bVersionRead ? ERROR : INVALID_DATAFILE;

    }

    END_CATCH_ALL

 

    return (nStatus);

  }

总结

到此为止,第二部分完成,待第三部分

 

第三部分

在前两个部分,我们学习了怎样提供简单的序列化,在这里,我们要学习序列化任何类型对象的规则。下面要考虑四种情况,每一种都建立在前一种之上。

l         序列化一个简单的类

l         序列化一个继承的类

l         序列化一个同质集合类

l         序列化一个异质集合类

我们的serialize()函数将返回以下几个状态码之一:

l         Success

l         InvalidFormat

l         UnsupportedVersion

l         ReadError

l         WriteError

序列化一个简单的类

一个简单的类是指一个对象没有父类并且不是一个集合类。序列化这样一个类,需要这样做:

1.         序列化类的签名和版本信息

2.         序列化类的成员变量

在下面的例子里,Point类包含两个int类型的表示一个坐标点成员变量。这个对象的签名和版本信息定义static成员变量(m_strSignaturem_nVersion),并用它们来表示Point的实例

  int Point::serialize

    (CArchive* pArchive)

  {

    ASSERT (pArchive != NULL);

 

    // Step 1: Serialize signature and version

    int nVersion;

    try {

      if (pArchive->IsStoring()) {

          (*pArchive) << Point::m_strSignature;

          (*pArchive) << Point::m_nVersion;

      } else {

          CString strSignature;

          (*pArchive) >> strSignature;

          if (strSignature != Point::m_strSignature)

             return (Status::InvalidFormat);

          (*pArchive) >> nVersion;

          if (nVersion > Point::m_nVersion;)

             return (Status::UnsupportedVersion);

      }

 

      // Step 2: Serialize members

      if (pArchive->IsStoring()) {

          (*pArchive) << m_nX;

          (*pArchive) << m_nY;

      } else {

          (*pArchive) >> m_nX;

          (*pArchive) >> m_nY;

      }

    }

    catch (CException* pException) {

      // A read/write error occured

      pException->Delete();

      if (pArchive->IsStoring())

        return (Status::WriteError);

      return (Status::ReadError);

    }

 

    // Object was successfully serialized

    return (Status::Success);

  }

 

序列化一个继承的类

在本文中,一个继承的类是指一个继承自简单类的类并且不是集合类。序列化这样的类,需要这样做:

1.         序列化类的签名和版本信息

2.         序列化对象基类<<额外步骤

3.         序列化类的成员变量

下面的例子中,ColoredPoint类继承自Point并且包含一个额外的int成员函数m_nColor来制定坐标点的颜色。和所有的序列化类一样,它也包含版本和签名信息。

  int ColoredPoint::serialize

    (CArchive* pArchive)

  {

    ASSERT (pArchive != NULL);

 

    // Step 1: Serialize signature and version

    int nVersion;

    try {

      if (pArchive->IsStoring()) {

          (*pArchive) << ColoredPoint::m_strSignature;

          (*pArchive) << ColoredPoint::m_nVersion;

      } else {

          CString strSignature;

          (*pArchive) >> strSignature;

          if (strSignature != ColoredPoint::m_strSignature)

             return (Status::InvalidFormat);

          (*pArchive) >> nVersion;

          if (nVersion > ColoredPoint::m_nVersion;)

             return (Status::UnsupportedVersion);

      }

 

      // Step 2: Serialize the base class

      int nStatus = Point::serialize (pArchive);

      if (nStatus != Status::Success)

         return (nStatus);

 

      // Step 3: Serialize members

      if (pArchive->IsStoring())

         (*pArchive) << m_nColor;

      else

         (*pArchive) >> m_nColor;

    }

    catch (CException* pException) {

      // A read/write error occured

      pException->Delete();

      if (pArchive->IsStoring())

        return (Status::WriteError);

      return (Status::ReadError);

    }

 

    // Object was successfully serialized

    return (Status::Success);

  }

序列化一个同质集合类

同质集合类用来存储同一个类型的动态大小的对象集合。序列化这样的类,需要这样做:

1.         序列化类的签名和版本信息

2.         序列化基类

3.         序列化集合内条目的数据<<额外步骤

4.         序列化集合内的每个对象<<额外步骤

5.         序列化类的成员变量

在这个例子中,ColoredPointListColoredPoint对象的集合。为了保持简单,ColoredPointList使用CPtrArray来存储对象。ColoredPointList同样含有版本信息和签名,下面是ColoredPointList的定义:

  class ColoredPointList

  {

    // Construction/destruction

    public:

      ColoredPointList::ColoredPointList();

      virtual ColoredPointList::~ColoredPointList();

 

    // Attributes

    public:

      static const CString m_strSignature;

      static const int m_nVersion;

 

    // Operations

    public:

      int serialize (CArchive* pArchive);

 

    // Members

    protected:

      CPtrArray m_coloredPoints;

  }

下面是我们序列化这个类的代码:

  int ColoredPointList::serialize

    (CArchive* pArchive)

  {

    ASSERT (pArchive != NULL);

    int nStatus = Status::Success;

 

    // Step 1: Serialize signature and version

    int nVersion;

    try {

      if (pArchive->IsStoring()) {

          (*pArchive) << ColoredPointList::m_strSignature;

          (*pArchive) << ColoredPointList::m_nVersion;

      } else {

          CString strSignature;

          (*pArchive) >> strSignature;

          if (strSignature != ColoredPointList::m_strSignature)

             return (Status::InvalidFormat);

          (*pArchive) >> nVersion;

          if (nVersion > ColoredPointList::m_nVersion;)

             return (Status::UnsupportedVersion);

      }

 

      // Step 2: Serialize base class (if any)

      //

      // Nothing to do since ColoredPointList isn't derived from anything.

      // But if it was derived from BaseColoredPointList, we'd do:

      //

      // nStatus = BaseColoredPointList::serialize (pArchive);

      // if (nStatus != Status::Success)

      //    return (nStatus);

 

      // Step 3: Serialize number of items in collection

      int nItems = 0;

      if (pArchive->IsStoring()) {

           nItems = m_coloredPoints.GetSize();

           (*pArchive) << nItems;

      } else

           (*pArchive) >> nItems;

 

      // Step 4: Serialize each object in collection

      for (int nObject=0; (nObject < nItems); nObject++) {

 

          // 4a: Point to object being serialized

          ColoredPoint* pColoredPoint = NULL;

          if (pArchive->IsStoring())

             pColoredPoint = (ColoredPoint *) m_coloredPoints.GetAt (nObject);

          else

             pColoredPoint = new ColoredPoint();

          ASSERT (pColoredPoint != NULL);

 

          // 4b: Serialize it

          nStatus = pColoredPoint->serialize (pArchive);

          if (nStatus != Status::Success)

             return (nStatus);

          if (!pArchive->IsStoring())

             m_coloredPoints.Add (pColoredPoint);

      }

 

      // Step 5: Serialize object's other members (if any)

      //

      // Nothing to do since ColoredPointList doesn't have any other

      // members. But if it contained an int (m_nSomeInt) and a Foo

      // object (m_foo), we'd do:

      //

      // if (pArchive->IsStoring())

      //    (*pArchive) << m_nSomeInt;

      // else

      //    (*pArchive) >> m_nColor;

      //

      // nStatus = m_foo::serialize (pArchive);

      // if (nStatus != Status::Success)

      //    return (nStatus);

 

    }

    catch (CException* pException) {

      // A read/write error occured

      pException->Delete();

      if (pArchive->IsStoring())

        return (Status::WriteError);

      return (Status::ReadError);

    }

 

    // Object was successfully serialized

    return (Status::Success);

  }

 

序列化一个异质集合类

移植集合类用来存储动态大小的不同类型的集合对象。序列化内容为:

1.         序列化类的签名和版本信息

2.         序列化基类

3.         序列化集合内条目的数据<<额外步骤

4.         序列化集合内的每个对象<<额外步骤

a)         序列化这个对象的签名

b)        序列化这个对象

5.         序列化类的成员变量

 

你可以注意到只有在第四步中增加了一个处理步骤。这里我们再序列化每个对象本身之前先序列化他的签名。这样做是为了方便的读取数据。当我们序列化一个同质集合类,我们只需要按照相同的类型解析。在读取ColoredPoint时,我们在堆中创建一个对象并调用它的serialize()函数就可以了。

  ColoredPoint* pColoredPoint = new ColoredPoint();

  nStatus = pColoredPoint->serialize (pArchive);

当我们读取一个异质集合类,我们需要知道要读取对象的类型,这是对象的签名起到了作用。

  // Read object signature

  CString strSignature;

  pArchive >> strSignature;

 

  // Construct object of appropriate type

  ISerializable* pObject = NULL;

  if (strSignature == ColoredPoint::m_strSignature)

     pObject = new ColoredPoint();

  else

    if (strSignature == Line::m_strSignature)

       pObject = new Line();

    else

       if (strSignature == Rectangle::m_strSignature)

          pObject = new Rectangle();

       else

          return (Status::InvalidFormat);

  ASSERT (pObject != NULL);

 

  // Read it back in

  nStatus = pObject->serialize (pArchive);

上面的片断中,ColoredPointLineRectangle同时继承于相同的基类ISerializableISerializable是一个只包含虚函数的接口类,并定义成员函数getSignature()getVersion()serialize()

  class ISerializable

  {

    // Construction/destruction

    public:

      ISerializable::ISerializable()

        { }

      virtual ISerializable::~ISerializable()

        { }

 

    // Operations

    public:

      // Get the object's signature

      virtual CString getSignature() = 0;

 

      // Get the object's version

      virtual int getVersion() = 0;

 

      // Serialize the object

      virtual int serialize (CArchive* pArchive) = 0;

  }

OK 让我们序列化这个类,下面的例子,ShapeList类是不同类型的成员对象(ColoredPoint, Line Rectangle)的集合。所有的对象类都继承自接口类ISerializable

  int ShapeList::serialize

    (CArchive* pArchive)

  {

    ASSERT (pArchive != NULL);

    int nStatus = Status::Success;

 

    // Step 1: Serialize signature and version

    int nVersion;

    try {

      if (pArchive->IsStoring()) {

          (*pArchive) << ShapeList::m_strSignature;

          (*pArchive) << ShapeList::m_nVersion;

      } else {

          CString strSignature;

          (*pArchive) >> strSignature;

          if (strSignature != ShapeList::m_strSignature)

             return (Status::InvalidFormat);

          (*pArchive) >> nVersion;

          if (nVersion > ShapeList::m_nVersion;)

             return (Status::UnsupportedVersion);

      }

 

      // Step 2: Serialize base class (if any)

      //

      // Nothing to do since ShapeList isn't derived from anything.

      // But if it was derived from BaseShapeList, we'd do:

      //

      // nStatus = BaseShapeList::serialize (pArchive);

      // if (nStatus != Status::Success)

      //    return (nStatus);

 

      // Step 3: Serialize number of items in collection

      int nItems = 0;

      if (pArchive->IsStoring()) {

           nItems = m_shapes.GetSize();

           (*pArchive) << nItems;

      } else

           (*pArchive) >> nItems;

 

      // Step 4: Serialize each object in collection

      for (int nObject=0; (nObject < nItems); nObject++) {

 

          // 4a: First serialize object's signature

          CString strSignature;

          if (pArchive->IsStoring())

             (*pArchive) << pObject->getSignature();

          else

             (*pArchive) >> strSignature;

 

          //

          // 4b: Then serialize object

          //

 

          // 4b (1): Point to object being serialized

          ISerializable* pObject = NULL;

          if (pArchive->IsStoring())

             pObject = (ISerializable *) m_shapes.GetAt (nObject);

          else {

             if (strSignature == ColoredPoint::m_strSignature)

                pObject = new ColoredPoint();

             else

               if (strSignature == Line::m_strSignature)

                  pObject = new Line();

               else

                  if (strSignature == Rectangle::m_strSignature)

                     pObject = new Rectangle();

                  else

                     return (Status::InvalidFormat);

          }

          ASSERT (pObject != NULL);

 

          // 4b (2): Serialize it

          nStatus = pObject->serialize (pArchive);

          if (nStatus != Status::Success)

             return (nStatus);

          if (!pArchive->IsStoring())

             m_shapes.Add (pObject);

      }

 

      // Step 5: Serialize object's other members (if any)

      //

      // Nothing to do since ShapeList doesn't have any other

      // members.  But if it contained an int (m_nSomeInt) and

      // a Foo object (m_foo), we'd do:

      //

      // if (pArchive->IsStoring())

      //    (*pArchive) << m_nSomeInt;

      // else

      //    (*pArchive) >> m_nColor;

      //

      // nStatus = m_foo::serialize (pArchive);

      // if (nStatus != Status::Success)

      //    return (nStatus);

 

    }

    catch (CException* pException) {

      // A read/write error occured

      pException->Delete();

      if (pArchive->IsStoring())

        return (Status::WriteError);

      return (Status::ReadError);

    }

 

    // Object was successfully serialized

    return (Status::Success);

  }

 

类工厂

你可以将代码中讨厌的if块用类工厂来替换,你可以参考以下文章:

尽管这些文章看起来比较复杂,但是都蕴含一个简单的道理。类工厂只比一个类多了一个静态成员函数Create()来创建一个制定类型的对象。你可以使用类工厂的Create()函数来隐藏丑陋的if语句块,使代码更加简洁。

  ...

  // Construct object of appropriate type

  ISerializable* pObject = MyClassFactory::create (strSignature);

  ASSERT (pObject != NULL);

  ...

 

总结

尽管这个入门手册使用了MFCCStringCPtrArray对象,但是序列化过程不是专门针对MFC的。它可以操作存储任何软件中的任何信息到持久存储介质。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
前台: (1)注册登录模块:按照学校的相关规定进行注册和登录。 (2)招聘信息查看:高校毕业生们可以网站首页上查看所有的招聘信息,除此之外还可以输入公司名称或岗位名称进行搜索。 (3)用人单位模块:此模块为宣传用人单位的主要功能模块,具体包括用人单位简介、岗位需求及职责及公司介绍等功能。 (4)就业指导:学生朋友们在就业前可以通过此模块获取指导。 (5)新闻信息:为了让用户们可以了解到最新的新闻动态,本系统可以通过新闻信息查看功能阅读近期的新闻动态。 (6)在线论坛:毕业季的同学们可以通过此模块相互交流。 后台: (1)系统用户管理模块:可以查看系统内的管理员信息并进行维护。 (2)学生管理模块:通过此功能可以添加学生用户,还可以对学生信息进行修改和删除。 (3)用人单位管理模块:管理员用户通过此模块可以管理用人单位的信息,还可以对用人单位信息进行查看和维护。 (4)招聘管理模块:管理员通过此功能发布和维护系统内的照片信息。 (5)就业指导管理模块:通过此模块可以编辑和发布就业指导信息,从而更好的帮助就业季的同学们。 (6)论坛管理:通过论坛管理可以查看论坛中的主题帖及里面的回复信息,除此之外还可以对论坛中的信息进行维护和管理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值