C++ 7.继承和派生

继承和派生

继承是面对对象设计的一个重要特性,是软件复用的一种形式,它允许在原有类的基础上创建新的类。新类可以从一个或多个原有类中继承函数和数据,并且可以重新定义或增加新的数据和函数,从而形成类的层次或等级。

继承与派生

类的继承是新的类从已有的类中得到已有的属性。从已有的类产生新的类的过程就是类的派生。

在继承过程中,原有的类或已经存在的用来派生新类的类称为基类或父类,而由已存在的类派生出的新类称为派生类或子类。

从派生的角度来看,根据它所拥有的基类的数目的不同,可分为单继承和多继承。

  • 单继承:一个类只有一个直接基类。
  • 多继承:一个类有多个直接基类。

从以上描述可知,任何一个类都可以派生出一个新类,派生类也可以再派生出新类,因此,派生类和基类是相对而言的,一个基类可以是另一个基类的派生类,从而形成了复杂的继承结构,出现了类的层次。

基类与派生类的关系如下:

  1. 基类是派生类的抽象,派生类是基类的具体化。基类抽取了它的派生类的公共特性,而派生类通过增加信息将抽象的基类变为某种有用的类型,派生类是基类定义的延续。
  2. 派生类是基类的组合。多继承可以看作是单继承的简单组合。
  3. 公有派生类的对象可以作为基类的对象处理。这一点与类聚集(成员对象为类对象)是不同的,再类聚集中,一个类的对象只能拥有作为其成员的其他类的对象,单不能作为其他类对象而使用。

派生类的定义:

class <派生类名> : <继承方式1> <基类名1>,
                  <继承方式2> <基类名2>,
                  ...,
                  <继承方式n> <基类名n>
{
    <派生类新定义成员>
}
  • <派生类名>:继承原有类的特性而生成的新类的名称。
    • 单继承:只需定义一个基类。
    • 多继承:需同时定义多个基类。
  • <继承方式>:派生类访问的控制方式,用于控制基类中的成员在多大范围内可被派生类的用户访问。
    • private(私有继承):
    • public(公有继承):
    • protected(保护继承):
  • <基类名>:已有的类的名称。
  • <派生类新定义成员>:新增加的数据成员和函数。

派生类中,其成员由两部分组成:

  • 从基类继承得到的(基类部分)
    • private:派生类不可访问
    • protecteed:随派生类对基类的继承方式而改变
    • public:随派生类对基类的继承方式而改变
  • 自己定义的新成员(新定义部分)
    • private
    • protected
    • public

派生类对基类成员的访问

派生类继承了基类的全部数据成员和除了构造函数、析构函数之外的全部成员函数,但是这些成员在派生类中的访问属性在派生的过程中是可以调整的,继承方式控制了基类中具有不同访问属性的成员在派生类中的访问属性。

类的继承方式有三种

  1. 公有继承(public)
  2. 保护继承(protected)
  3. 私有继承(private)

派生类对基类成员的访问能力

公有继承保护继承私有继承
私有成员xxx
公有成员√(公有成员->保护成员)√(公有成员->私有成员)
保护成员√(保护成员->私有成员)
  • 基类的私有成员在派生类中是隐藏的,只能在基类内部访问。
  • 派生类中的成员只可访问基类中的保护成员和公有成员。

派生类的构造函数和析构函数

派生类不仅继承了基类的成员,还添加了新的成员进行了功能的扩充。因此建立派生类的实例对象时,不仅要初始化派生类对象的基类成员,还要对派生类的新增成员进行初始化。

TIPS:因为基类的构造函数与析构函数不可被继承,因此派生类函数必须负责调用基类的构造函数,并对其进行设置。同样,对派生类对象的清理工作也需要加入新的析构函数。

派生类的构造函数

派生类的数据成员由所有的基类的数据成员与派生类新增的数据成员共同组成。构造派生类的对象时,必须对全部数据成员进行初始化。

<派生类名>::<派生类名>(<总参数表>):<基类名1>(<参数表1>),
                                    ...,
                                 <基类名n>(<参数表n>),
                                 <成员名1>(<参数表n+1>),
                                    ...,
                                 <成员名m>(<参数表n+m>)
{
    <派生类构造函数>
}
  • 派生类的构造函数名称与类名相同,在构造函数的的参数列表中,给出了初始化基类数据以及新增数据成员所需的全部参数。
  • 派生类存在多个基类时,处于同一层次的各个基类的构造函数调用顺序取决于定义派生类时声明的顺序(自左向右),与派生类构造函数的初始化列表中给出的顺序无关。
  • 建立派生类对象时,构造函数的执行顺序如下:
    1. 调用基类的构造函数对基类的数据成员进行初始化,调用顺序为基类被继承时声明的顺序(自左向右)。
    2. 对新增成员进行初始化,执行顺序按照各个成员在类中声明的顺序(自上而下)。
    3. 执行派生类的构造函数体。
  • 派生类的构造函数只有需要时才必须定义,如果不定义,编译器会给出默认的构造器。
派生类的析构函数

与构造函数相同,派生类的析构函数在执行过程中也要对基类和新增成员进行操作,但它的执行过程与构造函数严格相反,即:

  1. 对派生类新增成员进行清理,如果新增成员中包扩成员对象,则调用成员对象的析构函数对成员对象进行清理。
  2. 调用基类析构函数,对基类成员进行清理。
  3. 派生类析构函数的定义与基类无关,它只负责对新增成员的清理工作,系统会自动调用基类的析构函数进行相应的清理工作。

多继承与虚基类

在派生类中对基类成员的访问应该时唯一的,但是,在多继承情况下,可能具有多个基类具有同名属性或函数的情况,此时对基类成员的访问便出现了不唯一的情况,这时称为对基类成员的访问产生了二义性。

解决二义性的两种方法:

  1. 通过作用域运算符(::)指明访问的是哪个基类的成员。
    
    <对象名>.<基类名>::<成员名>                 // 数据成员
    <对象名>.<基类名>::<成员名>(<参数列表>)      // 成员函数
    
    obj.Base1::fun();
    
  2. 在类中定义同名成员,即派生类重写多个基类的同名方法,并在方法体中调用具体的基类的方法
    void fun
    {
        Base1::fun();
        // 或
        Base1::fun;
    }
    

在不同的作用域中声明标识符的可见性原则:
如果存在两个或多个具有包含关系的作用域,如果在内层没有声明与外层声明标识符同名的标识符,那么它在内层可见;如果内层声明了同名标识符,则外层标识符在内层不可见,这时内层变量改写了外层同名变量。

TIPS:继承关系中,派生类在内层,基类在外层。

虚基类的定义

当一个派生类从多个基类派生,而这些基类又有一个共同的基类,当对该基类中说明的成员进行访问时,可能会出现二义性。虚基类就是为了解决这种二义性问题提出来的。

在产生二义性问题的情况中,产生二义性最主要的原因是基类Base在派生类Derived2中产生了两个子对象,从而导致了对基类Base的成员data访问的不唯一性。虚基类的任务就是使公共基类Base在派生类中只产生一个对象。

虚基类的说明格式如下:

class <类名> : virtual <继承方式> <基类名>      // virtual与继承方式的位置无关,必须在虚基类名之前,并只对紧随其后的基类名起作用。


class Base
{
    public:
        int data;
};

class Derived11 : virtual public Base {};
class Derived12 : virtual public Base {};

class Derived : public Derived11, public Derived12 {};

虚基类与非虚基类的存储结构:

  • 非虚基类:派生类对象中都包含一个基类对象
  • 虚基类: 派生类对象中包含基类对象的引用,所有引用指向一个对象。
虚基类的构造函数

使用虚基类解决二义性问题的关键是在派生类中只产生一个虚基类子对象。

对于虚基类来说,由于派生类的对象中只可有一个虚基类子对象,所以,在建立一个派生类对象时,这个虚基类的构造函数必须只调用一次。所以虚基类子对象由最派生类的构造函数通过调用虚基类的构造函数实现。即最派生类的构造函数的初始化列表中必须列出对虚基类构造函数的调用,如果未列出,则表示使用该虚基类的默认构造函数。

当在构造函数的初始化列表中同时出现对虚基类和非虚基类的构造函数的调用时,虚基类的构造函数先于非虚基类的构造函数被调用。

子类型关系

公有继承时,派生类的对象可以作为基类的对象处理,派生类是基类的子类型。子类型关系使得在任何需要使用基类的地方都可以通过使用公有派生类对象代替。从而可以使用相同的函数同一处理基类对象和派生类对象(形参为基类对象时,实参可为派生类对象),大大提高了程序的效率。

TIPS:子类型关系是实现多态性的重要基础之一。

子类型关系的定义如下:

有一个特性的类型S,当且仅当它提供了类型T的行为时,称类型S是T的子类型。

公有继承方式可以实现子类型关系,即派生类S是基类T的子类型。

具有子类型关系的基类和派生类的对象之间满主如下赋值兼容规则:

  1. 公有派生类的对象可以赋值给基类的对象,即用公有派生类对象中从基类获取的成员逐个赋值给基类对象的成员。
  2. 公有派生类的对象可以初始化基类的引用。
  3. 公有派生类的对象的地址可以赋值给指向基类的指针。

多态性

在子类型关系中,在出现基类的场合可以使用派生类进行替代,但替代之后派生类仅能访问基类中的成员,要想使派生类可以访问自身类中的成员,就必须利用面向对象的多态性。

多态性的概念:一个面对对象的系统通常要求一组具有相同基本语义的方法能在同一接口下为不同的对象服务。

与传统的面向过程的高级语言相比,C++不但提供了固有的多态性,还提供了自定义多态性的手段。多态性使得不同的但又具有某种共同属性的对象不但能在一定程度上共享代码,而且还能共享接口,这大大提高了系统的一致性、灵活性和可维护性。

C++语言中,多态性可分为两类:

  • 编译时的多态性,在编译时完成,其实现机制称为静态绑定。
    • 函数重载
    • 模板
  • 运行时的多态性,其实现机制称为动态绑定。
    • 虚函数体现

虚函数

在非静态成员函数声明前加上 virtual 修饰符,即把该函数声明为虚函数。

在派生类中可以重写从基类继承下来的虚函数,从而提供该函数适用于派生类的专门版本。在不重写的情况下,继承下来的虚函数仍保持在基类中的定义,即派生类和基类使用同一函数版本。

TIPS: 除少数特殊情况外,在派生类中重写虚函数时,函数名、形参表和返回值类型必须保持不变。

虚函数在派生类中被重写后,重写的函数依旧是虚函数,可以在其派生类中再次重写。

TIPS:重写的虚函数无论是否使用 virtual 修饰都是虚函数,最好不要省略virtual,以免削弱程序的可读性。

对虚函数调用的两种方式:

  1. 非多态调用:不借助于指针或引用的直接调用。 通过成员访问运算符 . 进行的。与通常的成员函数调用类似,是建立在静态绑定机制的基础上,不具备多态特征。
  2. 多态调用:借助于指向基类的指针或引用的调用。C++中一个基类指针(或引用)可以指向其派生类对象,而通过这样的指针(或引用)调用虚函数时,调用的是该指针(或引用)实际所指向的对象所在类的重写版本。

基类中的实函数也可在派生类中重写,改写的函数仍是实函数。调用实函数时,通过基类指针(或引用)调用的仍是基类的函数版本,无法调用到派生类的改写函数。即以任何调用形式调用实函数都是非多态的。

构造函数不得声明为虚函数,也不要试图在构造函数中对某个虚函数进行多态调用,因为在执行构造函数时,派生类的自有部分尚未形成,因此编译系统将这种情况处理为静态绑定,被调用的总是所在类的那个函数版本。

虚析构函数:析构函数可通过 virtual 声明为虚函数。只要虚基类的析构函数声明为虚函数,则其所有派生类的析构函数都是虚函数。

TIPS:通常只要派生类中对析构函数进行了定义,其基类的析构函数就应定义为虚函数,否则如果以基类的指针删除派生类的对象,就会产生问题。

纯虚函数与抽象类

基类不可知函数的具体实现,只可由派生类提供具体的实现的虚函数称为纯虚函数。为了将一个虚函数声明为纯虚函数,需要在虚函数原型结束符;之前加上 =0 。

拥有纯虚函数的类称为抽象类,抽象类不可定义对象。如果一个抽象类的派生类没有对其纯虚函数进行重写,则该函数在派生类中仍是纯虚函数,该派生类也为抽象类。

纯虚函数也可包含实现,但是仍未纯虚函数,标志一个函数是否为纯虚函数的标志为 =0,与是否有函数实现没有关系。具有实现的纯虚函数通常都不是完整版本,仅包含了一些公有操作。

TIPS:纯虚函数不可声明为内联函数。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我可以为您介绍一下 C++继承派生的相关知识,并且给您一个定义 Employee 类的示例代码。 在 C++ 中,继承是一种重要的面向对象编程技术,它允许我们在一个类的基础上定义另一个类。被继承的类称为基类或父类,继承的类称为派生类或子类。派生类可以继承父类的成员变量和成员函数,并且还可以添加自己的成员变量和成员函数。 下面是一个定义 Employee 类的示例代码: ```c++ // Employee 类的定义 class Employee { public: Employee(string name, int age, string gender, double salary); virtual ~Employee(); virtual void display() const; protected: string name_; int age_; string gender_; double salary_; }; // Manager 类的定义,它是 Employee 类的派生类 class Manager : public Employee { public: Manager(string name, int age, string gender, double salary, string department); virtual ~Manager(); virtual void display() const; private: string department_; }; ``` 在上面的代码中,Employee 类包含了一些基本的成员变量和成员函数,如姓名、年龄、性别和薪水,以及一个虚函数 display()。Manager 类是 Employee 类的派生类,它继承了 Employee 类的所有成员变量和成员函数,并且添加了一个部门名称的成员变量 department_ 和一个重载了 display() 函数的成员函数。 请注意,Employee 类的析构函数和 display() 函数都是虚函数,这是因为我们希望在派生类中重载这些函数时能够正确地调用它们。 以上是一个简单的继承派生示例,希望能够帮助您更好地了解 C++ 中的面向对象编程技术。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值