C++ 笔记7

第13章 类继承

GitHub

建议下载下来用Typora软件阅读markdown文件

13类继承

基类和派生类的特殊关系

  • 1.派生类对象可以使用非私有的基类方法
  • 2.基类指针(引用)可以在不进行显示转换的情况下指向(引用)派生类对象(反过来不行);基类指针或引用只能用来调用基类方法,不能用来调用派生类方法。
  • 3.不可以将基类对象和地址赋给派生类对象引用和指针。

派生类构造函数要点

1.首先创建基类对象;
2.派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数。
3.派生类构造函数应初始化新增的数据成员。
注意:可以通过初始化列表语法知名指明要使用的基类构造函数,否则使用默认的基类构造函数。派生类对象过期时,程序将首先调用派生类的析构函数,然后在调用基类的析构函数

1
2
3
4
RetedPlayer::RetedPlayer(unsigned int r,const string & fn,const string &ln, bool ht)//:TableTennisPlayer()等价于调用默认构造函数
{
rating = r;
}

虚方法

  • 经常在基类中将派生类会重新定义的方法声明为虚方法。方法在基类中被声明为虚的后。它在派生类中将自动生成虚方法。然而,在派生类中使用关键字virtual来指出哪些函数是虚函数也不失为一个好方法。
  • 如果要在派生类中重新定义基类的方法,通常将基类方法声明为虚。这样,程序根据对象类型而不是引用或指针类型来选择方法版本,为基类声明一个虚的析构函数也是一种惯例,为了确保释放派生类对象时,按正确的顺序调用析构函数。
  • 虚函数的作用:基类指针(引用)指向(引用)派生类对象,会发生自动向上类型转换,即派生类升级为父类,虽然子类转换成了它的父类型,但却可正确调用属于子类而不属于父类的成员函数。这是虚函数的功劳。

派生类方法可以调用公有的基类方法

在派生类方法中,标准技术是使用作用域解析运算符来调用基类方法,如果没有使用作用域解析符,有可能创建一个不会终止的递归函数。如果派生类没有重新定义基类方法,那么代码不必对该方法是用作用域解析符(即该方法只有在基类中有)。

静态联编和动态联编

函数名联编(binding):将代码中的函数调用解释为执行特定的代码块。

  • 在C语言中,这非常简单,因为每个函数名都对应一个不同的函数。
  • 在C++中,由于函数重载的缘故,这个任务更繁杂,编译器必须查看函数参数以及函数名才能确定使用哪个函数。

    静态联编(static binding)

  • 在编译过程中进行联编,又称为早期联编

    动态联编(dynamic binding)

  • 编译器在程序运行时确定将要调用的函数,又称为晚期联编

    什么时候使用静态联编,什么时候使用动态联编?

  • 编译器对虚方法使用静态联编,因为方法是非虚的,可以根据指针类型关联到方法。
  • 编译器对虚方法使用动态联编,因为方法是虚的,程序运行时才知道指针指向的对象类型,才来选择方法。(引用同理)

    效率:为使程序能够在运行阶段进行决策,必须采取一些方法来跟踪基类指针和指向引用对象的对象类型,这增加了额外的处理开销

  • 例如,如果类不会用作基类,则不需要动态联编。
  • 同样,如果派生类不重新定义基类的任何方法,也不需要动态联编。
  • 通常,编译器处理函数的方法是:给每个对象添加一个隐藏成员–指向函数地址数组的指针(vptr)

    使用虚函数时,在内存和执行速度上有一定的成本,包括:
    a.每个对象为存储地址的空间;
    b.对于每个类,比那一期都将创建一个虚函数地址表(数组)vtbl;
    c.对于每个函数调用,都需要执行一项额外的操作,到表中查找地址。虽然非虚函数的效率比虚函数稍高,但不具有动态联编的功能

    总结:

  • 在基类方法的声明中使用关键字virtual可使该方法在基类以及所有的派生类(包括从派生类派生出来的类)中是虚的。
  • 如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不是用为引用或者指针类型定义的方法。这个成为动态联编或者晚期联编。这种行为非常重要。因为这样基类指针或引用可以指向派生类对象。
  • 如果定义的类将被用作基类,则应该将那些在派生类中重新定义的类方法生命为虚的。

虚函数细节

  • 1.构造函数不能是虚函数,派生类不能继承基类的构造函数,将类构造函数声明为虚没什么意义。

  • 2.析构函数应当是虚函数,除非类不用作基类。

    1.当子类指针指向子类是,析构函数会先调用子类析构再调用父类析构,释放所有内存。
    2.当父类指针指向子类时,只会调用父类析构函数,子类析构函数不会被调用,会造成内存泄漏。(基类析构函数声明为虚,可以使得父类指针能够调用子类虚的析构函数)所以我们需要虚析构函数,将父类的析构函数定位为虚,那么父类指针会先调用子类的析构函数,再调用父类析构,使得内存得到释放

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

  • 4.如果派生类没有重新定义函数。将使用该函数的基类版本。

  • 5.重新定义将隐藏方法不会生成函数的两个重载版本,而是隐藏基类版本。如果派生类位于派生链中,则使用最新的虚函数版本,例外的情况是基类版本是隐藏的。总之,重新定义基本的方法并不是重载。如果重新定义派生类中的函数,将不只是使用相同的函数参数列表覆盖其基类声明,无论参数列表是否相同,该操作将隐藏所有的同名方法。

    两条经验规则

  • 1.如果重新定义继承的方法,应确保与原来的原型完全相同,但是如果返回类型是积累的引用或指针,则可以修改为指向派生类的引用或指针(只适用于返回值而不适用于参数),这种例外是新出现的。这种特性被称为返回类型协变(convariance of return type),因此返回类型是随类类型变化的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //基类
    class Dwelling
    {
    public:
    virtual Dwelling & build(int n);
    }
    //派生类
    class Hovel:public Dwelling
    {
    public:
    virtual Hovel & build(int n);
    }
  • 2.如果基类声明被重载了,则应该在派生类中重新定义所有基类版本。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    //基类
    class Dwelling
    {
    public:
    //三个重载版本的showperks
    virtual void showperks(int a)const
    virtual void showperks(double a)const
    virtual void showperks( )const
    }
    //派生类
    class Hovel:public Dwelling
    {
    public:
    //三个重新定义的的showperks
    virtual void showperks(int a)const
    virtual void showperks(double a)const
    virtual void showperks( )const
    }

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

1
2
3
4
void Hovel::showperk()const
{
Dwelling::showperks();
}

访问控制:protected

  • 1.关键字protected与private相似,在类外,只能用公有类成员来访问protected部分中的类成员。
  • 2.private和protected之间只有在基类派生的类才会表现出来。派生类的成员可以直接访问基类的保护成员,但是不能直接访问基类的私有成员。

    提示:

  • 1.最好对类的数据成员采用私有访问控制,不要使用保护访问控制。
  • 2.对于成员函数来说,保护访问控制很有用,它让派生类能够访问公众不能使用的内部函数。

抽象基类(abstract base class)ABC->至少包含一个纯虚函数

  • 在一个虚函数的声明语句的分号前加上 =0 ;就可以将一个虚函数变成纯虚函数,其中,=0只能出现在类内部的虚函数声明语句处。
  • 纯虚函数只用声明,而不用定义,其存在就是为了提供接口,含有纯虚函数的类是抽象基类。我们不能直接创建一个抽象基类的对象,但可以创建其指针或者引用。
  • 值得注意的是,你也可以为纯虚函数提供定义,不过函数体必须定义在类的外部。但此时哪怕在外部定义了,也是纯虚函数,不能构建对象。
  • 派生类构造函数只直接初始化它的直接基类。多继承的虚继承除外。

抽象类应该注意的地方

  • 抽象类不能实例化,所以抽象类中不能有构造函数。
  • 纯虚函数应该在派生类中重写,否则派生类也是抽象类,不能实例化。

抽象基类的作用

  • C++通过使用纯虚函数(pure virtual function)提供未实现的函数。纯虚函数声明的结尾处为=0,

    1
    virtual double Area() const=0;//=0指出类是一个抽象基类,在类中可以不定义该函数
  • 可以将ABC看作是一种必须的接口。ABC要求具体派生类覆盖其纯虚函数—迫使派生类遵顼ABC设置的接口规则。简单来说是:因为在派生类中必须重写纯虚函数,否则不能实例化该派生类。所以,派生类中必定会有重新定义的该函数的接口。

  • 从两个类(具体类concrete)(如:Ellipse和Circle类)中抽象出他们的共性,将这些特性放到一个ABC中。然后从该ABC派生出的Ellipse和Circle类。

  • 这样,便可以使用基类指针数组同时管理Ellipse和Circle对象,即可以使用多态方法*

友元

  • 就像友元关系不能传递一样,友元关系同样不能继承,基类的友元在访问派生类成员时不具有特殊性,类似的,派生类的友元也不能随意访问基类的成员。

继承和动态内存分配(todo)

  • 只有当一个类被用来做基类的时候才会把析构函数写成虚函数。
  • 当基类和派生类都采用动态内存分配是,派生类的析构函数,复制构造函数,赋值运算符都必须使用相应的基类方法来处理基类

# C++

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×