天天看点

读书笔记|| 类继承

一、一个简单的基类

面向对象编程的主要目的之一是提供可重用的代码。传统的C函数库通过预定义、预编译的函数提供了可重用性。C++类提供了更高层次的重用性,类库由类声明和实现构成。因为类组合了数据表示和类方法,因此提供了比函数库更加完整的程序包。类继承,C++提供了比修改代码更好的方法来扩展和修改类,能够从已有的类派生出新的类,而派生类继承了原有类的特征,包括方法。通过继承派生出的类通常比设计新类要容易得多,下面是可以通过继承完成的一些工作:

  • 可以在已有类的基础上添加功能;
  • 可以给类添加数据;
  • 可以修改类方法的行为。

    继承机制只需要提供新特性,甚至不需要访问源代码就可以派生出类。

    从一个类派生出另一个类时,原始类称为基类,继承类称为派生类,为了说明继承,首先需要一个基类。

#ifndef TABTENN0_H_
#define TABTENN0_H_
#include <string>
using std::string;
// simple base class
class TableTennisPlayer
{
private:
    string firstname;
    string lastname;
    bool hasTable;
public:
    TableTennisPlayer (const string & fn = "none",
                       const string & ln = "none", bool ht = false);
    void Name() const;
    bool HasTable() const { return hasTable; };
    void ResetTable(bool v) { hasTable = v; };
};
#endif
           
#include "tabtenn0.h"
#include <iostream>

TableTennisPlayer::TableTennisPlayer (const string & fn, 
    const string & ln, bool ht) : firstname(fn),
	    lastname(ln), hasTable(ht) {}
    
void TableTennisPlayer::Name() const
{
    std::cout << lastname << ", " << firstname;
}
           
#include <iostream>
#include "tabtenn0.h"
int main ( void )
{
    using std::cout;
    TableTennisPlayer player1("Chuck", "Blizzard", true);
    TableTennisPlayer player2("Tara", "Boomdea", false);
    player1.Name();
    if (player1.HasTable())
        cout << ": has a table.\n";
    else
        cout << ": hasn't a table.\n";
    player2.Name();
    if (player2.HasTable())
        cout << ": has a table";
    else
        cout << ": hasn't a table.\n";
    // std::cin.get();
    return 0;
}
           
读书笔记|| 类继承

1.派生一个类

  • 派生类需要自己的构造函数
  • 派生类可以根据需要添加额外的数据成员和成员函数。

    构造函数必须给新成员和继承的成员提供数据。

    2.构造函数:访问权限的考虑

    派生类不能直接访问基类的私有成员,而必须通过基类的方法进行访问。派生类构造函数必须使用基类的构造函数。创建派生类对象时,程序首先创建基类对象。有关派生类构造函数的要点如下:

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

    3.使用派生类

    要使用派生类,程序必须要能够访问基类声明。

#ifndef TABTENN1_H_
#define TABTENN1_H_
#include <string>
using std::string;
// simple base class
class TableTennisPlayer
{
private:
    string firstname;
    string lastname;
    bool hasTable;
public:
    TableTennisPlayer (const string & fn = "none",
                       const string & ln = "none", bool ht = false);
    void Name() const;
    bool HasTable() const { return hasTable; };
    void ResetTable(bool v) { hasTable = v; };
};
// simple derived class
class RatedPlayer : public TableTennisPlayer
{
private:
    unsigned int rating;
public:
    RatedPlayer (unsigned int r = 0, const string & fn = "none",
                 const string & ln = "none", bool ht = false);
    RatedPlayer(unsigned int r, const TableTennisPlayer & tp);
    unsigned int Rating() const { return rating; }
    void ResetRating (unsigned int r) {rating = r;}
};
#endif
           
#include "tabtenn1.h"
#include <iostream>
TableTennisPlayer::TableTennisPlayer (const string & fn, 
    const string & ln, bool ht) : firstname(fn),
	    lastname(ln), hasTable(ht) {}
    
void TableTennisPlayer::Name() const
{
    std::cout << lastname << ", " << firstname;
}
// RatedPlayer methods
RatedPlayer::RatedPlayer(unsigned int r, const string & fn,
     const string & ln, bool ht) : TableTennisPlayer(fn, ln, ht)
{
    rating = r;
}
RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer & tp)
    : TableTennisPlayer(tp), rating(r)
{
}
           
#include <iostream>
#include "tabtenn1.h"
int main ( void )
{
    using std::cout;
    using std::endl;
    TableTennisPlayer player1("Tara", "Boomdea", false);
    RatedPlayer rplayer1(1140, "Mallory", "Duck", true);
    rplayer1.Name();          // derived object uses base method
    if (rplayer1.HasTable())
        cout << ": has a table.\n";
    else
        cout << ": hasn't a table.\n";
    player1.Name();           // base object uses base method
    if (player1.HasTable())
        cout << ": has a table";
    else
        cout << ": hasn't a table.\n";
    cout << "Name: ";
    rplayer1.Name();
    cout << "; Rating: " << rplayer1.Rating() << endl;
// initialize RatedPlayer using TableTennisPlayer object
    RatedPlayer rplayer2(1212, player1);
    cout << "Name: ";
    rplayer2.Name();
    cout << "; Rating: " << rplayer2.Rating() << endl;
    // std::cin.get();
    return 0;
}

           
读书笔记|| 类继承

4.派生类和基类之间的特殊关系

派生类对象可以使用基类的方法,条件是方法不是私有的;另外两个重要的关系是:基类指针可以在不进行显示类型转换的情况下指向派生类对象;基类引用可以在不进行显示类型转换的情况下引用派生类的对象。

二、继承:is-a关系

派生类和基类之间的特殊关系是基于C++继承的底层模型的。实际上,C++有3种继承方式:公有继承、保护继承和私有继承。公有继承是最常用的方式,它建立一种is-a关系,即派生类对象也是一个基类对象,可以对基类对象执行的任何操作,也可以对派生类对象执行。新类将继承原始类的所有数据成员,派生类可以添加特性。这种关系的通常使用术语is-a。

公有继承不建立has-a关系,举例:午餐可能包括水果,但通常午餐并不是水果,在午餐中加入水果的正确方法是将其做为一种has-a关系:午餐有水果。

公有继承不能建立is-like-a关系,也就说,他不采用明喻。继承可以在基类的基础上添加属性,但不能删除基类的属性。在这些情况下,可以设计一个包含公有特性的类,然后以is-a或has-a关系,在这个类的基础上定义相关的类。

公有继承不建立is-implemented-as-a(作为……来实现),例如,可以使用数组来实现栈,但从Array类派生出Stack类是不合适的,因为栈不是数组。正确的做法是让栈包含一个私有的Array对象成员来隐藏数组实现。

公有继承不建立uses-a关系。例如:计算机可以使用激光打印机,但从Computer类派生出Printer类(或者反过来是没有意义的),然而可以使用友元函数或类来处理Printer对象和Computer对象之间的通信,

三、多态公有继承

派生类对象使用基类的方法而未做任何修改。有两种机制可用于实现多态公有继承:

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

    1.类实现

    关键字virtual只用于类声明的方法原型中。派生类并不能直接访问基类的私有数据,而必须使用基类的公有方法才能访问这些数据。访问的方式取决于方法,构造函数使用一种技术,而其他成员函数使用另一种技术,

    2.演示虚方法的行为

    方法是通过对象(而不是指针或引用)调用的,没有使用虚方法特性。

    3.为何需要虚析构函数

    如果析构函数不是虚的,则将只调用对应于指针类型的析构函数。

四、静态联编和动态联编

程序调用函数时,将使用哪个可执行代码块?编译器负责回答这个问题。将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编。在C语言中,这非常简单,因为每个函数名都将对应一个不同的函数。在C++中,由于重载函数的缘故,这项任务更复杂。编译器必须查看函数参数以及函数名才能确定使用哪个函数。然而,C/C++编译器可以在编译过程完成这种联编。在编译过程中进行来联编被称为静态联编,又称为早期联编。然而,虚函数使这项工作变得更困难。使用哪一个函数是不能在编译时确定的,因为编译器不知道用户将选择哪种类型的对象,所以,编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编,又称为晚期联编。

1.指针和引用类型的兼容性

在C++中,动态联编与通过指针和引用调用方法相关,从某种程度上来说,这是由继承控制的。公有继承建立is-a关系的一种方法是如何处理指向对象的指针和引用。通常C++不允许将一种类型的地址赋给另一种类型的指针,也不允许一种类型的引用指向另一种类型:

double x = 2.5;
int * pi = &x ; // invalid assignment 
long & rl = x ; 
           

指向基类的引用或指针可以引用派生类对象,而不必进行显示类型转换。

BrassPlus dilly ("Annie Dill" , 493222,2000);
Brass * pb = &dilly ; // ok
Brass & rl = dilly ; //ok
           

将派生类引用或指针转换为基类引用或指针被称为向上强制转换,这使公有继承不需要及逆行显示类型转换,该规则使is-a关系的一部分。

相反的过程——将基类指针或引用转换为派生类的指针或引用——称为向下强制转换,如果不使用显示类型转换,则向下强制转换是不被允许的,原因是is-a关系通常是不可逆的。

2.虚成员函数和动态联编

如果动态联编让您能够重新定义类方法,而静态联编在这方面很差,但是依旧要默认静态联编。原因有两个——效率和概念模型

为使程序能够在运行阶段进行决策,必须采取一些方法来跟踪基类指针或引用指向的对象类型。同样,如果派生类不重新定义基类的任何方法,也不需要使用动态联编。在这些情况下,使用静态联编更合理,效率也更高。

在设计类时,可能包含一些不在派生类重新定义的成员函数。不该将函数设置为虚函数,有两方面的好处:首先效率更高,其次,指出不要重新定义该函数,这表明,仅将那些预期将被重新定义的方法声明为虚的。

虚函数的工作原理:C++规定了虚函数的行为,但将实现方法留给了编译器作者,不需要知道实现方法就可以使用虚函数,但了解虚函数的工作原理有利于更好地理解概念。通常,编译器处理虚函数地方法是:给每一个对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,这种数组称为虚函数表。虚函数表中存储了为类对象进行声明的虚函数地址。使用虚函数时,在内存和执行速度方面有一定的成本,包括:

  • 每个对象都将增大,增大量为存储地址的空间
  • 对于每个类,编译器都创建一个虚函数的地址表
  • 对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址。

五、访问控制:protected

关键字protected与private相似,在类外只能用共有类成员来访问protected部分中的类成员。private与protected之间的区别只有在基类派生的类中才会表现出来,派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。因此,对于外部世界来说,保护成员的行为与私有成员相似;但对于派生类来说,保护成员的行为与公有成员相似。对于成员函数来说,保护访问控制很有用,它让派生类能够访问公众不能使用的内部函数。

六、抽象基类

圆是椭圆的一种特殊情况——长轴和短轴等长的椭圆。因此,所有的圆都是椭圆,可以从Ellipse类派生出Circle类。

考虑Ellipse类包含的内容,数据成员可以包括椭圆中心的坐标、半长轴、短半轴以及方向角。

另外,还可以包括一些移动椭圆、返回椭圆面积、旋转椭圆以及缩放长半轴和短半轴的方法:

class Ellipse
{
private:
    double x ;
    double y ;
    double a ;
    double b;
    double angle ;
    ……
public :
    ……
    void Move(int nx , ny ){x = nx;y = ny ;}
    virtual double Area() const {return 3.14159 * a * b ;}
    virtual void Rotate(double nang){angle += nang;}
    virtual void Scale(double sa , double sb)
           

现在从Ellipse类派生出一个Circle类:

class Circle : public Ellipse
{
    ……
};
           

虽然圆是椭圆的一种,但是这种派生是笨拙的。圆只需要一个半径就能描述大小和形状,并不需要那么多的量。所以,总的来说,不使用继承而直接定义Circle类更简单:

class Circle 
{
private :
    double x ;
    double y ;
    double r ;
    ……
public:
    ……
    void Move(int nx , ny){ x = nx; y = ny;}
    double Area() const{return 3.14159 * r * r;}
    void Scale {double sr } {r *= sr ;}
    ……
};
           

还有另外一种解决方法,就是将Ellipse和Circle类中抽象出它们的共性,将这些特性放到一个ABC中,然后从该ABC派生出Circle和Ellipse类。这样,便可以使用基类指针数组同时管理Circle和Ellipse对象,即可以使用多态方法。

当类声明中包含虚函数是,则不能创建该类的对象。包含纯虚函数的类只用作基类,要成为真正的ABC,必须至少包含一个纯虚函数,原型中的=0使虚函数成为纯虚函数。在原型中的=0指出类使一个抽象基类,在类中可以不定义该函数。

七、继承和动态内存分配

1.派生类不使用new

假设基类使用了动态内存分配:

class baseDMA
{
private :
    char * label;
    int rating'
public :
    baseDMA (const char * 1 = 'null', int r = 0);
    baseDMA (const baseDMA & rs);
    virture -bassDMA();
    bassDMA & operator =(const baseDMA & rs);
    ……
 }; 
           

声明中包含了构造函数使用new时需要的特殊方法;析构函数、复制构造函数和重载赋值运算符。

2.派生类使用new

class hasDMA : public baseDMA
{
private :
    char * style ;
public :
……
};
           

在这种情况下,必须为派生类定义显式析构函数、复制构造函数和赋值运算符。

派生类析构函数将自动调用基类的析构函数,故其自身的职责是对派生类构造函数执行工作的进行清理。