第15章 继承和派生• 继承和派生是父类和子类之间的关系,是从两个不同角度 谈同一件事情继承让子类可以获得父类的特性,派生让 父类可以将自己的特性遗传给子类学习本章,读者可以 设计出更加满足实际需要的类,在更高的抽象层次上考虑 和解决问题,从而应对更复杂的实际需要15.1 由类生成类• 派生和继承都是在描述同一件事情,它们是从两个角度来 谈父类和子类的关系 • 从已有的类遗传产生一个新的类,称为类的派生该类被 称为父类,也叫基类新的类被称为子类通过派生,父 类将其已有的特性遗传给了子类,子类将自然拥有父类的 各种特性派生机制提供了扩展或定制基类特性的手段, 子类可以增加自己独有的特性通过类的派生就可构造可 重用的类库,扩展类的特性从子类的角度来看这件事情 ,即一个新类从已有的类那里获得其特性,就称为继承 从父类到子类是一个特殊化、具体化的过程,从子类到父 类则是一个泛化、抽象的过程•例如,定义动物类animal,从animal派生子类dog在这个过程 中,父类animal派生了子类dog,或者说子类dog继承于父类 animal从animal还可以派生cat或chick等子类子类也可以继 续派生。
例如,dog可以继续派生policedog、armydog等具体的 类从这个过程可以看出,子类和父类是一个相对的概念dog 是animal的子类,又是policedog的父类 •一个父类可以派生多个子类,一个子类也可以继承于多个父 类如果一个子类只从一个父类派生,就称为单继承如果一 个子类继承于多个父类,就称为多重继承任何一个类都可以 作为父类派生新的子类,也都可以作为子类继承于其他的类 •从父类派生子类时,子类可以具有:继承父类的数据成员;继 承父类的成员函数;增加新的数据成员;增加新的成员函数; 重新定义基类中已有的成员函数;改变现有成员的属性等特 征15.2 派生一个类• 派生指从父类产生子类,并将父类的所有特性遗传给子类 ,这个过程就叫作派生派生将一个父类具体并特例化为 新的子类,它避免了重复定义某些公有的特性,又允许子 类定义自己特有的性质本节将详细讲解如何从一个父类 派生新的子类15.2.1 派生的起点——基类• 基类就是父类,也可以是其他父类的子类但是,只有派 生了至少一个子类的类才能称为是基类这个概念实质上 是一个相对的概念,与普通类并没有什么特殊的区别只 要某个类派生了至少一个子类,就可以称其为基类。
但是 一般情况下,基类都会被定义为一个抽象的概念,它泛化 了某一类事物的共有特征因此,直接从这样的基类实例 化一个对象是没有意义的例如,直接用animal实例化一 个类对象是没有任何意义的基类的用处是常常可以定义 基类的指针,从而指向子类,这在实现面向对象的多态性 方面很有帮助基类成员的下述性质将影响到子类的成 员1.private修饰符• 被private修饰的成员是私有成员,它对外界完全封闭的 私有成员可以被类自身的成员和友元访问,但不能被包括 派生类在内的其他任何类和任何普通函数访问2.public修饰符• 被public修饰的成员是公有成员,它对外界是完全公开 的公有成员可以被任何普通函数和任何类的成员函数访 问,也可以被子类访问3.protected修饰符• 被protected修饰的成员是保护成员,它对外界是半开半闭 的保护成员可以被类自身的成员和友元访问,还可以被 派生类的成员函数访问,但不能被任何非友元的普通函数 访问 • 注意:好的编程习惯是尽量避免使用友元去访问私有成 员,除非确实需要因为这破坏了类的封装性示例15-1】• 演示类成员的限定符用法•class CPerson •{ •public://公共成员区 •CPerson(); •virtual ~CPerson(); •protected://保护成员区 •string m_name; •bool m_sex; •short m_age; •public: •int GetVersion(char *info); •private://私有成员区 •string m_ver; •};• 分析:该示例定义了一个基类CPerson的一部分。
CPerson 的构造函数和析构函数被声明为公有的,这是因为该类的 子类将会自动调用它们CPerson的属性m_ver表示类的版 本号,它只能通过公有成员函数GetVersion()获得它的 属性m_name、m_sex等被定义为了保护属性,只能被子类 访问到15.2.2 派生的方式• 派生指从基类衍生出一个新的子类但是派生并不是把基 类的成员和派生类自己增加的成员,简单地加在一起就成 为派生类构造一个派生类需要依次进行下述工作•(1)全继承:不加选择地继承基类的所有成员,但是不包括构 造函数和析构函数 •(2)成员调整:按照指定的继承方式和重载或覆盖方式调整基 类的成员满足自己的需 要,从而实现多态 •(3)重写构造函数与析构函数:子类不继承这两种函数,无论 原来是否可用,子类最好重写它们 •(4)特例化:增加子类自己的成员,扩展基类的属性和方法 •其中,第(1)步是自动完成的,第(2)和第(3)步是按需进 行在从一个基类派生一个子类后,至少会在第(2)或第(3 )步有至少一种改动,否则,派生就变得没有任何意义因此 首先需要有一个基类,基类提供了基本的属性和方法然后再 派生子类,子类将修改或增加新的属性方法。
•类的派生格式如下:•class 子类:基类 •{ •. •};•类的派生仅是在声明类时增加“:基类”表示前面 的类派生于后面的基类,“派生方式”规定了子类如何继承基类 的成员子类自动接收了全部基类的成员,但构造函数和析构 函数是不能被继承的,派生类要重新定义构造函数和析构函 数示例15-2】• 使用示例15-1中的类派生新的子类 • 分析:该示例使用示例15-1中的CPerson类派生了CStudent 类和CStudent2类,以受保护方式继承CPerson中的所有 成员都成了CStudent中的受保护成员CStudent除了继承 CPerson的成员外,还定义了属于自己的id和school属性, 重载了m_ver和GetVersionCStudent没有重载m_ver和 GetVersion因此,CStudent访问可以直接访问GetVersion 函数和m_ver,而且访问的是自身的成员但是CStudent2 则不能而且m_GetVersion是CPerson的受保护成员,所 以只能在CStudent2内部访问所以,如果放开示例中被注 释掉的两行语句,编译器会提示成员不可访问。
15.2.3 使用构造函数• 构造函数是类的特殊函数,只要声明一个类,就自动为它 分配了一个默认的构造函数其声明方式如下:• ClassName();• 其中,ClassName是类的名字,可以有参数,也可以不带 参数它不能有返回值,也不能用void来修饰• 构造函数的定义方式如下:• ClassName::ClassName() • { • . • }• 构造函数是实例化类时要调用的函数,它对类对象进行了 初始化当从基类派生子类时,构造函数不被继承,子类 必须自行声明• 子类声明自己的构造函数时,遵循如下原则: • 初始化子类的新增成员,继承来的成员则由基类完成初始 化 • 若基类的声明中使用不带参数的构造函数或未声明构造函 数,则子类构造函数的声明中可以省略对基类构造函数的 调用,或不声明构造函数 • 若基类声明了带参数的构造函数,则派生类也应声明带参 数的构造函数,并显示将参数传递给基类的构造函数•根据上述原则,当基类带参数时,构造函数的形式如下所示•ClassName():BaseClass()[,Object()] •{ • . •}•子类的构造函数后显示给出了基类的构造函数,基类构造函数 的参数来自子类的构造函数的参数。
如果子类内还有其他对象 需要被初始化,则也可以放在构造函数后冒号后部分的顺序 可以随意,与调用顺序无关在声明构造函数时,不需要带冒 号及冒号后的部分• 在实例化子类时,构造函数的调用顺序遵循下述原则: • 基类构造函数先于子类执行,调用顺序按照继承时声明的 顺序从左向右执行; • 如果有内嵌对象,则调用内嵌对象的构造函数,调用顺序 由声明顺序决定; • 最后执行子类的构造函数示例15-3】• 构造函数的调用过程演示 • 分析:该实例中,子类CStudent的构造函数同时也调用了 基类CPerson和成员变量m_school的构造函数从输出结 果可以看出,基类和子类成员m_school的构造函数先于子 类的构造函数执行15.2.4 使用析构函数•析构函数是类的特殊函数,只要声明一个类,就自动为它分配 了一个默认的析构函数析构函数的作用是当类不再使用时进 行善后工作,比如收回动态分配的内存其声明方式 如下:•~ClassName();•其中,ClassName是类的名字,~是析构函数的标志符它不能 有返回值,也不能用void来修饰析构函数的定义方式如下:•ClassName::~ClassName() •{ •. •}• 与构造函数一样,析构函数也不被继承,子类必须自行声 明。
但是,是否需要定义构造函数与基类没有关系,完全 依赖子类是否需要在撤销时做一些善后工作基类的析构 函数不会因派生类没有析构函数而得不到执行,它们各自 是独立的子类也不需要显式地调用基类的析构函数,系 统会自动隐式调用 • 析构函数的调用次序与构造函数相反当撤销子类对象时 ,先执行子类的析构函数,再执行新增成员对象的析构函 数,最后执行基类的析构函数示例15-4】• 演示析构函数的调用过程 • 分析:该示例中CStudent继承于CPerson,在CStudent内又 有内嵌的类对象CPerson cp构造函数的调用顺序是按照 派生类的构造函数的声明顺序来调用的,即先调用基类 CPerson的构造函数,再调用内嵌成员CPerson cp的构造函 数析构函数则恰好与构造函数的调用顺序相反从输出 可以看出,析构的过程恰好与构造相反首先析构子类, 然后是子类的对象cp,最后才是基类 • 注意:由于基类和子类的析构函数都会被调用到,所以 在子类中释放为基类的成员申请的动态内存时,一定要先 检查是否已经被释放;否则,将导致因重复释放某个指针 而出错15.2.5 方法同名,怎么办• 当子类和父类有同名的方法时,如果父类中的方法是虚拟 的,则子类可以重新定义它,也可以直接使用;如果不是 虚拟的,则子类的方法将覆盖父类的同名方法。
如果没有 明确指明,则通过子类调用的是子类中的同名成员要想 在子类中访问被覆盖的同名成员,要用域限定符来指出 这属于面向对象思想中的多态性,在第16章将会有详细讲 解示例15-5】• 子类与父类有同名方法的示例 • 分析:该示例中CPerson和CStudent有同名的方法 getName()如果直接用CStudent对象cs调用,则调用的是 CStudent的getName()函数要想调用父类的getName(),就 必须用域限定符以“CPerson::getName(name);”的形式调 用域限定符显式指定了要调用的方法属于哪个类15.2.6 属性同名,怎么办• 如果子类与父类有同名的属性时,子类将覆盖掉父类的属 性要想调用父类的属性,必须用域限定符显示指定这 是类的多态性,在第16章将有详细讲解示例15-6】• 演示子类与父类有同名属性时的使用方法 • 分析:在CStudent的构造函数中,将实例化时的参数jack 传给了父类构造函数,但是却对自己的成员m_name赋值 为tom15.3 单 重 继 承•单重继承指从单。