探索继承技术
本文是基于大家已经知道继承技术的基础上强化一些知识
继承的客户视图:
Super
↑ Sub类型的对象也是Super类型的对象,因为Sub是从Super继承而来的。
Sub
指向对象的指针或者引用可以引用所声明类的对象或其任何子类对象。比如,指向Super的指针可以实际上指向Sub对象,对于引用也是这样。客户代码仍然只访问Super中的方法和数据成员,但是通过这种机制,对Super进行操作的代码也可以对Sub进行操作。
Super *SuperPoint = new Sub();
继承的子类视图:
子类(public继承)可以访问超类的public和protected方法和数据成员。
覆盖方法:
超类中只有声明为virtual的方法才能被子类正确覆盖。
一个好的经验就是所有方法都声明为virtual(构造函数除外),这样就不需要担心覆盖方法是否有效了。唯一的不足就是牺牲了性能。
语法:在子类的定义中重新声明。在子类的实现文件中,重新定义。超类和子类中,此函数的定义之前都不需要加上virtual关键字,在类定义的声明处加上就可以了。
如果子类中不想继续被子类的子类中的方法覆盖,在子类中方法的声明处就不需要加上virtual关键字了,但经验就是,最好加上,以备子类继续被扩展。
如:
class Super
{
public:
...code...
virtual void someMethod();
...code...
};
void Super:: someMethod()
{
cout<<”Super’s method.”< } class Sub:public Super { public: ...code... virtual void someMethod(); virtual void otherSomeMethod(); ...code... }; void Sub:: someMethod(); { cout<<”Sub’s method.”< } 覆盖方法的客户视图: 经过前面的覆盖之后,对于someMethod()方法,Super与Sub对象都可以调用,只是行为有所不同。对于指针或引用可以引用类的对象或任意子类的对象。对象自身知道自己实际是哪个类的对象,所以只要方法用virtual声明,就会调用正确的方法。 考虑下面: Super mySuper; Sub mySub; Super *superPoint = &mySub; Super &ref = mySub; Super obj = mySub; mySuper.someMethod(); mySub.someMethod(); superPoint->someMethod(); ref.someMethod(); obj.someMethod(); 依次输出为: Super’s method.” Sub’s method. Sub’s method. Sub’s method. Super’s method.” 注意:即使超类的指针和引用知道实际上它是一个子类对象,也不能调用在超类中未定义的子类方法或者成员。 对于: ref.otherSomeMethod(); //bug 对于非指针、非引用的对象,它不知道自己到底是哪个类的对象,这样,下面的aObj对象就会失去子类中的一些知识。 Sub mySub; Super aObj = mySub; aObj.someMethod(); 输出: Super’s method.” 综上:就相当于把Super对象看做一个盒子,Sub看做另一个更大的盒子(因为子类添加了一些自己的内容)。在使用Sub的引用或指针时,盒子不会改变,只是采用一种新的办法来访问。然而,在把Sub强制转换为Super时,就会扔掉一些Sub自己独有的内容,才能把它放进一个较小的盒子中。 考虑父类: 1. 父构造函数: C++定义的对象的创造顺序为: a. 如果有的话,首先构造基类。 b. 非static数据成员按照声明顺序构造。 c. 执行构造函数。 注意其父类构造函数是系统自动调用的。如果其父类存在默认构造函数,C++会自动调用。如果父类没有默认构造函数,或者尽管有,但是希望使用另外一个构造函数,则可以把构造函数用链串起来,就像初始化列表中初始化数据成员一样。 如: class Super { public: Super(int i); }; class Sub:public Super { public: Sub(); }; Sub::Sub():Super(7) { ...code... } 如果向父类构造函数传递自己的数据成员作为参数则是不行的,因为先调用父类构造函数,后再初始化自己的数据成员,如果这样,则传递的数据成员是未初始化的。 2. 父析构函数 因为析构函数不能包含参数,所以C++会给父类自动调用其析构函数。撤销的顺序与构造顺序恰好相反。 a. 调用析构函数体 b. 按照构造的逆序删除数据成员 c. 如果有父类,析构父类 注意,作为经验,所有析构函数都应该使用virtual关键字声明。如果不这样,就可能会发生错误。考虑:如果代码可能对一个超类指针调用删除操作,但这个超类指针实际指向一个子类对象,那么析构链的开始位置就不对了。 3. 引用父类的数据 在子类中函数和数据成员的名称可能会产生二义性,进行多重继承尤其如此。C++提供了一种机制来消除二个类之间的名字二义性:作用域解析操作符。 在子类中覆盖方法时,实际上是替换其他代码感兴趣的原始代码?不过父类中的方法依然存在,可能还会用到。在子类中的方法要调用父类中的方法(该方法子类覆盖了),则需加上父类名加上作用域解析操作符。 如: class Super { public: ...code... virtual string doSomething(){ return “Super”;} ...code... }; class Sub:public Super { public: ...code... virtual string doSomething(){ return “Sub”+ Super:: doSomething() ;} virtual void otherSomeMethod(); ...code... }; 4. 向上类型强制转换和向下类型强制转换 向上类型强制转换: 下面会造成切割 Super mySuper = mySub; 下面就不会发生切割了 Super *superPoint = &mySub; Super &ref = mySub; 总结:进行向上类型强制转换时,要使用指向超类的指针或引用来避免切割问题。 向下类型强制转换: 考虑下面: void presumptuous(Super* inSuper) { Sub * mySub = static_cast(inSuper); ...other code... }