《Effective C++:改善程序与设计的55个具体做法》阅读笔记 6——继承与面向对象设计
条款32:确定你的public继承塑模出is-a关系
参考:链接
使用public继承时,要确保子类和父类是is-a关系。is-a关系:子类是一种父类。
函数形参类型为父类指针或引用,那么可以给函数传入子类指针或引用。(此结论只对public继承有效,private继承需要看条款39,作者没有说protected继承的时是否有效)
is-a关系被破坏的情况——父类中有子类不应该有的属性或方法。如果Penguin(企鹅)继承自Bird(鸟),但是Bird中有fly方法。解决方法:
- (1)方法一:在Penguin的fly()方法里面抛出异常,一旦调用了p.fly(),那么就会在运行时捕捉到这个异常。这个方法不怎么好,因为它要在运行时才发现问题。
- (2)方法二:去掉Bird的fly()方法,在中间加上一层FlyingBird类(有fly()方法)与NotFlyingBird类(没有fly()方法),然后让企鹅继承与NotFlyingBird类。这个方法也不好,因为会使注意力分散,继承的层次加深也会使代码难懂和难以维护。
- (3)方法三:保留所有Bird一定会有的共性(比如生蛋和孵化),去掉Bird的fly()方法,只在其他可以飞的鸟的子类里面单独写这个方法。这是一种比较好的选择,因为根本没有定义fly()方法,所以Penguin对象调用fly()会在编译期报错。
条款33:避免遮掩继承而来的名称(避免在子类中重载函数,或定义和父类相同的成员变量)
参考:链接
子类中避免出现和父类同名的成员变量;子类中避免重载父类的成员函数,重写倒是没有问题。因为会出现下面那些麻烦的事情。
成员变量被遮掩:
#include<iostream>
//成员变量被遮掩
class Base
{
public:
int x;
Base(int _x):x(_x){}
void show(){
std::cout<< x << std::endl;
}
};
class Derived: public Base
{
public:
int x;
Derived(int _x):Base(_x),x(_x + 1){}
};
int main()
{
Derived d(3);
std::cout<< d.x << std::endl; //输出4
d.show(); //输出3
}
- 对于
std::cout<< d.x << std::endl;
,因为定义的是子类的对象,所以会优先查找子类独有的作用域,这里已经找到了x,所以不会再查找父类的作用域,因此输出的是4,如果子类里没有另行声明x成员变量,那么才会去查找父类的作用域。 - 对于
d.show();
,由于show的存在与父类的作用域里,所以使用的是父类作用域中的x。
子类中可以通过int GetBaseX() {return Base::x;}
访问父类的x。
成员函数被遮掩
class Base
{
public:
void CommonFunction(){cout << "Base::CommonFunction()" << endl;}
void virtual VirtualFunction(){cout << "Base::VirturalFunction()" << endl;}
void virtual VirtualFunction(int x){cout << "Base::VirtualFunction() With Parms" << endl;}
void virtual PureVirtualFunction() = 0;
};
class Derived: public Base
{
public:
void CommonFunction(){cout << "Derived::CommonFunction()" << endl;}
void virtual VirtualFunction(){cout << "Derived::VirturalFunction()" << endl;}
void virtual PureVirtualFunction(){cout << "Derived::PureVirtualFunction()" << endl;}
};
int main()
{
Derived d;
d.VirtualFunction(3); // ?
return 0;
}
上述程序会编译出错,理由如下:编译器先查找子类独有的域,一旦发现了完全相同的函数名(VirtualFunction),它就已经不再往父类中找了!在核查函数参数时,发现了子类的VirtualFunction没有带整型形参,所以直接报编译错了。
结论:子类重载(函数名相同但形参不一样)父类的成员函数,会将父类所有同名的函数都隐藏掉。(不管有没有virtual关键字,结论都成立)
访问父类里面的方法:
- 子类中使用
using Base::VirtualFunction;
,告诉编译器,把父类Base的VirtualFunction函数也纳入第一批查找范围里面。 - 子类中重写父类函数:
void virtual VirtualFunction(int x){Base::VirtualFunction(x)}; // 隐含inline
条款34:区分接口继承和实现继承(假定讨论的成员函数都是public的)
参考:链接
接口、实现和声明、定义是相关的,总结:
- 纯虚函数只继承接口;
- 虚函数既继承接口,也提供了一份默认实现;
- 普通函数既继承接口,也强制继承实现。
纯虚函数、虚函数和普通函数:
- 纯虚函数有一个“等于0”的声明,具体实现一般放在派生中(但基类也可以有具体实现)。所在的类(称之为虚基类)是不能定义对象的,派生类中仍然也可以不实现这个纯虚函数,交由派生类的派生类实现,总之直到有一个派生类将之实现,才可以由这个派生类定义出它的对象。
“基类中有纯虚函数的具体实现”的目的:告诉子类此函数必须实现(与虚函数不同),并且提供了此函数的默认实现(子类中可以通过类似BaseClass::PureVirtualFunction();
的方式调用此函数的默认实现)。 - 虚函数则必须有实现,否则会报链接错误。虚函数可以在基类和多个派生类中提供不同的版本,利用多态性质,在程序运行时动态决定执行哪一个版本的虚函数(机制是编译器生成的虚表)。virtual关键字在基类中必须显式指明,在派生类中不必指明,即使不写,也会被编译器认可为virtual函数,virtual函数存在的类可以定义实例对象。
- 普通函数所代表的意义是不变性凌驾与特异性,所以它绝不该在派生类中被重新定义。当需要重写时,使用虚函数或者纯虚函数。当需要重载时,条款33告诉我们——子类中避免重载父类的成员函数。
书上提倡尽量用纯虚函数去替代虚函数(避免子类忘记实现自己的成员函数),因为虚函数提供了一个默认的实现,如果派生类的想要的行为与这个虚函数不一致,而又恰好忘记去覆盖虚函数,就会出现问题。但纯虚函数不会,因为它从语法上限定派生类必须要去实现它,否则将无法定义派生类的对象。
下面是三类成员函数的应用:
class BaseClass
{
public:
void virtual PureVirtualFunction() = 0; // 纯虚函数
void virtual ImpureVirtualFunction(); // 虚函数
void CommonFunciton(); // 普通函数
};
void BaseClass::PureVirtualFunction()
{
cout << "Base PureVirtualFunction" << endl;
}
void BaseClass::ImpureVirtualFunction()
{
cout << "Base ImpureVirtualFunciton" << endl;
}
class DerivedClass1: public BaseClass
{
void PureVirtualFunction()
{
cout << "DerivedClass1 PureVirturalFunction Called" << endl;
}
};
class DerivedClass2: public BaseClass
{
void PureVirtualFunction()
{
cout << "DerivedClass2 PureVirturalFunction Called" << endl;
}
};
int main()
{
BaseClass *b1 = new DerivedClass1();
BaseClass *b2 = new DerivedClass2();
b1->PureVirtualFunction(); // 调用的是DerivedClass1版本的PureVirtualFunction
b2->PureVirtualFunction(); // 调用的是DerivedClass2版本析PureVirtualFunction
b1->BaseClass::PureVirtualFunction(); // 当然也可以调用BaseClass版本的PureVirtualFucntion
return 0;
}
条款35:考虑virtual函数以外的其他选择
参考:链接
virtual函数以外的其他选择:
- 将虚函数DoA放入private,然后在public中的函数A调用DoA。这样向外提供的接口就不是一个虚函数。NVI(Non-Virutal Interface)的一个流派主张所有的虚函数都是private的,将父类与子类都会使用的前置方法与后置方法单独作一个non-virtual的函数
【注】这里是将虚函数都置成了private的,但编译器生成的虚表指针则不是private的,否则会因private成员变量根本不被继承而无法实现多态。NVI的方式也不是绝对的,比如虚析构函数,它必须是public的,才能确保它的子类,以及子类的子类们能够顺利释放资源。 - 定义一个函数指针作为成员变量,这样就可以给父类和派生类的对象指定不同的函数。可以使用
tr1 : : function
来构建函数指针,tr1 : : function
构建的函数指针可以指向函数、函数对象、std::tr1::bind
构造的函数。 - 定义另一个类A的指针作为成员变量,然后将类A的对象传入父类B,将类A的派生类对象传入给B的子类。