Chapter13 类继承

  • 类继承:一种很好的来扩展和修改类的方法。从已有的类派生出新的类,而派生类继承了原有类(基类)的特征

基类

  • 基类:TableTennisPlayer;派生类:RatedPlayer

  • class RatedPlayer: public TableTennisPlayer表示TableTennisPlayer是一个共有基类,这为公有派生

  • 派生类的继承特性:
    • 派生类需要自己的构造函数
    • 派生类增加新的数据成员和成员函数
  • 派生类不能直接访问基类的私有成员,必须通过基类的成员函数访问,即派生类的构造函数必须使用基类构造函数。即创建派生类对象时,首先创建基类对象,

  • 派生类构造函数:
    • 首先创建基类对象
    • 派生类构造函数通过初始化列表将基类信息传递给基类构造函数
    • 派生类构造函数初始化派生类新增的数据成员
  • 释放对象的顺序与创建对象的顺序相反,先执行派生类的析构函数,再自动调用基类析构函数

  • 派生类与基类的特殊关系:
    • 派生类对象可以使用基类的非私有函数
    • 基类指针可在不进行显式类型转换的情况下,指向派生类对象(基类指向派生类)。TableTennisPlayer & rt=rplayer; rt.Name();(Name()为基类函数)
    • 基类引用可在不进行显式类型转换的情况下,引用派生类对象(基类指向派生类)。TableTennisPlayer *pt=& rplayer; pt->Name();
    • 基类指针或引用只能用于调用基类方法,不能使用派生类函数(单向操作,不可将基类对象和地址赋给派生类引用和指针)

继承

  • 派生类对象也是一个基类对象;但是基类对象不是派生类对象

  • is-a关系:is-a-kind-of(是其中的一种);水果是基类,香蕉是派生类,则香蕉是水果,水果不是香蕉,即派生类属于基类,但是基类不是派生类

多态公有继承

  • 多态:同一个函数在派生类和基类的实现是不同的

  • 实现多态公有继承
    • 在派生类中重新定义基类的方法
    • 使用虚函数
  • 函数在基类中被声明为虚函数后,在派生类中,将自动称为虚函数;在派生类中使用virtual指明虚函数也可

  • 如果函数是通过引用或指针调用,而非实际对象调用,则可确定是使用基类或派生类;dom是基类对象,dot是派生类对象:
    • 如果没有使用关键字virtual,根据引用类型或指针类型选择是基类函数或派生类函数。

        Brass & b1=dom; 
        Brass & b2=dot; 
        b1.View();    //使用Brass::View();
        b2.View();    //使用Brass::View();
      
    • 如果使用 virtual View(),根据引用或指针所指向的对象类型(基类或派生类)选择函数

        Brass & b1=dom; 
        Brass & b2=dot; 
        b1.View();    //使用Brass::View();
        b2.View();    //使用BrassPlus::View();       
      

-** 如果在派生类中重新定义基类函数,将基类函数声明为虚函数。则程序将根据对象类型(基类或派生类),而不是引用或指针的类型(基类或派生类)选择哪个类别的函数。为基类声明一个虚析构函数也是惯例。**

  • 派生类不能直接访问基类的私有数据,必须使用基类的公有函数才能访问。可用析构,或其他函数。

  • 非构造函数不能使用成员初始化列表,但是派生类函数可以调用公有的基类函数:

      void BrassPlus::View() const
      {   Brass::View();
          cout << max << own;}
    
  • 派生类中,使用多态的虚函数,必须使用作用域解析运算符::来调用基类函数;如果派生类中没有重新定义函数,则不必对该函数使用作用域解析运算符

  • 创建指向Brass的指针数组 ,则每个元素类型相同,由于公有继承,Brass指针可指向Brass对象或BrassPlus对象: Brass * p_client[CLIENTS];

  • 虚析构函数:保证指针能够调用基类与继承类的析构函数,否则智能调用对应于指针类型的析构函数(可能只调用基类的析构函数)

静态联编和动态联编

  • 派生类引用或指针转换为基类引用或指针被称为向上强制转换,使公有继承不需要进行显式类型转换

  • 基类指针或引用转换为派生类指针或引用,称为向下强制转换

  • 编译器对非虚函数使用静态联编,编译器对虚函数使用动态联编;动态联编可以使基类指针或引用指向派生类对象

  • 如果要在派生类中重新定义基类的函数,则将其设置为虚函数。

  • 如多定义的类做用作基类,则在派生类中重新定义的类函数。

  • 基类函数的声明中使用virtual,使函数在基类以及所有派生类(派生类的派生类)中是虚的

  • 构造函数不能是虚函数

  • 析构函数应当是虚函数

  • 友元不能是虚函数,友元不是类成员,只有成员才能是虚函数

  • 派生类重新定义了基类函数,非重载,是隐藏了基类的同名函数

  • 如果返回类型是基类或指针,应该保证新类型相同。但是如果返回类型是基类引用或指针,可以改为指向派生类和指针。范湖类型协变

访问控制:protected

  • pretected:

    • 与private相同:在类外,只能用公有函数访问protected类成员
    • 与private相反:派生类的成员可以直接访问基类的保护成员(protected),但不能访问基类的私有函数(private)
  • 最好对类数据成员采用私有访问控制,不要使用保护访问控制。通过基类函数,让派生类访问基类数据

抽象基类

  • 抽象基类(Abstract Base Class):从Ellipse和Circle中抽象出二者共性,将这些特性放到BaseEllipse类中,即可从BaseEllipse类中派生出Circle和Ellipse类;还可用基类指针数组(BaseEllipse)共同管理Circle和Ellipse对象

  • 当类声明中包含纯虚函数时,则不能创建该类的对象,包含纯虚函数的类只用作基类;纯虚函数使其称为

  • 原想中的=0使虚函数成为纯虚函数,virtual double Area() const=0; C++允许纯虚函数有定义

  • 在原型中使用=0指出类是一个抽象基类,在类中可以不定义该函数

  • 从基类BaseEllipse类中派生出Ellipse类和Circle类,可创建Ellipse对象和Circle对象,不能创建BaseEllipse对象,即抽象基类对象;Circle和Ellipse类称为具体类,表示可以创建类型的对象

  • ABC:只定义接口,而不涉及实现;在声明的分好前面加=0来声明纯虚方法;对于包含纯虚的类,不能用其创建对象。纯虚函数用来定义派生类的通用接口

类设计回顾

  • 编译器生成的成员函数:
    • 默认构造函数:要么都没有参数,要么所有参数都有默认值;如果没有定义任何构造函数,编译器将定义默认构造函数;如果定义了某种构造函数,编译器将不会定义默认构造函数。派生类构造函数的成员初始化列表中,没有显式调用基类构造函数,则编译器将使用基类的默认构造函数来构造派生类对象的基类部分。类包含指针成员,则必须初始化指针成员;最好提供一个显式默认构造函数,将所有的类数据成员都初始化为合理的值
    • 复制构造函数:接收其所属类的对象作为参数,Star(const Star &); 如果没有提供复制构造函数,程序将定义一个执行成员初始化的复制构造函数。对于new初始化的指针,或需要修改的静态变量,需要定义自己的复制构造函数。下列情况使用复制构造函数:
      • 将新对象初始化为一个同类对象
      • 按值将对象传递给函数
      • 函数按值返回对象
      • 编译器生成临时对象
    • 赋值运算符:处理同类对象之间的赋值。创建新的对象,使用初始化;语句修改已有对象的值,则是赋值。

        Star sirius;
        Star alpha=sirius;  //initialization
        Star dogstar;
        dogstar=sirius;		  //assignment
      
  • 其他类方法:
    • 构造函数:其创建新的对象,而其他方法只是被现有对象调用
    • 析构函数:定义显式析构函数释放构造函数用new分配的所有内存;对于基类,及时不需要析构函数,也应提供一个虚析构函数
    • 转换:使用一个参数调用构造函数定义从参数类型到类的转换;

        Star(const char*)	//convert char* to char   或者要将类对象转换为其他类型,可以是没有参数的类成员函数,也可以是返回类型被声明为目标类型的类成员函数。**explicit允许使用强制类型转换进行显式转换,不允许阴式转换**
      			
        Star::Star double(){...} // convert star to double
      
    • 按值传递对象与传递引用:(在函数中,使用参数为对象,还是对象的引用)通常,编写使用对象作为参数的函数时,应按引用而不是按值类传递对象。按值传递对象涉及生成临时拷贝,即调用复制构造函数,然后调用析构函数,造成开销。在继承使用虚函数时,被定义为接受基类引用参数的函数可以接受派生类。
    • 返回对象和返回引用:(成员函数的返回值类型)直接返回对象与返回引用之间的唯一去边在于函数原型和函数头:

        Star nova1(const Star &);			Star & nova2(const Star &);
      

      返回对象涉及生成返回对象的临时副本,返回引用节省时间和内存。如果函数返回在函数中创建的临时对象,则不要使用引用

    • 使用const:保证成员函数不修改成员数据,即其调用对象,将const放在函数右侧;(只能保证成员函数不修改对象,不能保证成员函数返回对象不被修改)如果函数将参数声明为指向const的引用或指针,则不能将该参数传递给另一个函数,除非后者也使用const限制参数
  • 公有继承的考虑因素:
    • is-a关系:无需进行显式类型转换,基类指针就可指向派生类对象,基类引用可以引用派生类对象。
    • 什么不能被继承:
      • 构造函数是不能继承的,即创建派生类对象时,必须调用派生类的构造函数。派生类通常使用初始化列表调用基类构造函数,创建派生类的基类部分;
      • 析构函数也不能继承,释放对象时,首先调用派生类的析构函数,再调用基类的析构函数
      • 赋值运算符
    • 赋值运算符:编译器使用基类赋值运算符处理派生类对象中基类部分的复制。
      • 如果基类构造函数使用new初始化指针,需要提供显式复制运算符。
      • 如果派生类使用了new,必须提供显式赋值运算符,给类的每个成员提供赋值运算符,不仅仅是新成员。
      • 派生类对象可赋给基类对象,基类对象赋给派生类对象看是否有函数支持
    • 私有成员与保护成员:派生类可以直接访问基类的保护成员,但只能通过基类的成员函数来访问私有成员。
    • 虚方法:如果希望派生类能够定义方法,则应在基类中将方法定义为虚的,启用动态联编;不希望重新定义方法,不必将其声明为虚的
    • 析构函数:基类析构函数应该是虚的,当通过指向对象的基类指针或引用来删除派生对象时,首先调用派生类的析构函数,再调用基类的析构函数
    • 友元函数:友元函数并非类成员,不能继承,如果希望派生类的友元函数能够使用基类的友元函数,可通过强制类型转换符,将派生类引用或指针转换为基类引用或指针,然后使用转换后的指针或引用,来调用基类的友元函数
    • 有关基类方法说明 - 如果派生类没有重新定义新的函数,派生类对象自动使用继承的基类方法 - 派生类构造函数自动调用基类构造函数 - 派生类没在成员初始化列表中指定其他构造函数,则自动调用基类的默认构造函数 - 派生类构造函数显式调用成员初始化列表中指定基类构造函数 - 派生类函数可使用作用域解析运算符来调用公有的和受保护的基类函数 - 派生类的友元函数,可通过强制类型转换,将派生类引用或指针转换为基类引用或指针,使用该引用或指针调用基类的友元函数