【Effection C++】继承与面向对象设计
条款36:绝不重新定义继承而来的non-virtual函数
class D由class B以public形式派生而来。如果class B定义了一个public non-virtual 成员函数mf,并且class D同样定义了自己的mf版本。在语法的角度上来看,class D就是隐藏了class B定义的同名成员函数mf。
考虑如下情况:
class B {
public:
void mf();
}
class D public B {
public:
void mf();
}
D d;
B *pB = &d;
D *pD = &d;
pB->mf(); //调用B::mf()
pD->mf(); //调用D::mf()
要注意到non-virtual函数都是静态绑定的,所以对于pB其类型为指向B的指针,所以通过指针pB调用的non-virtual函数永远都是B所定义的版本,而无论其实际对象是B还是D。同样,引用也有着相同的表现。
探讨理论原因
- 条款32指明public是一种is-a的一种关系。条款34指出基类声明non-virtual函数的目的,是为了令derived classes继承函数的接口及一份强制性实现,此时表明该函数的不变性凌驾于其特异性之上。
- 由此得出,适用于B对象的每一件事情,也适用于D对象,因为每个D对象都是一个B对象。B的derived classes一定会继承mf的接口和实现,因为mf是B的一个non-virtual函数,
- 重新定义D的mf函数,将会破坏B定义non-virtual mf成员函数时所主张的不变性大于特异性,或者D不应该以pubic继承B。
所以,绝对不要重新定义继承而来的non-virtual函数。
条款37:绝不重新定义继承而来的缺省参考值
原因:virtual函数动态绑定,但是函数的缺省参数值都是默认绑定的。对于一个对象而言,其静态类型和动态类型一直是相同的。对于一个指针而言,静态类型是定义指针的时候所声明的类型,其动态类型是指针所指向的对象的实际类型。
所以在实际工程中,就可能会出现如下情况:
1. 对于通过指针调用的虚函数,其虚函数是动态绑定的,即根据动态类型来决定使用的实际的虚函数。
2. 如果虚函数有着默认参数,那个这个默认参数是根据静态类型来决定的,指针的静态类型是什么其默认参数就是什么。
3. 这就导致静态类型和动态类型各出一半力的奇怪组合,也会出现各种莫名其妙的状态。
考虑如下:
class Shape {
public:
enum ShapeColor { Red, Green, Blue };
virtual void draw(ShapeColor color = Red) const = 0;
};
class Rectangle: public Shape {
public:
virtual void draw(ShapeColor color = Green) const;
};
class Circle: public Shape {
public:
virtual void draw(ShapeColor color) const;
};
在如上的定义中,如果直接按照对象的实际类型来调用draw,那么就是按照其相应对象类型函数的定义方式来执行。
如果使用基类Shape的指针或者引用来调用相应对象的draw,那么就会有基类Shape的默认函数参数和相应实际对象类型的函数定义来组合在一起。
当然,如果你使用Rectangle的指针来调用函数,那么此时的默认参数值就是和Rectangle中的定义相同。使用Circle指针来调用相应的函数就会没有默认参数。
其他注意事项
对于派生类中虚函数重写基类中虚函数,参数要完全一致,返回值可以“协变”(比如父类函数返回A类指针,子数返回B类指针,但B是A的子类)。而“默认参数”则不纳入考量。
但子类override父类的虚函数时,如果在默认参数方面与父类不同,那是个很坏的编程习惯。编译器顶多给个警告,可这样的代码会导致“出乎意料”的结果。
除此之外,如果基类中虚函数定义了默认函数参数,按照本节要求,派生类的函数参数应该有着和基类相同的默认函数参数。这样子会导致代码复用并且派生类的函数默认参数值依赖于基类的默认参数值。这个设计使得在以后的修改默认参数值的时候,很不方便。
考虑可替代的设计(NVI手法)。基类中使用一个non-virtual函数作为基类和派生类的接口,在此non-virtual函数中调用private virtual函数,并指定相应的虚函数的默认参数。相应的派生类中,只需要重写这个private virtual函数即可。
class Shape {
public:
enum ShapeColor { Red, Green, Blue };
void draw(ShapeColor color = Red) const // 现在draw是非虚函数
{
doDraw(color);
}
private:
//此函数完成实际功能
virtual void doDraw(ShapeColor color) const = 0;
};
class Rectangle: public Shape {
public:
private:
// 此函数重写基类函数,并且不需要默认参数值
virtual void doDraw(ShapeColor color) const;
};