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: public Base { public: // Base::~Base invoked 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 no arguments 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 inherits definition of Base::fcn() }; class D2 : public D1{ public: int fcn(int); // non virtual 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::fcn at run time bp2->fcn(); // ok:virtual call, will call Base::fcn at run time bp3->fcn(); // ok:virtual call, will call D2::fcn at run time
9. 名字查找与继承
理解C++ 中继承层次的关键在于理解怎样确定函数调用。确定函数调用遵循下面四个步骤:
1. 首先确定进行函数调用的对象、引用或指针的静态类型。
2. 在该类中查找函数,假设找不到,就在直接基类中查找,如此循着类的继承链往上找,直到找到该函数或者查找完最后一个类。假设不能在类或其相关基类中找到该名字,则调用是错误的。
3. 一旦找到了该名字,就进行常规类型检查查看假设给定找到的定义,该函数调用是否合法。
4. 假定函数调用合法,编译器就生成代码。假设函数是虚函数且通过引用或指针调用,则编译器生成代码以确定依据对象的动态类型执行哪个函数版本号,否则,编译器生成代码直接调用函数。
将派生类对象拷贝到基类对象时,派生类对象将被切掉
10. 句柄类与继承
C++ 中面向对象编程的一个颇具讽刺意味的地方是,不能使用对象支持面向对象编程,相反,必须使用指针或引用。比如,以下的代码段中:
void get_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; }
通过pointer 和reference 进行的调用在执行时依据它们所绑定对象的动态类型而确定。