870920 Menu

SwingCoder之C++备忘录·12

继承与多态

 可根据已有基类派生子类,也可根据已有的多个类,提取并抽象其公共特性,创建它们的基类。

 使用继承的最主要原因是:在已有的基础之上,添加或替换功能,实现函数调用的多态性。

 继承体现了软件工程中的重要思想之一:代码复用,减少编码量。

 派生类的对象也是基类的对象,但基类对象不是派生类对象。即:子类即父类,反过来却不成立,即:父类非子类(除非强转)。
 使用继承的一个原则是:适用于基类的每一件事情(每一个非private成员函数)也一定且必须适用于派生类,因为派生类“is-a”基类。
 派生类的直接基类是派生类显式继承的类。也就是说,通过查看一个类的类定义,只能看到其上一层基类,并不能确定其所在的整个类系,也不能看到它所派生的类。
 无论哪种继承方式,基类的private成员均不能被派生类直接访问。但是,派生类依然继承了基类的所有private成员。即:基类的private成员也是派生类中的一部分,只不过类内类外均无法访问。
 派生类的大小 == 所有基类的大小 + 派生类中显式声明的所有数据成员的大小。
 创建派生类的对象时,总会启动一连串的构造函数,包括其基类的构造函数与其数据成员(其他类的对象)的构造函数。最先构造完毕的是该派生类的最顶层基类。所有基类构造完毕后,开始构造该派生类的数据成员,最后执行派生类构造函数体中的语句。派生类有多个基类和多个数据成员时,构造顺序按类中的声明顺序进行,而不是按初始化列表中的前后顺序。
 派生类对象析构时,先析构自身,而后析构其数据成员,最后逐级析构所继承的基类。析构顺序与创建时的构造顺序正好相反。
 派生类不继承基类的构造函数、拷贝构造函数、析构函数和重载的赋值运算符。但是,派生类的构造函数、析构函数和重载的赋值运算符中可以调用(且必须调用或初始化,前提是基类无默认的此类函数)基类的构造函数、析构函数和重载赋值运算符。派生类如果有拷贝构造函数,则一定要在该函数中首先调用基类的拷贝构造函数。赋值运算符重载同理。
 即使派生类不使用virtual限定符,基类的虚函数在派生类中依然是虚函数。派生类无法将继承而来的虚函数转变为非虚函数。为了可读性,派生类继承而来的虚函数最好加上virtual限定符。
 virtual虚函数关键字仅用于成员函数的声明语句(位于函数声明的最前面),该函数的实现代码中不必再添加此前缀。
 只有类的成员函数才可以声明为virtual虚函数,类的静态函数没有虚函数一说。某个成员函数是否需要声明为虚函数是设计决策。即:根据该函数的用途来判断,如果派生类重新定义此函数有更多的作用与实际意义,则将其声明为虚函数。
 虚函数无法被内联,因为无法在编译时确定并展开。并且虚函数有一定的性能损耗,因为运行时调用虚函数要首先查虚表。对于小函数来说,声明为虚函数有些得不偿失。而大函数则没有这种顾虑。
 public、protected、private三种继承方式中,无论哪一种继承方式,基类的private成员,派生类均不可访问。派生类以public方式继承基类时,基类的public成员和protected成员性质在派生类中保持不变。protected继承时,基类的public成员和protected成员全部成为派生类的protected成员。private继承时,基类的public成员和protected成员全部成为派生类的private成员。继承方式与访问属性见下表:
子类public继承基类 子类protected继承基类 子类private继承基类
基类的public成员 依然是public 成为派生类的protected 成为派生类的private
基类的protected成员 依然是protected 成为派生类的protected 成为派生类的private
基类的private成员 派生类不可访问
表 1 7 三种继承方式及其访问属性
 派生类以protected或private方式继承基类时,不属于“is-a”继承关系,且没有多态性。这样的派生类可称为“混入类(Mix-in)”。
 派生类中如果未重写基类的成员函数,可直接调用之。如果重写了基类的成员函数,并且该函数中需调用基类的同名函数做额外的处理,此时需前缀基类名和作用域运算符“::”,否则,就成了派生类递归调用自己的成员函数了,并且是无限递归,这是致命的逻辑错误。
 派生类在构造函数初始化列表中完成对基类的初始化(构造基类),语法:
派生类类名(…) : 基类类名(基类的构造参数1, 基类的构造参数2…) { // … }
 派生类的头文件中总是需要#include “基类的头文件”。即:代码编译之前的预处理阶段,必须首先将基类的类定义展开在派生类的类定义之前。原因有三:一是提前保证基类类名可见。二是编译器可确定基类的大小,从而确定派生类的大小(编译器根据类定义中的所有数据成员和基类的大小来确定一个类的对象所需的内存空间)。三是编译器根据基类的类定义来判断派生类是否正确使用了继承自基类的各种成员,如有错误,则拒绝编译并报错。
 无论基类是否类模板,派生类均可以定义类模板和函数模板。
 友元关系无法被继承。基类的静态成员也无法被继承。
 实现运行时多态的前提有二:必须是基类对象的引用或指针,基类对象的引用或指针必须调用其虚函数。基类对象的引用或指针不能调用基类中所没有的派生类新增函数。基类对象的引用或指针如需调用派生类的新增函数,必须使用dynamic_cast<子类*>强转为派生类指针,并在使用该指针之前进行判断。
 一个类如有虚函数,则意味着可派生其子类,此时必须将该类的析构函数声明为virtual虚析构。换句话说,如果一个类打算派生子类,则应将析构函数声明为virtual虚函数,或者说:如果一个类的析构函数是非虚的,则证明类的设计者不打算或不允许派生该类的子类。
 基类为虚析构,则其所有派生类的析构函数自动为虚。某个类系中,仅声明基类的析构函数为virtual即可。但为了清晰直观,建议在派生类的析构函数声明中也前缀virtual关键字。
 不要在构造函数和析构函数中使用虚函数,此时子类尚未创建或已经销毁,虚函数机制不起作用,所调用的虚函数只是基类本身的。
 除了赋值运算符,基类所有重载的运算符均被派生类继承。
 继承与虚函数等机制导致对象的指针或引用可具有多种不同的数据类型,大体分两大类:静态类型(所声明的类型,比如基类型)和动态类型(实际指向的类型,比如各个派生类型)。此即多态性的根本含义(一个对象多种类型,运行时绑定)。
 非尾端类(具体类)应定义为抽象类,其析构函数必须定义为虚函数。抽象基类为派生类提供了极大的弹性,但也增加了派生类的负担。
 使用类库中的类来定义新类时,如果该类已经是具体类(最典型的就是析构函数非虚,类中无虚函数),则不适于用继承的方式来自定义新类。可将该具体类的对象组合到自定义类中,而后,自定义类中声明与具体类一致的成员函数,这些成员函数中,用组合而来的具体类的对象调用其同名函数。
 基类的析构函数不仅可以是虚函数,还可以是纯虚函数。普通纯虚函数没有且无须定义,而纯虚析构函数则可以且必须定义。这一点非常让人意外,但却是符合C++规则的。为了使某个类成为抽象类,在其他成员函数无法为纯虚的前提下,可以将析构函数声明为纯虚函数,但必须实现之。任何纯虚函数都可以实现,但除了纯虚析构函数之外,实现其他纯虚函数并无意义。
 子类重写父类的某个非虚函数,会隐藏父类的同名函数。且无法使用多态性(即函数的调用是编译期硬编码)。
 绝对不要把多态用于数组。原因:多态是通过引用或指针来实现的,遍历或访问数组元素也通过指针来实现,但基类和派生类的内存布局与大小并不完全一致。
 使用virtual虚函数的多态类,不能写成完全内联类(类定义与类实现全部位于h头文件中),至少要有一个成员函数或静态成员位于cpp源文件中,否则,可能会出现链接错误。
 如果父类的虚函数的参数具有默认值,子类重写时也应该提供默认值,而且应与父类的默认值保持一致,否则,指向子类的父类指针或引用调用该函数时,会出现这个现象:所调用的函数是子类重写的,但该函数的参数却采用父类的默认值。
 父类的public函数,即使被子类声明为private或protected,也依然可以访问,前提是使用指向子类的父类指针或引用(直接使用子类对象调用该方法,是不能访问的)。同理,父类的private或protected函数,如果被子类声明为public,那么,可以直接用子类对象调用该函数,但是,指向子类的父类指针或引用却无法调用该函数。C++的多态性在工作时,首先判断和处理的依然是指针的类型,而后才是指针实际所指的对象及是否运用虚函数机制。
 需注意子类的拷贝构造函数和赋值运算符重载函数的写法。如果子类没有新增数据成员,那么无需显式声明和定义这两个函数,编译器生成的默认拷构和赋值即可正常使用(将自动调用父类的这两个函数)。如果子类新增了数据成员,或者显式声明和定义自己的拷构与赋值函数,则需在这两个函数中分别调用父类的同名函数(拷构函数在初始化列表中调用父类的拷构函数),示例:
class Base
{
public:
Base(const Base& rhs);
Base& operator= (const Base& rhs);
};
class Sub : public Base
{
public:
Sub(const Sub& rhs) : Base(rhs) // 初始化列表中,调用父类的拷构函数
{
// 子类新增的数据成员获取赋值
}
Sub& operator= (const Sub& rhs)
{
if (rhs == *this)
return *this;
Base::operator= (rhs); // 调用父类的赋值函数
// 子类新增的数据成员获取赋值
return *this;
}
};