目录
一、确保public继承塑模出is-a关系
二、避免隐藏继承而来的名称
三、区分接口继承和实现继承
四、考虑virtual函数以外的其他选择
五、不要重新定义继承而来的non-virtual函数
六、不要重新定义继承而来的缺省参数
七、尽量使用复合塑模出has-a
总结
一、确保public继承塑模出is-a关系
还是先以一个简单的例子来说明
class Person
{
//…………
};
class Student : public Person
{
//…………
};
根据生活经验我们知道,每一个学生都是人,但是并不是每一个人是学生,这就是所谓的is-a的关系。我们可以预期,对人可以成立的每一件事对学生也是成立的,学生是一种特殊的人。
所以,任何函数如果参数是Person类型(或者是Person类型的指针或者引用),那么也可以将一个student类型传过去。
这个观点只对public继承才会成立,private继承并不属于is-a的关系。
public继承和is-a的等价关系听起来十分的简单,但是十分容易犯错误。
再举一个简单的例子:
企鹅是鸟,符合is-a的关系
鸟会飞,这也是一个事实
按照我们之前所说,企鹅是鸟,但是企鹅不会飞,如果按照上述的继承体系,我们就要好好研究一下,怎么处理企鹅会飞这一问题
有两种做法:
1、令程序在运行期间发生错误
2、在编译期间直接报错,"企鹅不会飞"
我们一般采用的是第二种做法,我们应该防止无效代码通过编译
二、避免隐藏继承而来的名称
我们还要回顾一下C++的相关概念
所谓的隐藏是指隐藏名称,至于函数的参数和返回值并不重要
接下来再回顾一下作用域
派生类的作用域是内嵌于基类的作用域中
class Base
{
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
};
class Derived : public Base
{
public:
virtual void mf1();
void mf3();
void mf4();
};
我们分别调用这些函数
Derived d;
int x;
d.mf1(); // 没问题调用Derived::mf1
d.mf1(x); // 有问题Derived::mf1隐藏了Base::mf1
d.mf2(); // 没有问题调用Base::mf2
d.mf3(); // 没有问题调用Derived::mf3
d.mf3(x); // 有问题Derived::mf3隐藏了Base::mf3
如果想要调用到Base类中的成员函数,需要我们手动指明作用域
三、区分接口继承和实现继承
身为class的设计者,有时希望派生类只继承成员函数的接口,有时又希望派生类同时继承函数的接口和实现,但是又希望能够重写它们所继承的实现,有时候又希望派生类同时继承函数的接口和实现,并且不允许重写任何东西
class Shape
{
public:
virtual void draw() const = 0;
virtual void error(const std::string& msg);
int objectID() const;
};
class Rectangle : public Shape
{
};
class Ellipse : public Shape
{
};
我们看这个简单的继承关系
Shape是一个抽象类,因为它有纯虚函数draw,所以它不能够实例化出对象,它只能够实例化它的派生类,但是他还是强烈的影响了与它继承的所有派生类,因为它的全部派生类都继承了它的纯虚函数,如果派生类没有重写该虚函数,导致它的派生类依然是抽象类。
1、成员函数的接口总是被继承的,所谓接口就是值它的函数声明
2、纯虚函数只具体指定接口继承
3、非纯虚函数具体指定接口继承及缺省实现继承
4、普通成员函数具体指定接口继承以及强制性实现继承
但是普通成员函数同时指定函数声明和函数缺省行为,却可能造成危险
假如有一家航空公司,它现在有两种飞机A,B,两者都以相同的方式飞行
下面是简单的代码实现
class AirPlane
{
public:
virtual void fly(const std::string &destination);
};
void AirPlane::fly(const std::string &destination)
{
std::cout << " fly "
<< "青岛"
<< " to " << destination << std::endl;
}
class ModelA : public AirPlane
{
public:
virtual void fly(const std::string &destination)
{
AirPlane::fly(destination);
}
};
class ModelB : public AirPlane
{
public:
virtual void fly(const std::string &destination)
{
AirPlane::fly(destination);
}
};
为了表明不同飞机的飞行方式,所以fly函数加上了virtual声明,代表继承接口和缺省实现继承
这是一个典型的面向对象的设计,两个class共享同一份fly函数,所有的飞机共性都放到了Base类中,避免了代码重复,减少长期维护的成本。
现在该公司添加了C型飞机,该公司程序员增加了class ModeC,但是忘记重新定义了C的fly函数
class ModelC : public AirPlane
{
public:
//…………
};
这将造成重大灾难,因为C机型飞行方式根本与前两种飞行方式完全不同
为了避免这样的问题,我们可以将fly函数声明为纯虚函数,只继承它的接口,不继承它的实现,这样因为没有重写虚函数导致C还是抽象类而报错
这里将基类改成抽象类
class AirPlane
{
public:
virtual void fly(const std::string &destination) = 0;
};
编译时就会报错,避免了无效代码编译通过
class ModelC : public AirPlane
{
public:
virtual void fly(const std::string &destination)
{
std::cout << "Model C "
<< " fly "
<< " 青岛 "
<< "to" << destination << std::endl;
}
};
这里我们重写C机型的fly函数,而其他机型的fly函数什么都不用做
class AirPlane
{
public:
virtual void fly(const std::string &destination) = 0;
};
void AirPlane::fly(const std::string &destination)
{
std::cout << " fly "
<< "青岛"
<< " to " << destination << std::endl;
}
class ModelA : public AirPlane
{
public:
virtual void fly(const std::string &destination)
{
AirPlane::fly(destination);
}
};
class ModelB : public AirPlane
{
public:
virtual void fly(const std::string &destination)
{
AirPlane::fly(destination);
}
};
class ModelC : public AirPlane
{
public:
virtual void fly(const std::string &destination)
{
std::cout << "Model C "
<< " fly "
<< " 青岛 "
<< "to" << destination << std::endl;
}
};
四、考虑virtual函数以外的其他选择
假如你在写一个FPS游戏,游戏中的人物有血量限制,需要为任务角色类添加一个成员函数来计算血量。因为每一个人物会有不同的方式计算血量,所以让这个成员函数声明为virtual
class Character
{
public:
virtual int healthValue() const;
//…………
};
但是我们还可以选择更好的实现方法
class Character
{
public:
int healthValue() const
{
int retVal = 0;
//…………
retVal = doHealthValue();
//…………
return retVal;
}
private:
virtual int doHealthValue() const
{
//………………
}
};
这种通过非virtual成员函数间接调用private virtual函数称为"non-virtual interface"(NVI)手法
它是Template Method设计模式的一种独特表现形式,这个非virtual函数称为外覆器。
外覆器能够确保在一个virtual函数被调用之前设定好适当场景,并在调用结束之后清理场景
事前工作包括锁定互斥器,记录日志等等
NVI模式下,没有必要让virtual函数一定是private的,可以灵活变通,某些class继承体系要求派生类在virtual函数的实现内必须调用其基类的兄弟,为了让这种操作合法,virtual函数必须声明为protected
另一种做法是使用function来完成Strategy模式
我们可以让每个角色的构造函数接受一个function,function中是计算血量的函数(仿函数,lambda表达式),然后我们直接调用function就能够根据不同的角色来计算血量
int defaultHealthValue(const Character& c);
class Character
{
typedef std::function<int(const Character&)> HealthCalcFunc;
public:
Character(HealthCalcFunc hfc = defaultHealthValue)
:_healthFunc(hfc)
{}
int healthValue() const
{
return _healthFunc(*this);
}
private:
HealthCalcFunc _healthFunc;
};
五、不要重新定义继承而来的non-virtual函数
class A
{
public:
void func() const
{
std::cout << "Hello A" << std::endl;
}
};
class B : public A
{
};
B b;
A *pa = &b;
pa->func();//调用A::func
B* pb = &b;
pb->func();//调用B::func
原因是普通成员函数是静态绑定的,pa定义为A*类型,所以通过它调用的普通成员函数永远是指向A所定义的版本
virtual成员函数是动态绑定的,如果func是virtual函数,无论是通过pa调用func还是通过pb调用func,他都会调用B::func
简单说:
先看函数类型,如果是普通成员函数那么就看指针类型,如果是virtual函数那么就看指针实际所指的空间类型
为了避免出现上面的情况,在任何情况下都不应该重新定义一个继承而来的普通成员函数
六、不要重新定义继承而来的缺省参数
根据前面所说,不要重新定义继承而来的普通成员函数
所以本条成立的前提是:继承一个带有缺省值的virtual函数
我们还是看一个小例子
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函数时,一定要传参数
因为静态绑定下这个函数并没有从基类继承缺省值
如果以指针或者引用去调用draw,可以不指定参数
因为动态绑定下这个函数会从基类继承缺省参数
解决办法还是NVI手法
基类内的一个普通成员函数调用private virtual函数
class Shape
{
public:
enum ShapeColor
{
RED,
GREEN,
BLUE
};
void draw(ShapeColor color = RED) const
{
doDraw(color);
}
private:
virtual void doDraw(ShapeColor color) const = 0;
};
class Rectangle : public Shape
{
public:
void draw(ShapeColor color = GREEN) const
{
doDraw(color);
}
private:
virtual void doDraw(ShapeColor color) const
{
//…………
}
};
七、尽量使用复合塑模出has-a
总结
以上就是今天要讲的内容,本文仅仅回顾了C++继承和面向对象的细节