C++继承知识点总结

1、派生类对象包含基类对象,使用公有派生,基类的公有成员将成为派生类的公有成员,基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问。


2、派生类对象存储了基类的数据成员,派生类对象可以使用基类的方法。


3、继承示例:

//基类
//tableTennis.h
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;
    {
        std::cout<

4、派生类对象构造和析构流程

创建派生类对象,必须首先创建基类对象;如果不调用基类构造函数,程序将使用默认的基类构造函数,因此如下代码等效:

RatedPlayer(unsigned int r = 0, 
            const string & fn = "none",
            const string & ln = "none",
            bool ht = false) 
{
    rating = r;
}

RatedPlayer(unsigned int r = 0, 
            const TableTennisPlayer & tp) 
    : TableTennisPlayer(tp)
{
    rating = r;
}
  • 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数
  • 派生类构造函数应初始化派生类新增的数据成员
  • 释放对象的顺序与创建对象的顺序相反,即首先执行派生类的析构函数,然后自动调动基类的析构函数

注意:创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。基类构造函数负责初始化继承的数据成员 ;派生类构造函数主要用于初始化新增的数据成员。派生类的构造函数总是调用一个基类构造函数。可以使用初始化器列表语法指明要使用的基类构造函数,否则将使用默认的基类构造函数。派生类对象过期时,程序将首先调用派生类析构函数,然后再调用基类析构函数。


5、派生类和基类之间的特殊关系

  • 派生类对象可以使用基类的方法,条件是方法不是私有的

  • 基类指针可以不进行显示类型转换的情况下指向派生类对象,基类引用可以在不进行显示类型转换的情况下引用派生类对象

    RatedPlayer rplayer(1140,"Mallory",true);
    TableTennisPlayer & rt = rplayer;
    TableTennisPlayer * pt = &rplayer;
    //Name() belongs to TableTennisPlayer class
    rt.Name(); 
    pt->Name();
    
  • 基类指针或引用只能调用基类方法,不能调用派生类的方法

  • 对于形参是基类引用或指针的方法,因为基类引用或指针既可以指向基类对象,又可以指向派生类对象,所以传入的实参可以基类对象,也可以是派生类对象


6、多态公有继承

多态:同一个方法在派生类和基类中的行为是不同的,方法的行为应取决于调用该方法的对象,即同一个方法的行为随上下文而异

实现方式:

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

程序示例:

class Brass
{
private:
    std::string fullName;
    long acctNum;
    double balance;
public:
    Brass(const std::string & s = "Nullbody",
          long an = -1,
          double ba1 = 0.0);
    virtual ~Brass();
    void Deposit(double amt);
    double Balance() const;
    //==================================
    virtual void Withdraw(double amt);
    virtual void ViewAcct() const;   
    //==================================
}

class BrassPlus : public Brass
{
private:
    double maxLoan;
    double rate;
    double oweBank;
public:
    BrassPlus(const std::string & s = "Nullbody",
              long bal = 0.0, double ml = 500,
              double r = 0.11125);
    BrassPlus(const Brass & ba, double ml = 500,
              double r = 0.11125);
    //==================================
    virtual void Withdraw(double amt);
    virtual void ViewAcct() const;
    //==================================
    void ResetMax(double m)
    {
        maxLoan = m;
    }
    void ResetRate(double r)
    {
        rate = r;
    }
    void ResetOwes()
    {
        owesBank = 0;
    }
}
  • Brass和类BrassPlus都定义了Withdraw()ViewAcct()方法,程序将根据对象类型来确定使用哪个版本:

    Brass dom("Dominic Banker",11224,4183.25);
    BrassPlus dot("Dominic Banker",12118,2590.25);
    dom.ViewAcct(); //invoke Brass::ViewAcct()
    dot.ViewAcct(); //invoke BrassPlus::ViewAcct()
    
  • 如果方法是通过引用或指针而不是对象调用的,它将确定使用哪一种方法。如果没有使用关键字virtual,程序将根据引用类型或指针类型选择方法;如果使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法。

  • 如果ViewAcct()不是虚的,则程序的行为如下:

    Brass dom("Dominic Banker",11224,4183.25);
    BrassPlus dot("Dominic Banker",12118,2590.25);
    Brass & b1_ref = dom;
    Brass & b2_red = dot;
    b1_ref.ViewAcct(); //invoke Brass::ViewAcct()
    b2_red.ViewAcct(); //invoke Brass::ViewAcct()
    
  • 如果ViewAcct()是虚的,则程序的行为如下:

    Brass dom("Dominic Banker",11224,4183.25);
    BrassPlus dot("Dominic Banker",12118,2590.25);
    Brass & b1_ref = dom;
    Brass & b2_red = dot;
    //invoke Brass::ViewAcct()
    b1_ref.ViewAcct(); 
    //invoke BrassPlus::ViewAcct()
    b2_red.ViewAcct(); 
    
  • 方法在基类中被声明为虚的后,它在派生类中将自动成为虚方法,当然也可以在派生类中使用关键字virtual来指出哪些函数是虚函数

  • 基类声明了一个虚析构函数,这样做是为了确保释放派生对象时,按正确的顺序调用析构函数


7、静态联编和动态联编

程序调用函数时,将使用哪个可执行代码块,编译器负责回答这个问题。将源代码中的函数调用解释为执行特定的函数代码被称为函数名联编。

在编译过程中进行联编被称为静态联编;由于虚函数的引入,使用哪一个函数是不能在编译时确定的,因为编译器不知道用户将选择哪种类型的对象,所以编译器必须生成能够在程序运行时选择正确的虚方法的代码,这样称为动态联编。

注意

  • 由于动态联编使得程序能够在运行阶段进行决策,所以必须采取一些方法来跟踪基类指针或引用指向的对象类型,这增加了额外的处理开销,所以效率会比静态联编要低

  • 如果要在派生类中重新定义基类的方法,则将它设置为虚方法;否则。设置为非虚方法。


8、虚函数的工作原理

通常,编译器处理虚函数的方法是:给每个对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组(虚函数表)的指针,虚函数表中存储了为类对象进行声明的虚函数的地址,例如:

class Scientist {
    ...
    char name[40];
public: 
    virtual void show_name();
    virtual void show_all();
};

class Physicist : public Scientist
{
  ...
  char field[40];
public:
    void show_all();  //重写父类方法
    virtual void show_field();
    ...
};

Scientist有一个隐藏的数据成员,里面存储了一个指向一张虚函数表的指针,该虚函数表记录了类Scientist定义的虚函数的地址;同理类Physicist也同样有一个隐藏的数据成员,里面存储了一个指向一张虚函数表的指针,该虚函数表记录了类Physicist继承自Scientist的虚函数的地址和自己新定义虚函数的地址。

调用虚函数时,程序将查看存储在对象中的虚函数表地址,然后通过查询虚函数表转向相应的虚函数,所以虚函数相比于非虚函数存在一些缺点(凡是都具有两面性):

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

虽然非虚函数的效率比虚函数稍高,但不具备动态联编功能。

虚函数注意事项

  • 在基类方法的声明中使用关键字virtual可使该方法在基类以及所有的派生类(包括派生类派生出来的类)中是虚的;

  • 如果使用指向对象的引用或指针来调用虚方法,程序将使用对象类型定义的方法,而不使用为引用或指针类型定义的方法,即非虚看引用或指针类型,虚看对象类型;

  • 如果定义的类被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚的;

  • 构造函数不能是虚函数;

  • 析构函数应当是虚函数,除非类不用做基类

    class Singer : public Employee
    {
        ...
    }
    
    Employee * pe = new Singer;
    delete pe;
    

    如果使用静态联编 ,delete pe;将根据指针类型调用类Employee的析构函数~Employee(),这样只会释放类Singer从类Employee中继承过来的那一部分,自身的那一部分因为没有调用自身的析构函数而不会被释放;

    如果使用动态联编,delete pe;将根据对象类型先调用类Singer的析构函数~Singer(),释放属于类Singer的那一部分,然后再调用类Employee的析构函数~Employee(),释放属于类Employee的那一部分;

  • 友元函数不能是虚函数,因为友元函数不是类成员,而只有成员才能是虚函数,如果因为这个原因引起的设计问题,可以通过让友元函数使用虚成员函数来解决;

  • 如果派生类没有重新定义函数,将使用该函数的基类版本。如果派生类位于派生链中,则将使用最新的虚函数版本,例外的情况是基类版本是隐藏的;

  • 重新定义将隐藏方法

    class Dweling
    {
    public: 
        virtual void showperks(int a) const;
        ...
    };
    
    class Hovel : public Dwelling
    {
    public:
        virtual void showperks() const;
    }
    
    Hovel trump;
    trump.showperks();   //valid
    trump.showperks(5);  //invalid
    

    新定义将showperks()定义为一个不接受任何参数的函数,重新定义不会生成函数的两个重载版本,而是隐藏了接受一个int参数的基类版本。

    因此这引出了两条经验规则:

    • 如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针(注意:这种例外只适应于返回值,而不适用于参数)

      class Dweling
      {
      public: 
          virtual Dweling & showperks(int a) const;
          ...
      };
      
      class Hovel : public Dwelling
      {
      public:
          virtual Hovel &  showperks(int a) const;
      }
      
    • 如果基类声明被重载了,则应在派生类中重新定义所有的基类版本

      class Dweling
      {
      public: 
          virtual void showperks(int a) const;
          virtual void showperks(double x) const;
          virtual void showperks() const;
          ...
      };
      
      class Hovel : public Dwelling
      {
      public:
          virtual void showperks(int a) const;
          virtual void showperks(double x) const;
          virtual void showperks() const;
      }
      

      如果只重新定义一个版本,则另外两个版本将被隐藏,派生类对象将无法使用它们。注意,如果不需要修改,则新定义可只调用基类版本。


9、抽象基类(ABC)

当某个类是一个类的特殊情况时(例如:圆是长轴和短轴相等的椭圆),可以考虑采用继承的方式来实现。但是派生类可能并不需要继承基类所有的成员变量和成员方法,例如:椭圆包括中心坐标、长半轴长,短半轴长、方向角等成员变量,移动椭圆、返回椭圆面积、旋转椭圆、缩放长半轴和短半轴等成员方法;而圆只需要中心坐标、半径等成员变量,成员方法也有一些是不需要的或者实现方式是不同的,所以这样分析下来,还不如直接定义一个圆的类来的方便。

但是,将圆的类单独定义,就忽略了它和椭圆具有共性这一事实 。为了解决这一问题,可以采用抽象基类(Abstract Base Class,ABC)的方法。

从Ellipse和Circle类中抽象出它们的共性,将这些共性放到一个ABC中,然后从该ABC派生出Ellipse和Circle类,这样,便可以使用基类指针数组同时管理Ellipse和Circle对象。例如:椭圆和圆的共同点是中心坐标、Move()方法,因此ABC可以这么定义:

class Base
{
private:
    double x;  //中心坐标
    double y;
public:
    Base(double x0 = 0,double y0 = 0);
    virtual ~Base();
    void Move(int nx,int ny)  //移动
    {
        x = nx;
        y = ny;
    }
}

然后Ellipse类和Circle类都从Base类中继承,对于派生类中都有但是实现方式不同的成员方法可以在基类中定义纯虚函数,例如面积,椭圆和圆的面积计算方法是不同的,而且ABC中也没有包含必要的数据成员,所以根本就不能实现Area()函数,C++通过使用纯虚函数提供未实现的函数,纯虚函数声明的结尾处为 = 0,如下所示:

class Base
{
private:
    double x;  //中心坐标
    double y;
public:
    Base(double x0 = 0,double y0 = 0);
    virtual ~Base();
    void Move(int nx,int ny)  //移动
    {
        x = nx;
        y = ny;
    }
    virtual double Area() const = 0; //纯虚函数
}

注意:

  • 当类声明中包含纯虚函数时,则不能创建该类的对象。
  • 包含纯虚函数的类只能用作基类,要成为真正的ABC,必须至少包含一个纯虚函数。
  • 在虚函数后加= 0可使虚函数变为纯虚函数。
  • 纯虚函数只是一个接口,可以不需要定义,在派生类中使用常规虚函数来实现这种接口。
  • 派生类中必须实现ABC中的纯虚函数,这是一种接口规则

你可能感兴趣的:(C++)