第三十二条款
public继承意味着is-a关系,并且基类的每一个功能都应该在派生类上发挥同样效果,否则is-a关系不成立,如基类为矩形,派生为正方形,在代码上就不属于is-a关系。
第三十三条款
不要遮掩基类的成员(public继承来的)
在使用public继承的时候,结合using声明和重载(override)来替换不要的版本并保留需要的其他版本(using声明放在public)。
至于private继承,想要保留,某个版本可以使用转接函数,即在同名的派生类函数里调用基类函数。(不使用using是因为这些函数已经在派生类可见了,但是继承在派生类的private域之中,而且using会把所有的重载版本引入。注意这里指的是private继承的基类的public中的函数,基类中private的函数对派生类是不可见的。)
Derived Class{
public:
同名函数()
{
基类::同名函数()
}
}
如上,完成转接。
条款三十四
区分不同的继承方式,如只继承接口,继承接口与缺省实现,继承接口和强制实现
只继承接口->纯虚函数
若需要纯虚函数的缺省实现,则使用Base::name()在派生类中调用纯虚函数定义的实现,但这么做就意味着这个缺省为公有。
继承接口和缺省实现->一般virtual。
意味着希望派生类拥有出现错误时可调用的函数,即缺省实现。问题是当派生类继承了接口,但是使用默认的实现会出现错误时。
一个解决方法是定义一个protected的成员函数来实现缺省功能,如果派生类需要缺省的行为,则调用这个函数(而且这个函数还可以inline调用),同时定义一个纯虚函数要求派生类必须实现接口。问题是增加了一个名字。
继承接口与强制实现->一般成员函数
并不意味着不能重写,只是在语意上表示这是一个不该更改的函数,派生类也应该使用一样的实现。
条款三十五
考虑virtual以外的方法
NVI方法(non-virtual-interface)实现template method(模板方法模式)
NVI方法(non-virtual-interface)实现template method(模板方法模式)
namespace NVI {
class GameCharacter {
public:
GameCharacter() :health(100) {}
GameCharacter(int h) :health(h) {}
int healthCalc();
private:
int health;
virtual int calcHealth(int num);
};
int GameCharacter::healthCalc()
{
//prepare
int healthPoint = calcHealth(health);//执行实际功能
//after
return healthPoint;
}//定义非虚拟的成员函数来取代virtual函数,隐式inline
int GameCharacter::calcHealth(int num)
{
std::cout << "gamecharacter" << std::endl;
return num;
}//私有的virtual函数决定实际的功能
class EvilBadGuy : public GameCharacter {
public:
EvilBadGuy() :GameCharacter() {}
EvilBadGuy(int h) :GameCharacter(h) {}
private:
virtual int calcHealth(int num) override;
};
int EvilBadGuy::calcHealth(int num)
{
std::cout << "evilbadguy" << std::endl;
return num * 2;
}//不同的计算策略
}
/*
//执行代码 结果为 gamecharacter 100 evilbadguy 200
GameCharacter g;
EvilBadGuy e;
cout << g.healthCalc() << endl;
cout << e.healthCalc() << endl;
*/
NVI的前提是virtual函数不能是public。
Function Pointer实现strategy(策略模式)
namespace strategy {
int defaultCalc(int num);
class GameCharacter {
public:
using calHealthType = int(int);//定义函数指针类型
GameCharacter() :calMethod(defaultCalc), health(100) {}
GameCharacter(calHealthType *p, int hp) :calMethod(p), health(hp) {}
int CalcHealth() const;
private:
calHealthType * calMethod;
int health;
};
int defaultCalc(int num)
{
std::cout << "defaultCalc" << std::endl;
return num;
}
int GameCharacter::CalcHealth() const
{
//...准备
int healthPoint = calMethod(health);//使用函数指针完成工作
//...善后
return healthPoint;
}
int EvilBadGuyHealthCal(int num)
{
std::cout << "EvilBadGuyCalc" << std::endl;
return num * 2;
}
}
//以下使用function生成的可调用对象实现同样的strategy模式
namespace funStrategy {
int defaultCalc(int num);
class GameCharacter
{
public:
using calcType = std::function<int(int)>;//定义可调用对象类型
GameCharacter() :calcFun(defaultCalc), health(100) {}
GameCharacter(calcType cal, int hp) :calcFun(cal), health(hp) {}
virtual ~GameCharacter() {}
int CalcHealth();
int MemHealthCal(int num)
{
std::cout << "MemHealthCal" << std::endl;
std::cout << "this->health: " << this->health << std::endl;
//返回的这个值是调用者的health * 3,但是这个函数并不是调用者的
//故this->Health将会不同于调用者的this->health
return num * 3;
}
private:
calcType calcFun;//存储可调用对象
int health;
};
int GameCharacter::CalcHealth()
{
//...某些准备
int healthPoint = calcFun(health);//实际使用int(int)形式的可调用对象来计算
//...善后
return healthPoint;
}
int defaultCalc(int num)
{
std::cout << "defaultCalc" << std::endl;
return num;
}
float defaultEBGcal(short num);//注意返回类型和参数类型,short,float分别可以提升/降级到int
class EvilBadGuy :public GameCharacter
{
public:
EvilBadGuy() :GameCharacter(defaultEBGcal,100) {}//non-member函数
//EvilBadGuy() :GameCharacter(evilHealthCal2, 100) {}//函数对象(结构体)
EvilBadGuy(calcType cal, int hp) :GameCharacter(cal, hp) {}
virtual ~EvilBadGuy() override{};
struct EvilHealthCal2 {
int operator()(int num)
{
std::cout << "EvilHealthCal2" << std::endl;
return num / 2;
}
}evilHealthCal2;//其实应该放private,但是这样外部就用不了了
private:
};
float defaultEBGcal(short num)
{
std::cout << "defaultEBGcal" << std::endl;
return num * 2;
}
}
//测试:
int main()
{
funStrategy::GameCharacter a;
funStrategy::EvilBadGuy eb;//默认构造,使用外部函数
funStrategy::EvilBadGuy el(
[/*捕获列表*/](int num) {cout << "lambda" << endl;return num * 5; }
, 200);//lambda表达式
funStrategy::EvilBadGuy e(std::bind(&funStrategy::GameCharacter::MemHealthCal, el, placeholders::_1),
100);//可调用对象,绑定成员函数,注意这里需要绑定一个实际的类型,一个能调用这个类的类型
funStrategy::EvilBadGuy e1(e.evilHealthCal2, 100);//函数对象
cout << a.CalcHealth() << endl;
cout << eb.CalcHealth() << endl;
cout << el.CalcHealth() << endl;
cout << e.CalcHealth() << endl;
cout << e1.CalcHealth() << endl;
}
/*
结果
defaultCalc
100
defaultEBGcal
200
lambda
1000
MemHealthCal
this->health: 200 //这里200的原因是使用labmda的对象health为200
//而bind绑定的this正是这个对象
300 //但调用函数时传入参数num的是e的health = 100
EvilHealthCal2
50
*/
古典实现strategy模式
namespace classic {
class HealthCalcFunc {
public:
int calcHealth(int num);
}defaultCalcFunc;//计算策略类
int HealthCalcFunc::calcHealth(int num)
{
return num;
}
class GameCharacter
{
public:
GameCharacter() : healthCalFunc(&defaultCalcFunc), health(100) {}
GameCharacter(HealthCalcFunc *hcf, int hp) :healthCalFunc(hcf), health(hp) {}
~GameCharacter();
int CalcHealth();
private:
HealthCalcFunc *healthCalFunc;
int health;
};
int GameCharacter::CalcHealth()
{
healthCalFunc->calcHealth(health);//使用策略类方法计算
}
}
结论
不要总是使用virtual函数,其实有很多选择,可以带来更好的可控性和封装性。
NVI:使用non-virtual函数来调用virtual函数以在调用前后进行检测、控制。
Function Pointer/function 实现的strategy模式:把具体的计算转移到了类外的可调用对象,优点是可以随时进行方法的切换(即使是运行时),而且也可以在计算前后进行一定控制。
classic的strategy模式:一眼就可以看出来是strategy模式,使用不同的类和类指针控制方法,与NVI相似的是在non-virtual中调用了virtual函数,不过NVI调用的是本类的private virtual函数,方法切换通过继承本类,经典的方法是调用方法类中的public virtual函数,切换通过继承方法类。
条款三十六
绝不重新定义继承来的non-virtual函数
这样会使同一个对象通过不同的指针/引用调用的non-member函数出现不同。比如用基类的引用调用一个派生类对象的non-virtual函数,调用的会是基类的函数,用派生类引用存放则调用派生类版本,如果需要这样的功能,那么就用virtual函数来代替,否则就不用重新定义non-member。
条款三十七
绝对不要重新定义继承来的函数的缺省值,因为缺省值是绑定静态类型的。
class Shape {
public:
virtual void draw(string color = "red")const { cout << color << endl; }
};
class Rectangle : public Shape {
public:
virtual void draw(string color = "green") const override { cout << color << endl; }
};
int main()
{
Shape shape;
Rectangle rectangle;
Shape *s = &shape;
Shape *s2 = &rectangle;
Rectangle *R = &rectangle;
shape.draw();
rectangle.draw();
s->draw();//基类指针基类对象
s2->draw();//基类指针派生对象
R->draw();//派生指针派生对象
}
结果:
red 基类对象
green 派生类对象
red 基类指针指向基类
red 指向派生类
green 派生类指针指向派生类
结论
缺省参数由静态类型决定(即使是虚函数),但virtual函数版本却又由动态类型决定,所以不要更改virtual的缺省函数。
一定要使用缺省的时候定义一个非虚拟的接口函数,把虚拟函数放到private中,派生类修改private函数即可。
class Shape {
public:
void draw(string color = "red")const { doDraw(color); }
private:
virtual void doDraw(string color) const { cout << color << endl;; }
};
class Rectangle : public Shape {
public:
virtual void doDraw(string color) const override { cout << color << endl; }
};
这样保证每个派生类的缺省参数就是”red”。
条款三十八
通过复合来实现"has - a"关系,或者根据某物来实现当前类以实现“has-a”关系(private继承/通过某类的对象/指针/引用)
复合!=public继承
复合表示的是一个类中有其他类对象
比如Person有(has-a)name birthday address等类
而根据某物来实现也是一个类中有其他类,不同的是,这个类的技能依赖于其他类
比如定义一个自定的空间需求更小的Set
namespace clause38 {
template<typename T>
class Set {
public:
bool member(const T&) const;
void insert(const T&);
void remove(const T&);
std::size_t size();
private:
std::list<T> rep;//使用list实现set
};
template<typename T>
bool Set<T>::member(const T& mem) const
{
//使用泛型算法
return std::find(rep.begin(),rep.end(),mem) != rep.end();
}
template<typename T>
void Set<T>::insert(const T& mem)
{
//不存在则加入(set是不能有同样成员的)
if (!member(mem))rep.push_back(mem);
}
template<typename T>
void Set<T>::remove(const T& mem)
{
typename std::list<T>::iterator it
= std::find(rep.begin(), rep.end(), mem);//获取迭代器
if (it != rep.end())//对象在内
rep.erase(it);//删除
}
template<typename T>
std::size_t Set<T>::size() { return rep.size(); }
}
条款三十九
谨慎使用private继承
private继承意味着被继承的成员进入派生类的private域,private继承则意味着无法发生派生类->基类的转换,也就不能把私有继承的“派生类”地址传递给一个“基类“引用/指针。这也说明private不是用来做is-a关系的。
private继承通常发生在一个类需要使用另一个类的protected成员,但是又和另一个类不具有“is-a”关系时发生
比如需要使用Timer记录Person类的调用次数等,则让Person继承private Timer(事实上你应该不会这么做)。然后Timer中的virtual就可以在Person中重写(即使是Timer的virtual private成员函数也是可以被重写的,NVI就是基于这一点实现的),而这些函数又可以调用Person的成员
同样的,可以使用经典的strategy模式实现这一点。定义一个计数类(策略类)继承public Timer,然后在Person类private中放一个计数类的指针就好了。
这样做的好处有二:
一是防止Person的派生类重写Timer的virtual函数,也就是说派生类不需要计数时,使用经典的strategy可以阻止可以阻止派生类继承计数体制,若需要计数体制只要为派生类也加上计数类指针即可,相当于实现了java中的final。(问题是现在c++也有final了,所以加上final也可以将解决这个问题,但是继承的方式任然让派生类拥有了计数器的某些public继承到基类的函数,这些函数派生类可能根本不需要)
而是可以减少编译的依赖性(针对指针的情况),只放一个指针的做法不需要了解Timer的具体定义,而是知道Timer的声明即可,而继承则需要全部的定义才行。
EBO:
一个完全没有任何数据的类的对象空间至少为1,因为编译器会强制放一个char进去,如果这个类的对象被放进其他类,则为了“齐位”,则可能会扩充成更大的类型(但并不代表其中真的有个这种类型的变量);所以为了使用某个EBO的方法,应当使用private继承,这使得派生类的大小完全不会改变,因为无数据的EBO不是独立的对象,所以编译器就不再需要像其中加入某个不存在的成员来占位,这是private继承的完美用地。但是这样的继承要保证EBO的确不占任何空间,也就是也不能含有virtual函数或者virtual的基类。
条款四十
谨慎使用多重继承。
多重继承首先要提到的就是有多少个基类的部分,派生类的两个直接基类可能又有同样的间接基类,那么就会让派生类拥有n个间接基类部分,那么直接调用间接基类的成员时就会出现多种版本,来自每个直接基类的间接基类部分。要使用特定版本则需要加上类作用域限定符。故一般来说我们不想要多个,故应该使用virtual继承,但virtual继承的问题是会减慢速度加大体积,并且派生类需要初始化直接基类。故大部分的场合可以的话就不要使用virtual继承,或者你可以使用一个没有数据成员的base class,这样继承就不会增加任何空间问题,调用的方法也不会用到不同版本的成员,更不用在派生类初始化在哪里的基类。
合理的多重继承情况是public继承接口、private继承实现。正好做了他们该做的事。由public继承一个纯粹只有函数的基类,在当前的派生类实现,再private继承一个用以实现接口的private类,这样public继承的接口只有声明,private继承的类也被封装在当前派生类中,用户只需要接口的声明即可以使用,很好得区分了声明与定义,解决了编译上的麻烦。
条款四十一
注意隐式接口和编译器多态
编译器多态->使用不同的template参数会调用不同的template函数,这是在编译期决定的,实际上是Template衍生出了一系列的重载函数,不同参数调用不同重载的版本,这是template实现多态的方法。而运行期多态则是派生类基类指针的动态绑定决定的不同virtual函数
隐式接口
template参数需要实现template函数中的操作/操作符。但实际上不是硬性的,比如operator>,template的参数可能并不实现>操作,然后template参数可以隐式转换成另一端的参数,那么template参数便也可以实现这一版本的函数,虽然它也可能根本不支持其中的任何操作。这些操作视为参数要实现的隐式接口,与显示不同的是显示接口有具体的声明定义,隐式就不一定了,可以说隐式接口意味的是参数类型可以完成操作组成的表达式,因此借助其他操作数的操作与转换也ok。