6 继承和面向对象设计
32 确定你的public继承塑膜出is-a关系
public继承意味着“is-a”,适用于基类身上的每一件事情也一定适用于派生类,因为每一个派生类对象也是一个基类的对象。
33 避免遮掩继承而来的名称
1 派生类内的名称会遮掩基类内的名称,基类里面重载的所有同名称函数(包括纯虚函数,虚函数,普通成员函数)会被派生类中重写的函数所掩盖,在派生类中无法访问基类中的那些函数。
class Base{
private:
int x;
public:
virtual void f1() = 0;
virtual void f1(int);
virtual void f2();
void f3();
void f3(double);
...
};
class Derived : public Base{
public:
virtual void f1();
void f3();
void f4();
...
};
因此基类中所以名为f1()和f3()的函数都被派生类中的f1()和f3()遮掩掉了,
Derived d;
int x;
...
d.f1(); //调用Derived::f1
d.f1(x); //错误,基类的f1(int)被派生类同名函数遮掩
d.f2(); //调用Base::f2
d.f3(); //调用Derived::f3
d.f3(x); //错误,基类的f3(double)被派生类同名函数遮掩
2 为了符合公有继承的is-a关系(即派生类必须继承基类的所有成员函数,可以重写虚函数),可以使用using声明或者转交函数来让函数不被遮掩。
using声明会令继承下来的某给定名称之所有同名函数在派生类中都可见。
class Base{
private:
int x;
public:
virtual void f1() = 0;
virtual void f1(int);
virtual void f2();
void f3();
void f3(double);
...
};
class Derived : public Base{
public:
using Base::f1;
using Base::f3;
virtual void f1();
void f3();
void f4();
...
};
但是要在合适的权限下进行using声明,由于继承下来的函数本身会成为公有函数,是public权限,所以要在public权限下进行using声明。
转交函数就是在派生类中调用基类的函数。
class Base{
private:
int x;
public:
virtual void f1() = 0;
virtual void f1(int);
...
};
class Derived : public Base{
public:
virtual void f1(){ Base::f1(); } //声明为inline函数
...
};
...
Derived d;
int x;
d.f1(); //调用Derived::f1
d.f1(x);//错误,基类的f1(int)被派生类同名函数遮掩
34 区分接口继承和实现继承
1 基类中的三种函数对于派生类有不同的性质
- 纯虚函数:派生类继承接口
- 虚函数:派生类继承接口和默认实现
- 普通函数:派生类继承函数的接口和实现,并且不允许复写(复写了也不具有多态性质)
2 基类的纯虚函数可以实现,但是对于派生类来说,其无法继承基类纯虚函数的实现,只能继承其接口并自行实现(如果不实现,无论基类是否实现此纯虚函数,那么派生类也是抽象类,不能生成对象)。派生类如果想调用基类的纯虚函数的实现,可以通过基类名::纯虚函数名的方法进行调用。
一般情况下派生类会重写基类的虚函数实现,如果不重写就继承基类的实现,如果在少部分情况下不想因此让派生类继承基类的实现产生意外的话,可以使用纯虚函数+实现的方式,这样派生类如果要继承基类实现就要显示调用。避免意外的发生。
实现了部分代码:
#include <iostream>
using namespace std;
class Base
{
public:
Base(int _a) : a(_a){ }
virtual void f1() = 0;
virtual void f2();
void f3();
private:
int a;
};
void Base::f1() //纯虚函数也是可以定义的,但是纯虚函数的定义,派生类并不会继承,而是要通过基类名::方法名来调用
{
cout << "this is base pure virtual f1\n";
}
void Base::f2()
{
cout << "this is base virtual f2\n";
}
void Base::f3()
{
cout << "this is base f3\n";
}
class Derived : public Base
{
public:
Derived(int _a, int _d) : Base(_a), d(_d){ }
//virtual void f1();//纯虚函数的继承
void f1();
//virtual void f2();//虚函数的继承
void f2();
//void f3(); //重写基类普通成员函数,不具有多态性质
void f4();
private:
int d;
};
void Derived::f1()
{
cout << "this is derived f1\n";
}
void Derived::f2()
{
cout << "this is derived virtual f2\n";
}
/*
void Derived::f3()
{
cout << "this is derived f3\n";
}
*/
void Derived::f4()
{
Base::f1();
}
int main()
{
Derived d(1, 2);
d.f1();
d.f2();
d.f3();
d.f4();
return 0;
}
35 考虑virtual函数以外的其他选择
本篇主要考虑虚函数的替代方法,有以下两个方法。
1 Non-Virtual Interface手法实现Template Method模式
使用私有虚函数(私有虚函数仍然是可以被继承的),并且通过一个公有的非虚函数去调用这个虚函数。这称为NVI手法,是Template Method模式的一个特殊表现手法。
Template Method模式:
定义一个操作中的算法的骨架。而将一些步骤延迟到子类中,模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
其关键是将通用算法(逻辑)封装在抽象基类中,并将不同的算法细节放到子类中实现。
这个公有的成员函数就叫做虚函数的外覆器(wrapper)。
但有时也可以让虚函数是protected或者是public(例如具有多态性质的基类析构函数)。
2 Function Pointers实现Strategy模式
Strategy模式的一个简单应用是在内部定义一个私有的函数指针,构造函数中传入这样的指针,并予以运行。这样的话可以在不同的对象中有着不同的处理函数,并且处理函数也可以在运行期变化。
但是函数指针用处比较小,可以使用C++11中的std::function来代替函数指针的作用,这个类的对象可以是所有兼容模版定义类型的可调用对象,包括函数指针,成员函数指针,仿函数,或者是lambda表达式(C++11)。所谓兼容,就是这个可调用物的参数可以被隐式转换为const A &
,其返回类型可以被隐式转换为int
。
在传入成员函数的时候要注意成员函数是要隐式传入一个this指针的,这里需要用std::bind函数指定这个this指针
typedef funtion<int(const A &)> Calfunc;
这里的模版类型就是int
为返回值,参数类型为const A &
。
传统的Strategy模式中维护的是类的指针,在类的内部维护一个计算函数所在的类的对象,然后通过类中的虚函数进行计算,这个虚函数还能通过继承和重写实现不同的功能。
36 绝不重新定义继承而来的non-virtual函数
非虚成员函数,如果在子类中重写,那么将不具有多态性。函数的调用取决于指针/引用的类型而不是对象的类型。基于这个原则,基类的析构函数一定要声明为虚函数。
37 绝不重新定义继承而来的缺省参数值
静态类型:对象在声明时采用的类型。是在编译期确定的。
动态类型:目前所指对象的类型。是在运行期决定的。对象的动态类型可以更改,但是静态类型无法更改。
静态绑定:绑定的是对象的静态类型,某特性(比如函数)依赖于对象的静态类型,发生在编译期。
动态绑定:绑定的是对象的动态类型,某特性(比如函数)依赖于对象的动态类型,发生在运行期。
在C++的继承中,虚函数采用的是动态绑定,但参数是静态绑定。
class A{
enum shapeColor{Red, Green, Blue};
virtual void draw(shapeColor = Red) const = 0;
};
class B : public A{
virtual void draw(shapeColor ) const;
};
以上的程序,当以基类指针调用派生类对象时,可以不指定参数,但是以派生类指针调用派生类对象时,则必须指定参数,这就是由于静态绑定的参数是不能被派生类继承的。
所以当基类和派生类的默认参数不同时,也会出现上述的问题,即基类指针和派生类指针默认参数不同的问题。
一个解决方案就是NVI,采用成员函数调用私有虚函数的方法,在成员函数上指定默认参数,由于普通的成员函数是不允许被重写的,所以派生类若不想拥有一样的默认参数就自行定义,否则直接使用基类的成员函数即可。
38 通过符合塑模出has-a或“根据某物实现出”
复合是类型之间的一种关系,当某种类型的对象内含它种类型的对象,便是这种关系。
class A{};
class B{A a;};
39 明智而审慎地使用private继承
1 如果类之间的继承关系是私有继承(或者保护继承),那么编译器不会自动将一个派生类对象转换为一个基类对象,即不具有多态性,基类的指针,不可以指向派生类。原因是因为从私有基类继承来的所有成员,无论在基类中是什么属性,在派生类中都会成为私有属性。
2 私有继承体现出的关系是“根据基类构建出一个派生类”,这类似于复合关系,所以很明显,私有继承可以换一种书写方式,就是复合。对于派生类来说,基类只能通过基类的公有和保护函数去访问,就像是一个存在它之中的类对象,它只能通过公有或者保护函数去处理基类的私有成员,这样其实就是用基类来作为派生类的一部分,构造了一个派生类。
- 复合的好处是如果派生类不想继承基类中的部分成员函数,则可以使用复合的方式。将复合的类对象作为私有成员,这样派生类的派生类就无法访问基类中的部分成员函数。
- 私有派生占用更小的空间,如果存在一个空类(在C++中空类的大小为1,编译器会默默安插一个char到空对象内部),并在别的类中有对象的声明,则会增加所在类的内存大小,并且还会产生对齐现象,增大了类的内存占用,而如果是继承的方法,这个空类将不会有内存占用(空白基类最优化(EBO))。(STL中常用的空基类有unary_function和binary_function)。
40 明智而审慎地使用多重继承
1 多重继承会导致派生类继承不同基类中的同名函数,这样在调用的时候会产生二义性,所以需要用类名::函数名的方式指定调用哪一个基类里的函数。
2 菱形继承
class A{
private:
int a;
};
class B : public A{
private:
int b;
};
class C : public A{
private:
int c;
};
class D : public B, public C{
private:
int d;
};
菱形继承最常见的问题,就是D中会出现两个A的副本,现在A的大小是4,很明显B,C的大小都是4+4=8,这个时候如果D同时继承B,C,内部就维护了两个A的副本,现在D的大小是sizeof(a) + sizeof(b) + sizeof(a) + sizeof(c) + sizeof(d) = 4 + 4 + 4 + 4 + 4 = 20。
若想解决重复副本的问题,可以使用B,C虚拟继承A,这样A是B,C的虚基类,B,C中维护指向A的虚表指针和A的成员,这样解决了重复副本,代价是引入了多余的虚表指针,增加了内存,增长了程序运行的时间。虚继承下D的大小为sizeof(a) + sizeof(ptrtoA) + sizeof(b) + sizeof(a) + sizeof(ptrtoA) + sizeof(c) + sizeof(d) = 4 + 4 + 4 + 4 + 4 + 4 + 4 = 28。
3 多重继承适用的途径是如果某个类需要继承某个接口类,并且也需要一个类协助实现某些功能,则可以使用公有继承继承接口类,使用私有继承继承协助的类。