一、继承中接口的处理方式
- 作为类的设计者,对于基类的成员函数可以大致做下面三种方式的处理:
- ①纯虚函数:基类定义一个纯虚函数,然后让派生类去实现
- ②非纯虚的virtual虚函数:基类定义一个非纯虚的virtual虚函数,然后让派生类去重写覆盖(override)
- ③普通的成员函数:基类定义一个普通的成员函数,并且不希望派生类去隐藏
- 本文依次介绍上面这三种设计的原理。下面定义一个类,作为本文讲解的基础:
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 {};
二、纯虚函数
- 这是文章开始提到的第一种情况:派生类只继承基类的成员函数的接口(纯虚函数),派生类自己实现纯虚函数
- 纯虚函数的一些特征:
- ①拥有纯虚函数的类不能实例化
- ②拥有纯虚函数的类,其派生类必须实现该纯虚函数
- 在上面,我们的纯虚函数为:
class Shape {
public:
virtual void draw()const = 0; //纯虚函数
};
class Rectangle :public Shape {};
class Ellipse :public Shape {};
- 其中涉及纯虚函数的目的为:
- Shape是所有图形类的基类,其提供一个draw()的画图函数,但是由于其派生类(矩形、圆等)的画图方式都是不一样的,因此无法为draw()函数提供一种默认缺省行为,因此Shape将draw()定义为纯虚函数, 让其派生类去自动实现
- 演示案例:
class Shape {
public:
virtual void draw()const = 0;
};
class Rectangle :public Shape {
public:
virtual void draw()const {
std::cout << "Rectangle" << std::endl;
}
};
class Ellipse :public Shape {
public:
virtual void draw()const {
std::cout << "Ellipse" << std::endl;
}
};
int main()
{
//Shape *ps = new Shape; //错误,不能实例化
Shape *ps1 = new Rectangle;
Shape *ps2 = new Ellipse;
ps1->draw(); //调用Rectangle::draw()
ps2->draw(); //调用Ellipse::draw()
return 0;
}
- 纯虚函数虽然用途有限,但是它可以实现一种机制,为非纯虚的virtual虚函数提供更平常安全的缺省实现(见下面的介绍)
三、非纯虚的virtual虚函数
演示案例(介绍优点的)
- 先来看一个virtual函数的演示案例
- 假设某航天公司设计一个飞机继承体系,该公司现在只有A型和B型两种飞机,代码如下:
class Airport {}; //机场
class Airplane { //飞机的基类
public:
virtual void fly(const Airport& destination) {
//飞机飞往指定的目的地(默认行为)
}
};
//A、B两个派生类
class ModelA :public Airplane {};
class ModelB :public Airplane {};
- fly()函数被声明为virtual函数,因为A和B两个飞机具有相同的默认飞行行为,因此在Airplane类的fly()函数中定义这种默认飞行行为,然后让A和B继承。这样的好处是:
- 将所有性质搬到到base class中,然后让两个class继承
- 避免代码重复,并提升未来的强化能力,减缓长期维护所需的成本
演示案例(介绍以纯虚函数代替虚函数)
- 上面的A和B都有默认的飞行行为,因此在Airport的fly()函数中定义了这份默认行为
- 但是如果航天公司增加了一个C型的飞机,如下:
class ModelC :public Airplane {};
- 如果C型飞机可能不适合默认的飞行行为,那么可能就会误调用Airplane中的fly()函数,例如:
Airport PDX;
//C型飞机不具有fly()的行为,但是却调用了fly()
Airplane *pa = new ModelC;
pa->fly(PDX);
- 这是设计上的概念错误,我们可以修改代码,切断“virtual函数”和其“默认实现”之间的连接。代码如下:
class Airport {}; //机场
class Airplane {
public:
virtual void fly(const Airport& destination) = 0;
protected:
void defaultFly(const Airport& destination) {
//飞机飞往指定的目的地(默认行为)
}
};
class ModelA :public Airplane {
public:
virtual void fly(const Airport& destination) {
defaultFly(destination);
}
};
class ModelB :public Airplane {
public:
virtual void fly(const Airport& destination) {
defaultFly(destination);
}
};
class ModelC :public Airplane {
public:
virtual void fly(const Airport& destination) {
//C型飞机不可以使用默认飞行行为,因此定义自己的飞行方式
}
};
- 现在C型飞机,或者别的添加的飞机就不会意外继承默认的飞行行为了(因为我们将默认的飞行行为封装到一个defualtFly函数中了),自己可以在fly中定义飞行行为了
- 注意,在A和B的类的fly()函数中,对defaultFly()函数做了一个inline调用(见条款30,inline和virtual函数之间的交互关系)
演示案例(以纯虚函数代替虚函数的另一种实现)
- 上面我们将fly()接口和实现(defaultFly()函数)分开来实现,有些人可能会反对这样做,因为这样会因过度雷同的函数名称而引起class命名空间污染
- 如果不想将上述两个行为分开,那么可以为纯虚函数进行定义,在其中给出defaultFly()函数的相关内容。例如:
class Airport {}; //机场
class Airplane {
public:
//实现纯虚函数
virtual void fly(const Airport& destination) = 0 {
//飞机飞往指定的目的地(默认行为)
}
};
class ModelA :public Airplane {
public:
virtual void fly(const Airport& destination) {
Airplane::fly(destination);
}
};
class ModelB :public Airplane {
public:
virtual void fly(const Airport& destination) {
Airplane::fly(destination);
}
};
class ModelC :public Airplane {
public:
virtual void fly(const Airport& destination) {
//定义自己的飞行方式
}
};
- 这个设计实现的功能和上面的演示案例是一样的,只不过在派生类的fly()函数中用纯虚函数Airplane::fly替换了独立函数Airplane::defaultFly。这种合并行为丧失了“让两个函数享有不同保护级别”的机会:例如上面的defaultFly()函数从protected变为了public
四、普通的成员函数
class Shape {
public:
int objectID()const; //普通成员函数,不希望派生类隐藏
};
class Rectangle :public Shape {};
class Ellipse :public Shape {};
- 设置普通的成员函数的目的:
- 意味着基类不希望派生类去隐藏这个成员函数
- 实际上一个普通的成员函数所表现的不变性凌驾其特异性,因为它表示不论派生类变得多特特异化,它的行为都不可以改变
- 在上面的代码中:
- 每个Shape对象都有一个用来产生对象识别码的函数
- 此识别码总是采用相同计算方法,该方法有Shape::objectID的定义式决定,任何派生类都不应该尝试改变其行为
- 由于普通成员函数代表的意义是不变性凌驾特异性,所以它绝不该在派生类中被重新定义(这也是条款36所讨论的一个重点)
五、class设计常犯的两个错误
- “纯虚函数、非纯虚的virtual虚函数、普通的成员函数”之间的差异,使得指定你想要派生类继承的东西,针对于不同的函数,经验不足的class设计者最常犯的两个错误如下
第一个错误
- 第一个错误是将所有函数声明为“普通的成员函数”,这使得派生类米有多余空间进行特化工作
- non-virtual析构函数尤其会带来问题(见条款7)
- 当然,如果一个类不打算作为基类,那么将所有函数声明为“普通的成员函数”是可以的。但是如果该类会作为基类,那么可以适当的声明一些virtual函数(见条款7)
- 如果你当心virtual函数的成本,那么可以参阅80-20法则(也可参阅条款30):
- 这个法则为:一个典型的程序有80%的执行时间花费在20%的代码身上
- 这个法则意味着,平均而言你的函数调用中可以有80%是virtual而不冲击程序的大体效率。所以当你担心virtual函数的成本之前,先将精力放在那举足轻重的20%代码上,它才是真正的关键
第二个错误
- 第二个错误是将所有成员函数声明为virtual
- 有时候这样做是正确的,例如条款31的Interface classes。然而某些函数就是不该在派生类中被重新定义,因此你应该将那些函数声明为non-virtual的
六、总结
- 接口继承和实现继承不同。在public继承之下,derived classes总是继承base class的接口
- pure virtual函数只具体hiding接口继承
- 简朴的(非纯)impure virtaul函数具体指定接口继承及缺省实现继承
- non-virtual函数具体指定接口继承以及强制性实现继承