《C++ primer plus》学习笔记:10~14章


《C++ primer plus》学习笔记 第十章 对象和类型

  1. 类型是什么

举个栗子,男生是什么?受固有模式影响,可能会指出男生的外表特点,但又可以从行为上定义男生。那么对于内置数据类型,倾向于根据数据的外观(在内存如何存储),来考虑数据类型。例如,char占用一个字节,double占用8个字节,同时也规定了可以对数据类型进行的操作,例如,int类型可使用所有的算术运算符,包括%。指针类型的内存数量可能与int相同,但不能对其进行算术运算。

因此声明变量类型不仅分配内存,还规定了可对变量执行的操作
指定基本类型将完成3项工作:

  • 决定数据对象所需的内存数量
  • 决定如何解释内存中的位(像long和double在内存中占用的位数相同,但将他们转化为数值的方式不同)
  • 决定可使用数据对象执行的操作

类如何实现封装、抽象、数据隐藏
抽象:人们可以使用类方法的公有接口对类对象执行操作
封装:对实现具体细节的隐藏
数据隐藏:私有成员

C++目标是使得使用类与使用基本内置类型尽可能相同。

  1. 接口

OOP程序员通常依照客户/服务器模型来讨论程序。接口是共享框架,供两个系统(例如,计算机与打印机之间,用户与应用程序之间)交互时使用,用户不能直接将脑子的想法传达给程序,必须同程序提供的接口交互,用户敲键盘,计算机将字符显示在屏幕,用户移动鼠标,计算机移动屏幕上的光标。
类可以说是一个公共接口,公共用户指的是使用类的程序(也可以是编写程序的人),交互系统由类对象组成,接口由方法组成,接口能够让程序员编写与类对象交互的代码,从而让程序能够使用类对象。

  1. 客户/服务器模型

在这个概念中,客户是使用类的程序。类声明构成服务器,它是程序可使用的资源。客户只能通过以共有方式定义的接口使用服务器,客户的唯一责任是了解该接口,服务器(服务器设计人员)的责任是确保服务器根据该接口可靠并准确地执行。且服务器设计人员只能修改实现细节,而不能修改接口。

  1. 访问控制

使用类对象的程序可以直接访问公有部分,但只能通过公有成员函数(或友元函数)访问对象的私有成员。因此公有成员函数是程序和对象的私有成员之间的桥梁,提供了对象和程序之间的接口。防止程序直接访问私有成员被称为数据隐藏

类设计尽可能将公有接口与实现细节分开。
公有接口表示设计的抽象组件。将实现细节与抽象分开称为封装。数据隐藏、实现细节隐藏在私有部分、声明与类定义放在不同文件都是封装。
原则上将实现细节从接口设计中分离,如果以后找到了更好的实现数据表示或成员函数细节的方法,可对细节修改,而无需修改程序接口,便于维护。

  1. 实现类成员函数

除了函数参数、返回值、函数头、函数体还有两个特征

  • 定义成员函数时需使用解析运算符,标识所属的类
  • 类方法可以访问private组件

作用域解析运算符确定了方法定义对应的类,此外,类中的函数具有类作用域(class scope)。因为同属于一个类,其他的成员函数可以不必使用作用域解析运算符就可以使用该方法,而在类声明和定义之外需采用特殊措施。通常类声明包含在独立头文件中,类定义放在源文件中,使用该类的文件需包含头文件,从而编译器寻找函数定义(前面提到的单独编译)。

内联函数
有两种形式,其中位于类声明中的定义将自动转内联函数
也可以在类声明之外,函数定义时使用incline限定符。
内联函数与一般函数不同的是,一般函数只能有一个定义,在多文件程序中也是如此的,而内联函数要求每个使用它的文件都要有定义,所以确保其在所有文件中有效的方法是将内联定义放在类声明的头文件中

同一个类的所有对象共用同一组类方法
所创建的每个对象都有自己的存储空间,用于存储内部的变量和类成员,但同一个类的所有对象共用同一组类方法,调用时执行同一个代码块。在OOP中调用成员函数被称为发送消息,因此将同一消息发送给两个不同的对象将调用同一个方法。

  1. 类的构造函数

C++目的之一是让使用类像使用内置类型一样,但是初始化类对象的时候,由于类成员的私有化,常规语句无法对类对象赋初值,程序只能通过成员函数来访问数据成员,因此类构造函数专门用于构造新对象,将值赋给他们的数据成员。

构造函数的原型和函数头有一个有趣的特点——虽然没有返回值,但没有被声明为void。
声明
Stock(参数1,参数2,……);
定义
Stock::Stock(参数1,参数2,……) {……}

成员名与参数名
不熟悉构造函数可能将类成员名称作为构造函数的参数,但这没有意义。
为避免这一混乱,常见的做法是在数据成员名中使用m_前缀
例如:

class Stock{
private:
   string m_name;
   int m_nums;
……
}

也可以在数据成员名后加后缀_

class Stock{
private:
   string name_;
   int nums_;
……
}

创建构造函数

声明
Stock(const char *s,const int n);
定义
Stock::Stock(const char *s;const int n)
{
m_name=s;
m_nums=n;
}

调用
一种方式是显示地使用构造函数
Stock food=Stock(“Cabbage”,250);
另一种是隐式的调用
Stock food(“Cabbage”,250);
还可以使用new创建
Stock *ptr=new Stock(“Cabbage”,250);
无法使用对象调用构造函数,因为在未使用构造函数时,对象并未创建。

默认构造函数

Stock food;
2中的示例是提供初始值时调用的构造函数,默认构造函数是在未提供初始值时,用来创建对象的构造函数。

声明  Stock();
定义  Stock::Stock() {};

在没有提供任何构造函数的情况下,C++将自动提供默认构造函数,它没有参数且不会对数据成员进行初始化.
另外,只有在没有定义任何构造函数的前提下,C++才会提供默认构造函数,当为类定义了任何构造函数,那么就必须提供默认构造函数。

定义默认构造函数有两种方法

1)利用默认参数为所有的数据成员提供默认值
声明 Stock(const char *s=”no name”,const int n=250);
定义 Stock::Stock(const char *s;const int n)
{
m_name=s;
m_nums=n;
}

2)函数重载提供一个没有参数的构造函数
声明 Stock();
定义 Stock::Stock()
{
m_name=”no name”;
m_nums=0;
};

  1. 构造函数重载及初始化方法

构造函数的名称与类名相同,但通过函数重载,可以创建多个同名的构造函数,条件是每个函数的特征标(参数列表)不同。

Stock::Stock(const char *s, int n);    //constructor prototype
Stock food=Stock(“Cabbage”,250);  //primer form
Stock food(“Cabbage”,250);        //short form
Stock *ptr=new Stock(“Cabbage”,250)//dynamic object

也可以使用初始化列表

Stock food={“Cabbage”,250};     //C++11
  1. 析构函数

用构造函数创建对象后,程序负责跟踪该对象,直到过期为止,对象过期将自动调用一个特殊的成员函数——析构函数,完成清理工作。与构造函数一样析构函数也没有声明类型和返回值。
声明 ~Stock()
定义 Stock::~Stock() {}
对于用存在new创建的数据对象,则在析构函数中一定要使用delete来释放内存。析构函数的调用由编译器决定。如果创建静态存储类型对象,则其析构函数将在程序结束时被调用,如果创建的是自动存储类对象,则其析构函数将在程序执行完代码块时被自动调用。

  1. #ifndef技术

假设stock.h存有stock类的声明部分

#ifndef  STOCK_H_ 
#define  STOCK_H_
class Stock
{
……
}
#endif

在一个头文件中只能包含同一个头文件一次,但很可能在不知情的情况下将头文件多次包含。例如使用包含了另一个头文件的头文件。预处理编译指令#ifndef意味着当且仅当没有使用预编译指令#define**定义名称STOCK_H_**的时候,才处理#ifndef和#end之间的语句。
STOCK_H_是我们创建的名称,编译器首次遇见该文件的时候,名称没有定义,当文件检测到名称被定义后,则跳过代码段。

  1. 赋值与初始化
Stock stock1=Stock(“Cabbage”,100);
Stock stock1;
stock1= Stock(“Cabbage”,100);   //temperory object

对于既可以通过初始化构造,同时可以通过复制的对象采用哪种呢?对于第一种来说,使用默认构造函数可能会创建临时对象(也可能不会),但对于第二种情况,在赋值前一定会构建一个临时对象。所以应该使用直接初始化。

  1. this 指针

上述例子中设一个成员函数求两个Stock类中nums大的对象

const Stock& Stock::Max(const Stock& a)const
{    
     if(a.nums>nums)     //nums实际上是this->nums
          return s;
     else
         return *this;
}

那么通过调用stock1.Max(stock2)即可得到nums大的对象
对象方法可以不显示地指明,而调用对象的数据成员,实际上是由this指针指向对象来完成的(this被作为隐式参数传递给方法),这样调用方法是this指的是调用它的对象的地址。每个成员函数都有一个this指针,this指针指向调用对象,如果方法需要引用整个调用对象,则可使用表达式 *this(注意 * this是作用对象本身,而不是地址)。

参数中的const表示作为引用参数的类不可修改,括号后的const将this限定为const,这样将不能使用this来修改对象的值,由于参数和调用对象都是const引用,所以返回值也是const引用(返回类型为引用意味着调用对象本身,不是副本)。

  1. 对象数组

Stock food[2];
当程序创建未被显示初始化的类对象,总是调用默认构造函数。

Stock food[2]=
{
    Stock(“Cabbage”,250);
    Stock(“radish”,280);
}

为每个元素显示地初始化。

  1. 类作用域

在类中定义的名称(类数据成员和类成员函数)的作用域为整个类,类中的名称只在该类中已知,在类外是不可知的。另外,类作用域意味着不能从外部直接访问类的成员,要调用就必须通过对象。
在类声明和类成员函数定义中,可以使用未修饰的类成员名称,在其他情况下,使用类成员名时,必须根据上下文使用成员运算符,或者作用域解析符

  1. 作用域为类的常量

有时使用作用域为类的符号常量,例如,用来指定数组长度。

class Stock{
private:
   const int length=20;
   char s[length];
   ……
}

这种方法是错误的,因为类声明只是描述对象的形式,在创建对象之前,是不应该有存储空间的。

static const int length=20 

使用关键字static,将该常量以其他静态变量存放在一起,而并不是存放在对象中,因此length被所有的对象共享

  1. 作用域内的枚举

有时两个枚举中的枚举量可能发生冲突。
例如,

enum egg{small,mid,large};
enum shirt{small,mid,large};

这样将无法编译,因为他们处于相同的作用域中。
而使用作用域为类的枚举量,将避免发生错误。

  • 声明枚举将不会创建变量(所以枚举也可被用来表示常量)
  • 使用时加上作用域解析符
enum class egg{small,mid,large};
enum class shirt{small,mid,large};
egg choice =egg::small;
egg choice1 -shirt::small;

《C++ primer plus》学习笔记 第十一章 使用类

  1. 运算符重载

用算符重载实际上是隐藏内部机理,强调实质,强调OOP

operator op(argument-list)

op必须是有效的C++运算符(不能虚构),例如,operator函数将重载[]运算符,[]是数组索引运算符。
假设一个Time类

class Time
{
privateint hours;
    int minutes;
……
}

这里希望有一个实现两个时间相加的功能,有两种方法

  • 普通函数
  • 运算符重载函数

普通成员函数版本

//声明:
Time Time::Sum(const Time &t) const;
//定义:
Time Time::Sum(const Time &t) const
{
   Time sum;
   sum.minutes=minutes+t.minutes;
   sum.hours=hours+t.hours+sum.minutes/60;
   sum.hours%=24;
   sum.minutes%=60;
   return sum;
}
//调用:
time a=b.Sum(c);

这里两个需要注意:一个是,这里返回不得使用引用,因为返回既不是引用参数,也不是被调用对象,而是自动的sum,在函数结束后sum内存将释放;其次,参数中的const指出参数是不可变的,而括号后的const指出被调用对象的不可变,这在上面的内容中也说过。

运算符重载版本

//声明:
Time Time::operator+(const Time &t)const;
//定义:
Time Time::operator+(const Time &t)const
      {
   Time sum;
   sum.minutes=minutes+t.minutes;
   sum.hours=hours+t.hours+sum.minutes/60;
   sum.hours%=24;
   sum.minutes%=60;
   return sum;
      }
//调用有两种形式
Time a=b+c;
Time a=b.operator+(c);

从上面可以看出来,运算符重载其实就是一个普通的成员函数,只是为了使得实现的功能更加形象
其次运算符重载可以这样使用

Time a=b+c+d;
//+是从左往右结合的运算符,所以实质上是这样的形式
Time a=b.operator+(c.operator(d));

重载限制
1) 重载后的运算符必须至少有一个操作数是用户定义的类型,这防止了用户为标准类型重载运算符,例如,不能将减法(-)重载为两个int 的和。
2) 不能违反原来的运算规则,例如,将具有两个操作数的运算符,变成只有一个操作数。
3) 不能创建新运算符,例如operator **()表示求幂。
4) 有些运算符不能重载
5) 大多数运算符可以通过成员函数或者非成员函数重载运算符。但是有些只能通过成员函数来实现。(在书中P387有详细介绍)

  1. 友元

C++控制对类对象的私有访问。而公有类方法也只能通过对象调用。这种限制有时候太严格,不适合特定编程场合。C++提供另一种访问权限:友元

  • 友元函数
  • 友元类
  • 友元成员函数
  1. 友元函数

通过让函数成为类的友元,可以赋予该函数与类成员函数相同的访问权限。

友元的使用情景
假设上述Time类想要实现一个时间翻倍

//声明:
Time operator*(double a) const;
//调用:
Time A=B*2.5

那如果改成了A=2.5*B则会出错,因为成员函数要求左侧的操作数必须是对象
且如果告诉每个人只能使用一种形式,这是一种对服务器友好-客户警惕(sever-friendly,client-beware)解决方案,与OOP相悖。

友元可解决上述情况
为类重载运算符时,非类项作为第一个操作数,则可以用友元函数反转操作数顺序。

假设m和t分别为左右的操作数
1)将原型放在类中声明,并在原型中加上关键字friend:

friend Time operator*(double m,const Time &t);

注意:

  • 虽然operator*()函数在类声明中声明的,但并不是成员函数,也不能使用成员运算符调用。
  • 虽然不是成员函数,但访问权限相同。

2)函数定义:因为不是成员函数,定义时不需要限定符Time::也不要使用关键字friend

Time operator*(double m,const Time &t)
{
      Time result;
      ……
      return result;
}

非成员函数非友元方法

//声明:
Time operator*(int m,const Time a);
//定义:
Time operator*(int m,const Time a)
{
     return a*m;
}

首先运算符重载函数的成员函数只允许有一个参数,而非成员函数可使用多个参数,这里巧妙地使用返回调换操作数的位置,从而让其使用成员函数,这种return的使用方法在类继承上也经常用到,其次return也可用来获取私有成员。

  1. 友元是否有悖于OOP

友元机制允许非成员函数访问私有数据,违反了OOP数据隐藏的原则。但实际上应将友元视为类的扩展接口的组成部分。因为上述Timedouble和doubleTime完全相同,只是C++句法影响。而且只有在类声明可以决定哪个函数是友元,因此类声明可以控制哪些函数可以访问私有数据。总之,类方法和友元只是表达类接口的两种不同机制。

  1. 常用的友元:重载<<运算符

ostream类对<<进行了重载,对所有基本类型重载了operator<<(),所以cout作为ostream的对象,可以识别不同的类型进行输出。而要使用cout来识别Time并进行输出,一种方法是将一个新的运算符重载函数添加到ostream类中,这要修改iostream文件这是危险的,另一种是让Time类知道如何用cout输出。

1) <<的第一个重载版本
首先考虑使用成员函数,成员函数则必须将Time类作为第一个操作数
那么使用时情况如下:

Time t;
t<<cout;

这种新式不易理解

其次,考虑友元函数

void operator<<(ostream &os,Time a)
{
     os<< a.h<<”小时 ”<<a.m<<”分”;
}

这样使用时可以将cout作为第一个操作数。

Time t;
cout<<t;

便可以输出相应的Time格式了。
注意参数ostream &os,os在调用时可以是cout也可以是cerr,os可看做它们的别名,其次,引用则说明使用的是cout对象本身,而不需要拷贝。

2) <<重载第二个版本

上述函数的返回值void
对于连续输出cout<<a<<b;这样的情况不可用

了解iostream中对于<<运算符的定义
<<运算符要求左边是一个ostream对象,显然,因为cout是ostream对象,所以表达式cout<<a满足要求,而要使后续的<<也满足这种要求,也就是说cout<<a位于<<b的左边也满足这种要求,所以使cout<<a返回一个ostream对象来。实际上ostream将operator<<()函数返回一个调用对象的引用(这里是cout)。

ostream& operator<<(ostream &os,Time a)
{
     os<< a.h<<”小时 ”<<a.m<<”分”;
     return os;
}
  1. 重载运算符作为成员函数还是非成员函数

对于很多运算符来说,成员函数和非成员函数都可实现运算符的重载。

Time operator+(double m)const;      //member funcation
friend Time operator+(const Time t,double m);      //nonmember funcation
  • 对于成员函数版本,this可隐式地传递一个操作数,而非成员函数只能通过参数显示地传递。
  • 成员函数的版本所需参数少于等于1个,而非成员函数参数个数与操作数相同。
  • 对某些运算符来说,只能通过成员函数重载,有些类设计则使用非成员函数版本更好。
  1. 类的自动转换和强制类型转换

构造函数可用来实现类型转化,且只有接受一个参数(也可以函数参数为多个,但使其他参数设置为默认参数,那么使用时便可以接受一个参数)的构造函数才能作为转换函数。

//声明:
Time(int k);
//定义:
Time::Time(int k)
{
     h=k;
     m=0;
}
//调用:
Time t;
t=1;

这样便实现了由int到Time类的转换(隐式地转换,没有使用转换函数)。
但有时候这种自动特性会导致意外的类型转换,因此,C++新增的关键字explicit,用于关闭这种自动特性。

//声明:
explicit Time(int k);
//调用:
Time t;
t=1;         //no valid
t=Time(1);   
t=(Time)1;
  1. 转换函数

上述的强制类型转换是从某种类型到类类型的转换(构造函数只能用于此),而要实现从类类型到其他类型的转换,则需要特殊的运算符函数——转换函数。

转换函数的特点

  • 必须是类方法
  • 不能指定返回类型
  • 不能有参数
//声明:
operator int() const;
//定义:
Time::operator int() const
{
     return h;
}
//调用:
Time t;
int a=int(t);
int a=(int)t;

同样关键字explicit可以禁止int a=t这样隐式地转换。


《C++ primer plus》学习笔记 第十二章 类和动态内存分配

  1. 静态类成员变量
//类中声明:
static int length;
//类外初始化:
int Class::length=0;

静态成员变量可以在类声明之外使用单独的语句进行初始化,因为静态类成员单独存储,不是类对象的组成部分。注意初始化使用作用域解析符,但并没有使用关键字static。
初始化是在方法文件中,而不是在类声明文件中,如果在类声明文件中进行,程序可能将头文件中包含在多个文件中,则将出现多个static变量初始化语句,引发错误。如果静态变量为const则可以在类声明中初始化(这里可知将声明和定义放在不同的文件中,将避免多次定义)。

  1. 特殊成员函数

C++将自动提供下面的成员函数

  • 默认构造函数,如果没有定义任何构造函数
  • 默认析构函数
  • 复制构造函数
  • 赋值运算符
  • 地址运算符

1)默认构造函数
前面提到了默认构造函数有3种。这里不进行赘述了。

2)复制构造函数
复制构造函数用于将一个已有对象复制到新创建的对象。它在初始化过程而并非常规赋值过程中。

//声明
Class_name(const Class_name &);
//调用
Class_name A(B);              //calls Class_name(const Class_name &);
Class_name A=B;
Class_name A=Class_name(B);
Class_name * A=new Class_name(B);

每当程序生成一个对象副本时,编译器将使用复制构造函数。也就是说,当函数按值传递对象参数函数返回对象时,都将使用复制构造函数。

默认复制构造函数的特点:浅复制
默认复制构造函数逐个复制非静态成员(成员复制也叫作浅复制),复制的是成员的值。
一个很重要的问题是,这种浅复制对于指针类成员时,将只是复制地址,即将指向原有类成员的地址,那么当使用析构函数解析临时对象时,导致原有类成员也被删去,因此需避免这样的情况。
深度复制
对于使用new初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据,而不是指针,这叫做深度复制

3)赋值运算符

Class_name & Class_name::operator=(const Class_name &)
Class_name A; 
A=B;

默认的赋值运算符同样是浅复制。(赋值运算符只能由成员函数重载)。
对于作为类成员的指针,应当重新定义复制构造函数和赋值运算符,避免浅复制。

赋值运算符重载的其他问题
这里是一个类中存在指针str的例子:

Class_name & Class_name::operator=(const Class_name &st)
{
            if(this==&st)
                return *this;
            delete [] str;
            str=new char [strlen(st.str)+1];
            strcpy(str,st.str);
            return *this;
}
  • 由于目标对象引用之前的数据,应delete[]删除原来的数据。
  • 函数避免将对象赋给自身,否则在赋值前将释放内存。
  • 函数返回一个指向调用对象的引用
  1. C++11的空指针
  • 字面值0有两种含义:可表示数字0,也可表示空指针
  • (void *)0、NULL、nullptr均可用于表示空指针
  1. 比较成员函数

比较成员函数的友元,可实现类对象与C字符串的比较。


《C++ primer plus》学习笔记 第十三章 类继承

  1. 为什么有类继承

面向对象编程的主要目的之一是提供可重用的代码。重用经测试过的代码比重新编写要好的多。
传统的C函数库通过预定义、预编译的函数提供了可重用性。很多厂商提供了专用的C库,这些专用库提供标准C库没有的函数。例如,可以购买数据库管理函数库和屏幕控制函数库。然而函数库由局限性。除非厂商提供源代码(一般不提供),否则无法根据吱声需求对函数扩展或修改,必须根据库的情况修改程序。即使厂商提供了源代码,在修改时也有风险。
很多厂商提供了类库,类库由类声明和实现构成。类组合了数据表示和类方法这提供了比函数库更加完整的程序包。例如,单个类可以提供历对话框的全部资源。通常类库以源代码的方式提供,可以修改。但C++提供了类继承扩展和修改类。

  • 可在已有类的基础上添加新功能
  • 可以给类添加数据
  • 可以修改类方法的行为
  1. 公有派生

假设已有一个描述乒乓球俱乐部成员的类 TennisPlayer类

class TennisPlayer
{
public:
     string name;
     bool vip;
     int years;
}

假设还有一个描述乒乓球俱乐部队长的类 CaptainPlayer类继承TennisPlayer类

class CaptainPlayer : public TennisPlayer
{
    unsigned int members;
……
}

上述的声明头表明TennisPlayer是一个公有基类,这被称为公有派生。使用公有派生,基类的公有成员将成为派生类的公有成员,基类的私有成员将成为派生类私有的一部分,但只能通过基类的公有或者保护方法访问。

  • 派生类对象存储基类的数据成员(派生类继承了基类的实现)
  • 派生类对象使用基类方法(派生类继承了基类的接口)

需要在继承类中添加的

  • 派生类需要自己的构造函数
  • 根据需要添加自己的成员函数和变量
  1. 构造函数:访问权限的考虑

派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。那么继承类的构造函数无法直接设置继承类中的基类成员,而必须使用基类的公有方法来访问私有的基类成员。也就是说,派生类构造函数必须使用基类构造函数。

创建派生类时,程序首先创建基类对象。C++使用初始化列表语法完成这样的功能。
上述CaptainPlayer构造函数的代码

CaptainPlayer::CaptainPlayer(unsigned int r,string &st,bool ht,int nt):TennisPlayer(st,ht,nt)
{
           members=r;
}

TennisPlayer(st,ht,nt)是成员初始化列表,它是可执行代码,调用TennisPlayer构造函数(前提是TennisPlayer必须定义有这样的构造函数)。

假设程序有如下声明:

CaptainPlayer Cplayer1(5,“Jerry”,true,30);

首先Cplayer1构造函数将把实参“Jerry”,true,30赋给形参st,ht,nt,然后传递给TennisPlayer(st,ht,nt)构造函数。后者将创建一个嵌套的TennisPlayer对象,然后,程序进入CaptainPlayer构造函数体,完成CaptainPlayer对象的创建。

如果省略成员初始化列表
则将默认调用基类的默认构造函数

CaptainPlayer::CaptainPlayer(unsigned int r,string &st,bool ht,int nt) //:TennisPlayer()
{
           members=r;
}

其次也可使用复制构造函数

CaptainPlayer::CaptainPlayer(unsigned int r,const TennisPlayer & tp) :TennisPlayer(tp)
{
           members=r;
}

同时也可以对继承类成员使用成员初始化列表

CaptainPlayer::CaptainPlayer(unsigned int r,string &st,bool ht,int nt):TennisPlayer(st,ht,nt)members(r)
{}

派生类构造函数的要点

  • 首先创建基类对象
  • 派生类构造函数通过成员初始化列表将基类信息传递给基类构造函数
  • 派生类构造函数应初始化新增的数据成员

释放的顺序也相反,先调用派生类的析构函数,后调用基类的构造函数。注意使用基类构造函数应指明是哪个基类构造函数,否则将使用默认构造函数。

  1. 派生类与基类之间的特殊关系
  • 派生类对象可以使用基类的方法(方法不是私有的)
  • 基类指针可以指向派生类对象
  • 基类引用可以引用派生类对象(注意基类指针和引用只能调用基类方法,不能调用派生类方法)

然而上述只能是单向的,不能将基类对象或地址赋给派生类引用或指针,这也是有道理的,允许基类引用隐式地引用派生类对象,则可以使用基类引用为派生类对象调用基类方法,因为派生类继承了基类方法。相反则没有意义。

基类引用参数或者指针参数可以指向基类对象或者派生类对象:

//定义
void Show(const TennisPlayer &t)
{
     ……
}
//调用
TennisPlayer player1(“Jerry”,true,20);
CaptainPlayer player2(“Tom”,false,32);
Show(player1);
Show(player2);

引用兼容,能够使得将基类对象初始化为派生类对象:

CaptainPlayer player2(“Tom”,false,32);
TennisPlayer player1(player2);

实际上是使用了基类的复制构造函数TennisPlayer(const TennisPlayer &);将嵌套在派生类对象的player1中的基类初始化为player2。

同样可以将派生类对象赋值给基类对象:

TennisPlayer player1(“Jerry”,true,20);
CaptainPlayer player2;
player2=player1;

程序隐式重载赋值运算符TennisPlayer & operator=(const TennisPlayer &)const;

  1. 多态公有继承

上面的CaptainPlayer继承示例很简单,均使用基类方法,未做任何修改。当希望同一个方法在派生类和基类的行为不同时,即方法的行为取决于调用方法的对象——多态。有两种方法可以实现多态公有继承

  • 在派生类重新定义基类方法
  • 使用虚方法

这里假设一个基类Brass表示基本支票账户,公有派生类Brass Plus添加可透支功能。

class Brass
{
private:
    string name;
    int acctNum;
    double balance;
public:
    Brass(const string &st=”Null”,int nt=0,double dt=0.0);
    double Balance() {return balance;} const;
    virtual void ViewAcct() const;
    ~Brass() {}
}

class BrassPlus:public Brass
{
private:
    double loan;
public:
    BrassPlus(const string &st=”Null”,int nt=0,double dt=0.0,double ln=0.0)
    virtual void ViewAcct() const;
    ~BrassPlus() {}
}

上述例子中的方法ViewAcct()有两个版本,一个供Brass对象使用,一个供BrassPlus对象使用。那么既可以使用virtual,又可以不使用virtual,这两者的区别
如果方法通过指针或者引用而不是对象调用,有virtual时,程序将根据引用或指针指向的对象类型选择方法,没有virtual,则将根据引用类型或指针类型选择方法。

示例:(我们知道基类引用可以是派生类对象)
非virtual情况

//behavior with non-virtual ViewAcct()
Brass dom(“Jerry”,11101,2000.0);
BrassPlus dot(“Gina”,11102,4000.0);
Brass &b1=dom;
Brass &b2=dot;
b1.ViewAcct();    //use Brass:: ViewAcct();
b2.ViewAcct();    //use Brass:: ViewAcct();

virtual情况

//behavior with non-virtual ViewAcct()
Brass dom(“Jerry”,11101,2000.0);
BrassPlus dot(“Gina”,11102,4000.0);
Brass &b1=dom;
Brass &b2=dot;
b1.ViewAcct();    //use Brass:: ViewAcct();
b2.ViewAcct();    //use BrassPus:: ViewAcct();

类函数定义

Brass::Brass(const string &st=”Null”,int nt=0,double dt=0.0){}
void Brass::ViewAcct()
{
  std::cout<<name;
  std::cout<<accNum;
  std::cout<<balance;
}
BrassPlus::BrassPlus(const string &st=”Null”,int nt=0,double dt=0.0,double lo=0.0 ):  Brass(st,nt,dt)
{
    loan=lo;
}
void BrassPlus::ViewAcct()
{
   //double bal=Balance();
   Brass::ViewAcct();
   std::cout<<loan;
}

这里注意派生类并不能直接访问基类的私有数据,而必须使用基类的公有方法才能访问这些数据,构造函数可使用成员初始化列表,而非构造函数使用作用域解析符调用公有类的方法(若不使用则认为是函数的递归),其次(保护)内联函数用于传递私有成员是很好办法。

演示虚方法的行为
若方法通过对象调用则没有使用虚方法的特性,要想同时管理Brass和BrassPlus的账户,如果能用同一个数组表示,将很有用,若使用普通的Brass数组则不可实现,那么Brass基类指针数组使得其元素既可以指向Brass对象,又可以指向BrassPlus对象,用一个数组表示多种类型的对象——多态

int main(){
std::cout;
std::cin;
std::endl;
std::string temp;
int tempnum;
double tempbal; 
char kind;
Brass *p_client[10];
for(int i=0;i<10;i++){
   getline(cin,temp);
   cin>>tempnum;
   cin>>tempbal;
   cin>>kind;
   if(kind==1)
        p_clients[i]=new Brass(temp,tempnum,tempbal);
   if(kind==2){
        double loan;
        p_clients[i]=new BrassPlus(temp,tempnum,tempbal,loan);
}
for(int i=0;i<10;i++){
   p_clients[i]->ViewAcct();
   cout<<endl;
}
for(int i=0;i<10;i++){
   delete p_clients[i];
}
cout<<”Done!\n”;
}
  1. 静态联编和动态联编

将源代码中的函数调用解释为执行特定的函数代码块称为函数名联编(binding)
C语言每个函数名对应不同函数,C++由于函数重载,编译器查看参数列表,但这些都在编译过程完成的,被称为静态联编(static binding);虚函数则编译器将不知道使用哪种类型的对象,编译器生成能够在程序运行时选择正确的虚函数方法的代码,称为动态联编(dynamic binding)

为什么默认为静态联编呢
1) 效率来说,动态联编需要跟踪指针或引用指向的对象类型,增加了处理开销。静态联编效率更高
2) 概念模型,静态联编指出不要重新定义该函数,预期被重新定义的方法声明为虚。

  1. 虚函数的工作原理

编译器处理虚函数的方法是:给每个对象添加一个隐藏成员,隐藏成员是一个指向函数地址数组的数组指针,这一数组被称为虚函数表(virtual funcation table,vtbl)。虚函数表中存储了为类对象进行声明的虚函数地址。基类对象包含一个指针,该指针指向基类中所有虚函数的地址表。派生类也包含一个指向独立地址表的指针,若派生类提供了新的虚函数定义,则存储新函数的地址;没有提供,则保存原始版本地址;如果定义了新的虚函数,则将地址添加到表中。调用虚函数时,程序将查看这vtbl
在这里插入图片描述

使用虚函数,在内存和执行速度上有一定成本

  • 每个对象增大,增加存储空间
  • 对每个类,都创建一个虚函数地址表(数组)
  • 每个函数的调用,都需要表中查找地址
  1. 虚方法的注意事项

1)在基类方法声明中使用关键字virtual可使该方法在基类以及所有的派生类中为虚
但应在派生类中使用virtual指明,函数为虚函数

2)构造函数
构造函数不能是虚函数,创建派生类时将调用派生类的构造函数,而不是基类的构造函数,而派生类构造函数将使用基类的构造函数(初始化列表),装入不同意继承机制。

3)析构函数
析构函数应当用作虚函数,除非类不用做基类。
假设Employee是基类,Singer是派生类

Employee *pe=new Singer;
……
delete pe;     //~Employee() or ~Singer()?

若使用默认的静态联编,delete将调用~ Employee()析构函数,这将释放由Singer对象中Employee部分内存,不会释放新的类成员指向的内存。但如果析构函数是虚,则上述代码将想调用~ Singer()释放掉Singer组件的内存,然后调用~Employee()释放Employee部分内存
所以通常给基类提供一个虚析构函数,即使它并不需要。

3) 友元
友元不能为虚,因为友元不是类成员,而只有类成员才能为虚函数。

4) 重新定义将隐藏方法

class Brass
{
public:
   ……
   virtual void AcctView() const;
}
class BrassPlus:public Brass
{
public:
   ……
   virtual void AccView(int a) const;
}

这种重新定义不会生成两个重载版本,而是隐藏了基类版本,这种隐藏使得基类也无法使用基类版本的虚函数。所以如果重新定义继承方法,应确保和原来的原型完全相同,当基类有多个重载虚函数时,继承类需要同时一一定义多个与原型完全相同的继承版本。

class Brass
{
public:
   ……
   virtual void AcctView() const;
   virtual void AccView(int a) const;
virtual void AccView(double a) const;
}
class BrassPlus:public Brass
{
public:
   ……
virtual void AcctView() const;
   virtual void AccView(int a) const;
   virtual void AccView(double a) const;
}

4)返回类型协变(covariance of return type)
基类虚函数返回类型是基类指针或引用,则派生类虚函数可相应改为派生类的引用或指针。且不会导致隐藏。

virtual Brass & Show() const;
virtual BrassPlus & Show() const;
  1. 访问控制:protected

private和protect的区别在派生类中才有体现,派生类成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。因此对于外部世界来说,保护成员的行为与私有成员相似,但对于派生类来说,保护成员的行为与共有成员相似。

可将简单的内联函数作为保护类型,用于返回私有成员,供派生类使用。

protect:
   int Balance() const { return  balance};
   long AcctNum() const { return AcctNum};
  1. 抽象基类

Ellipse和Circle共同点,又有不同,所以从它们中抽象出他们的共性,放在BaceEllipse(ABC)中,然后从ABC中派生出来Ellipse和Circle,这样便可以使用基类指针数组同时管理Ellipse和Circle对象,即可以使用多态方法。
C++通过纯虚函数(pure virtual funcation)提供未实现的函数,纯虚函数声明结尾处为=0.
当类声明中包含一个纯虚数,则该类为ABC,就不能创建该类的对象。而上述Ellipse和Circle称为具体(concrete)类。

ABC理念
将ABC看做是一个必须实施的接口,则ABC要求具体派生类覆盖其纯虚数——迫使派生类遵循ABC设置的接口,使得基类设计人员能够制定“接口约定”。

  1. 类函数小结
    在这里插入图片描述

《C++ primer plus》学习笔记 第十四章 C++代码重用

  1. has-a关系

上一章提到的公有继承(包括ABC)属于is-a(is a kind of)关系,即派生类是是基类的一种(fruit和apple的关系),has-a关系是指派生类中包含有基类。
has-a有两种形式,一种是包含,一种是私有继承

  1. 包含

假设一个Student类中,有成员string类对象name,valarry类对象scores。这便是包含

#ifendif STUDENT_H_
#define STUDENT_H_
#include”iostream”
#include”string”
#include”valarray”
class Student
{
pricate:
   std:string name;
   typedef valarray<double> ArraySC;
   ArraySC scores;
public:
//默认构造函数
   Student():name(“Null Student”),scores() {}
//构造函数
   // valarray<int> v(8)  an array of 8 int elements 
   Student(const std::string &st,int n):name(st),scores(n) {} 
   // valarray<int> v(10,8)  an array of 8 int elements,each set to 10
   Student(const std::string &st,int m,int n):name(st),scores(m,n) {}
   // valarray<int> v(8)  an array of sc
   Student(const std::string &st,const ArraySC & sc):name(st),scores(sc) {}
   // valarray<int> v(pt,4)  an array of 4 int elements,initialized to pt
   Student(const std::string &st,const donble *pt,int n):name(st),scores(pt,n) {}
//显式转换函数
   explicit Student(const std::string & st):name(st),scores() {}
   explicit Student(int n):name(“Null Student”),scores(n) {}
   explicit Student(const ArraySC &sc):name(“Null Student”),scores(sc) {}
//普通成员函数
   double Average() {} const;
   cosnt std::string & Name() const;
   double operator[] (int i);
//友元函数
   friend std::istream & operator>>(std::istream & is,Student &st);
   friend std::isreeam &getline(std::istream & is,Student &st);
   friend std::ostream &operator<<(std::ostream & os,Student &st);
}
#endif

1) explicit
前面提到的可以用只包含一个参数的构造函数用作从参数类型到类类型的转换。使用explicit用于关闭隐式转换。
2) 初始化对象
构造函数的成员初始化列表用来初始化string类和valarry类对象name和scores,对于继承的对象来说,在成员初始化列表中,使用基类的构造函数来初始化。而这里,直接使用成员名当做构造函数进行初始化。这就是继承对象和成员对象的区别,所以实际上,包含并不属于继承层次结构。
3) 初始化顺序
当初始化列表包含多个项目时,这些项目被初始化的顺序为它们被声明的顺序
Student(const char* st,const double *db) : ArraySC(db),name(st) ()
这仍然将先初始化name,这在一个成员的值作为另一个成员初始化表达式中有用。
5)使用被包含对象的接口
被包含对象的接口不是共有的,但可以在类方法中使用它。
在使用公有继承时,类可以继承接口,可能继承实现(虚函数可使派生类不继承基类的实现,ABC中基类的纯虚数只提供接口,但不提供实现),但在包含关系中,类不继承接口。不继承接口也是has-a关系的组成部分。

  1. 私有继承

另一种实现has-a关系的是私有继承。私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。这意味着基类方法不会成为派生类的公有接口的一部分,但可以在派生类的成员函数中使用它们
而公有继承,基类方法将成为派生类的共有方法(接口的一部分),派生类继承基类的接口,这是is-a关系;私有继承,基类的公有方法成为派生类的私有方法,派生类不继承基类接口。

上一点写了Student的包含实现
这里我们用私有继承实现,这里也叫作多重继承(multiple inheritance,MI)

class Student:private std::string,private std::valarray<double>
{
privatetypedef valarray<double> ArraySC;
public:
   Student(): std::string(“Null Student”),ArraySC() {}
   Student(const std::string &st,int n):std::string(st),ArraySC(n) {}
   Student(const std::string &st,const ArraySC & sc):std::string(st),ArrarSC(sc) {}
   Student(const std::string &st,int m,int n):std::string(st),ArraySC(m,n) {}
   explicit Student(const std::string &st):std::string(st),ArraySC() {}
explicit Student(int n):name(“Null Student”),ArraySC(n) {}
   explicit Student(const ArraySC &sc):std::string(“Null Student”),ArraySC(sc) {}
   …… ……
}

1) 构造函数
私有继承中,没有包含中用name和score描述对象,和公有继承一样,使用类名(类的构造函数),而不是成员名进行初始化。
2) 访问基类方法
包含和私有继承将基类方法隐藏规定为私有,因此只能在派生类的方法中使用基类的方法,但有时希望基类方法是公有的

double Student::Average() const
{
   if(score.size()>0)
      return score.sum()/score.size()
   else
      return 0;
}
double Student::Average() const
{
if(ArraySC::size()>0)
      return ArraySC::sum()/ArraySC::size()
   else
      return 0;
}

使用共有方法,返回基类方法
在包含中可以使用基类对象调用基类方法,而私有继承没有对象,只有类,用类调用方法只有作用域解析符了。
3) 访问基类对象
包含中本身就有对象,这很简单。但是私有继承没有对象。

强制类型转换(这是继承层次上独有的,与包含有别)
Student是从string类派生出来的,因此可以通过强制类型转换,将Student对象转换为string对象,结果为继承而来的string类对象,其次,指针this指向调用方法的对象,*this就是调用方法的对象。
const string &Student::Name() const
{
return (const string &) *this;
}
4)访问基类的友元函数
类名显示地限定函数名不适合友元函数,而且友元不属于类,因此不可访问基类方法。但也可以通过强制类型转换的方法调用。
ostream &operator<<(ostream &os,const Student &stu)
{
os<<”scores: ”<<(const string &)stu<<endl;
return & os;
}

  1. 使用包含还是私有继承

包含易于理解,派生类中包含类的显式命名对象,可使用对象名称,而继承使得关系更加抽象。而且多重继承可能引发问题。
继承可提供更多的特性:若基类包含了保护成员(数据成员或者成员函数),这些成员在派生类是可用的,私有继承也属于继承层次结构之内的,而包含则在继承层次之外。其次,强制类型转换也是继承的一大特点。
若需要重新定义虚函数,则也应选择私有继承。

  1. 保护继承
class Student:protect std::string,protect std::valarray<double>
{……}

保护继承是私有继承的变体,只是将基类的保护成员和公有成员成为派生类的保护成员(私有继承是私有成员)。这在派生类再派生出类的时候就有区别了。

  1. 隐式向上转换

公有、私有、保护继承(继承层次结构体系之内)的implicit upcasting一位置无需进行显示类型的转换,就可以使用基类指针或引用指向派生类对象。
在这里插入图片描述

  1. 使用using重新定义访问权限

私有继承(保护继承)中基类方法只能在派生类方法中可用,要想基类方法在派生类外可用,方法之一是定义一个返回基类方法的派生类方法,这在之前提到了。

double Student::sum() const
{
    return std::valarray<double>::sum();
}

这样Student类对象可以通过调用Student::sum()来调用基类valarray::sum()方法。

另一种方法是,使用using将基类方法加入到派生类公有方法

class Student: private std::string,
            private std::valarray<double>::sum()
{
……
public:
  using std:: valarray<double>::sum;
  using std:: valarray<double>::max;
  using std:: valarray<double>::operator[];
}

这样Student类对象可直接使用基类方法了,但需要注意,using声明只是用成员名——没有圆括号、函数特征标、返回类型。

  1. 多重继承

MI描述的是有多个直接基类的类,私有MI和保护MI可以表示has-a关系,公有MI可以表示is-a关系。

MI会带来两个问题:一是从两个不同的基类继承同名方法;一个是从两个或多个相关基类那里继承同一个类的实例。

假设定义了抽象基类Worker并使它派生出来Waiter类和Singer类,然后使用MI从Waiter和Singer中派生出来SingingWinter。

在这里插入图片描述

#ifndef WORKER_H_
#define WORKER_H_
#include<string>
using namespace std;
class Worker
{
private:
   string name;
   long id;
public:
   Worker():name(“Not Name”),id(0L) {}
   Worker(const string &st,long n):name(st),id(n) {}
   virtual ~Worker();
   virtual void Set();
   virtual void Show() const;
}
class Waiter:public Worker
{
   private:
     int panache;
   public:
     Waiter():Worker(),panache(0) {}
     Waiter(const string &st,long n,int p=0):Worker(st,n),panache(p) {}
     Waiter(const Worker &t,int p=0):Worker(t),panache(p) {}
     void Set();
     void Show() const;
}

class Singer:public Worker
{
   private:
     int voice;
   public:
     Singer():Worker(),voice() {}
     Singer():Worker(),voice(0) {}
     Singer(const string &st,long n,int p=0):Worker(st,n),voice(p) {}
     Singer(const Worker &t,int p=0):Worker(t),voice(p) {}
     void Set();
     void Show() const;
}

这里可以使用指向基类的指针(或引用),调用两个派生类的方法
接下来声明SingingWaiter

class SingingWaiter:public Singer,public Waiter
{…… ……}

因为Singer和Waiter都继承了一个Worker组件,因此SingingWaiter将包含两个Worker组件,这将引起问题,例如将派生类对象的地址赋给基类指针,

SingingWaiter t;
Worker *p=&t;

这种赋值将基类指针设置为派生对象的基类地址,但t中包含两个Worker对象,有两个地址
这里可以使用类型转换

Worker *p=(Singer *&t;
Worker *p=(Waiter *&t;

但这样将有两个Worker对象的拷贝由此引入虚基类

1) 虚基类
虚基类使得从多个类(它们基类相同)派生出的对象只继承一个基类对象。例如,可以通过在类声明中会用关键字virtual,使Worker被用作Singer和Waiter的虚基类(virtual和public的位置可变)。

class Waiter:virtual public Worker {……}
class Singer:public virthal Worker {……}
class SingingWaiter:public Singer,public Waiter
{…… ……}

这样SingingWaiter中只包含一个Worker对象
在这里插入图片描述

3)使用虚基类应该修改构造函数
常规的非虚类的继承

class A
{
   int a;
public:
   A(int n=0):a=n {}
}
class B:public A
{  
   int b;
public:
   B(int m=0,n=0):A(m),b(n) {}
}
class C:public B
{
   int c;
public:
   C(int l=0,int m=0,int n=0):B(l,m),c(n) {}
}

C类调用B类的构造函数,B类调用A类的构造函数。

但虚基类则不能采取这样的传递

SingingWaiter(const &Worker wk,int m=0,int n):Waiter(wk,m),Singer(wk,n) {}

这样是不允许的,C++的虚基类,禁止信息通过中间类自动传递给基类对象。

SingingWaiter(const &Worker wk,int m=0,int n):Worker(wk),Waiter(wk,m),Singer(wk,n) {}

这也就是说必须显式地调用虚基类的构造函数,在虚基类中这是合法的,但在非虚基类中这是非法的。

2) 哪个方法
Waiter和Singer中都继承了Worker中的方法Show()和Set(),如果在SingingWaiter类中没有重新定义这两种方法(重新定义将覆盖原有方法),那使用SingingWaiter对象时产生二义性。

SingingWaiter wk(“Jerry”,2005,6);
wk.Show();    //ambiguous

一种方法是调用作用域解析符

wk.Singer()::Show();

还有一种是在SingingWaiter中重新定义Show,返回一个基类方法

void SingingWaiter::Show()
{
    Singer::Show();
}
void Worker::Show() const
{
     cout<<”name”<<name;
}
void Singer:;Show() const
{
   Worker::Show();
   cout<<”panache rating:<<panache;
}
void Waiter::Show() const
{
Worker::Show();
   cout<<”voice:<<voice;
}

使用这样递增地方式,对于虚基类来说无效,系统检测它忽视了Waiter组件,除非改成

void SingingWaiter::Show()
{
Singer::Show();
Waiter::Show();
}

因为Singer::Show()和Waiter::Show()都调用了Worker::Show(),这将显示两次姓名

模块化方式,而不是递增方式,即提供一个只显示Worker,然后在Singing Waiter:;Show(0中将他们组合起来。

void Worker::Data() const
{
     cout<<”name”<<name;
}
void Singer:;Data() const
{
   cout<<”panache rating:<<panache;
}
void Waiter::Data() const
{
   cout<<”voice:<<voice;
}
void SingingWaiter::Show()
{
    Worker::Data();
Singer::Data();
Waiter::Data();
}

Data()方法只在类内部可用,作为协助公有接口的辅助模块,设置为保护访问,则它只在继承层次结构中可用,在其他地方不可用。

  1. 类模板
一个简陋的Stack模板
#ifndef STACK_H_
#defile STACK_H_
template <class Type>
class Stack
{
private:
   enum{max=10};
Type item[max];
   int top;
public:
   Stack();
   bool isempty();
   bool isfull();
   bool push(const Type &item);
   bool pop(Type &item);
}
template<class Type>
Stack<Type>::Stack()
{
    top=0;
}
template<class Type>
bool Stack<Type>::isempty()
{
    return top==0
}
template<class Type>
bool Stack<Type>::push(const Type & item)
{
if(top<MAX)
{
    items[top++]=item;
    return true;
}   
return false;
}
bool Stack<Type>::pop(const Type & item)
{
if(top<MAX)
{
    item;=items[top--];
    return true;
}   
return false;
}
#endif

数组模板

template<class T,int n>
class Array
{
private:
   T a[n];
public:
   Array(const T &t);
}
Array<T,n>::Array(const T &t)
{
   for(int i=0;i<n;i++)
     a[i]=t;
}

template<class T,int n>

  • class指出T为类型参数,这种参数称为非类型参数。
  • 模板代码不能修改参数的值
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值