1. 只能初始化直接基类
一个类只能初始化自己的直接基类。直接就是在派生列表中指定的类。如果类C 从类B 派生,类B 从类A 派生,则B 是C 的直接基类。虽然每个C 类对象包含一个A 类部分,但C 的构造函数不能直接初始化A 部分。相反,需要类C 初始化类B,而类B 的构造函数再初始化类A。这一限制的原因是,类B 的作者已经指定了怎样构造和初始化B 类型的对象。像类B 的任何用户一样,类C 的作者无权改变这个规约。
2. 重构
将Disc_item 加到Item_base 层次是重构(refactoring)的一个例子。重构包括重新定义类层次,将操作和/或数据从一个类移到另一个类。为了适应应用程序的需要而重新设计类以便增加新函数或处理其他改变时,最有可能需要进行重构。
重构常见在面向对象应用程序中非常常见。值得注意的是,虽然改变了继承层次,使用Bulk_item 类或Item_base 类的代码不需要改变。然而,对类进行重构,或以任意其他方式改变类,使用这些类的任意代码都必须重新编译。
3. 尊重基类接口
构造函数只能初始化其直接基类的原因是每个类都定义了自己的接口。定义Disc_item 时,通过定义它的构造函数指定了怎样初始化Disc_item 对象。一旦类定义了自己的接口,与该类对象的所有交互都应该通过该接口,即使对象是派生类对象的一部分也不例外。同样,派生类构造函数不能初始化基类的成员且不应该对基类成员赋值。如果那些成员为public 或protected,派生构造函数可以在构造函数函数体中给基类成员赋值,但是,这样做会违反基类的接口。派生类应通过使用基类构造函数尊重基类的初始化意图,而不是在派生类构造函
class Derived: publicBase { public: // Base::~Baseinvoked automatically
数函数体中对这些成员赋值。
4. 派生类析构函数
析构函数的工作与复制构造函数和赋值操作符不同:派生类析构函数不负责撤销基类对象的成员。编译器总是显式调用派生类对象基类部分的析构函数。每个析构函数只负责清除自己的成员:
5. 虚析构函数
如果删除基类指针,则需要运行基类析构函数并清除基类的成员,如果对象实际是派生类型的,则没有定义该行为。要保证运行适当的析构函数,基类中的析构函数必须为虚函数:
class Item_base { public: // no work, butvirtual destructor needed // if base pointer thatpoints to a derived object is ever deleted virtual ~Item_base(){ } };
如果析构函数为虚函数,那么通过指针调用时,运行哪个析构函数将因指针所指对象类型的不同而不同:
Item_base *itemP =new Item_base; // same static and dynamic type delete itemP; // ok:destructor for Item_base called itemP = newBulk_item; // ok: static and dynamic types differ delete itemP;<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>
6. 作用域与成员函数
在基类和派生类中使用同一名字的成员函数,其行为与数据成员一样:在派生类作用域中派生类成员将屏蔽基类成员。即使函数原型不同,基类成员也会被屏蔽:
struct Base { int memfcn(); }; struct Derived : Base{ int memfcn(int); // hides memfcn in the base }; Derived d; Base b; b.memfcn(); // calls Base::memfcn d.memfcn(10); // calls Derived::memfcn d.memfcn(); // error: memfcn with noarguments is hidden d.Base::memfcn(); // ok: calls Base::memfcn
7. 虚函数与作用域
还记得吗,要获得动态绑定,必须通过基类的引用或指针调用虚成员。当我们这样做时,编译器器将在基类中查找函数。假定找到了名字,编译器就检查实参是否与形参匹配。
现在可以理解虚函数为什么必须在基类和派生类中拥有同一原型了。如果基类成员与派生类成员接受的实参不同,就没有办法通过基类类型的引用或指针调用派生类函数。考虑如下(人为的)为集合:
class Base { public: virtual int fcn(); }; class D1 : public Base{ public: // hides fcn in thebase; this fcn is not virtual int fcn(int); //parameter list differs from fcn in Base // D1 inheritsdefinition of Base::fcn() }; class D2 : public D1{ public: int fcn(int); //nonvirtual function hides D1::fcn(int) int fcn(); //redefines virtual fcn from Base };
D1 中的fcn 版本没有重定义Base 的虚函数fcn,相反,它屏蔽了基类的fcn。结果D1 有两个名为fcn 的函数:类从Base 继承了一个名为fcn 的虚函数,类又定义了自己的名为fcn 的非虚成员函数,该函数接受一个int 形参。但是,从Base 继承的虚函数不能通过D1 对象(或D1 的引用或指针)调用,因为该函数被fcn(int) 的定义屏蔽了。
类D2 重定义了它继承的两个函数,它重定义了Base 中定义的fcn 的原始版本并重定义了D1 中定义的非虚版本
8. 通过基类调用被屏蔽的虚函数
通过基类类型的引用或指针调用函数时,编译器将在基类中查找该函数而忽略派生类:
Base bobj; D1 d1obj;D2 d2obj; Base *bp1 =&bobj, *bp2 = &d1obj, *bp3 = &d2obj; bp1->fcn(); // ok:virtual call, will call Base::fcnat run time bp2->fcn(); // ok:virtual call, will call Base::fcnat run time bp3->fcn(); // ok:virtual call, will call D2::fcnat run time
9. 名字查找与继承
理解C++ 中继承层次的关键在于理解如何确定函数调用。确定函数调用遵循以下四个步骤:
1. 首先确定进行函数调用的对象、引用或指针的静态类型。
2. 在该类中查找函数,如果找不到,就在直接基类中查找,如此循着类的继承链往上找,直到找到该函数或者查找完最后一个类。如果不能在类或其相关基类中找到该名字,则调用是错误的。
3. 一旦找到了该名字,就进行常规类型检查查看如果给定找到的定义,该函数调用是否合法。
4. 假定函数调用合法,编译器就生成代码。如果函数是虚函数且通过引用或指针调用,则编译器生成代码以确定根据对象的动态类型运行哪个函数版本,否则,编译器生成代码直接调用函数。
将派生类对象复制到基类对象时,派生类对象将被切掉
10. 句柄类与继承
C++ 中面向对象编程的一个颇具讽刺意味的地方是,不能使用对象支持面向对象编程,相反,必须使用指针或引用。例如,下面的代码段中:
voidget_prices(Item_base object, const Item_base*pointer, const Item_base&reference) { // which version ofnet_price is called is determined at run time cout <<pointer->net_price(1) << endl; cout <<reference.net_price(1) << endl; // always invokesItem_base::net_price cout <<object.net_price(1) << endl; }<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>
通过pointer 和reference 进行的调用在运行时根据它们所绑定对象的动态类型而确定。