我们有一个shape类,在该类中有三种不同类型的函数:
- class Shape {
- public:
- virtual void draw() const = 0; //纯虚函数
- virtual void error(const string& msg); //普通虚函数
- int objectID() const; //普通函数
- };
- class Rectangle : public Shape {...};
- class Ellipse : public Shape {...};
本文从这三种不同函数出发,说明接口继承和实现继承的区别与联系。
1. 纯虚函数:
class Shape {public:
virtual void draw() const = 0; //纯虚函数
};
纯虚函数有两个突出特性:
1) 任何继承含有纯虚函数的子类都必须重新声明并实现该函数,否则该子类也如同基类不能实例化对象。
2)通常在抽象基类中的纯虚函数没有定义,但是我们也看为纯虚函数提供实现,而C++的编译器不会报错,只是在调用函数时需使用类名。
我们从代码的实践中考察纯虚函数的特性:
首先,对于一个含有纯虚函数的类,不能实例化对象:当我们的Shape类如最前面所声明时,在main函数中实例化Shape类的对象,编译器直接报错:
当我们有子类Rectangle公有继承了含有纯虚函数的抽象基类Shape时,我们在子类中没有重新声明和实现纯虚函数draw时,在main函数中声明一个Rectangle的对象,编译器报错:
当我们在基类中给出了纯虚函数的定义时,可以通过类名类调用对应的纯虚函数,代码如下:
class Shape {
public:
virtual void draw() const = 0; //纯虚函数
};
void Shape::draw() const {
cout<< "Shape draw" << endl;
}
class Rectangle : public Shape {
void draw() const {
cout << "Rectangle draw" << endl;
}
};
class Ellipse : public Shape {
void draw() const {//在子类中重新实现纯虚函数时,需要在函数声明后加上const关键字,否则程序报错
cout << "Ellipse draw" << endl;
}
};
int main()
{
Shape* ps1 = new Rectangle;
ps1->draw();
Shape* ps2 = new Ellipse;
ps2->draw();
ps1->Shape::draw();
ps2->Shape::draw();
return 0;
}
程序的执行结果如下:
2. 非纯虚函数:
class Shape {
public:
virtual void error(const std::string& msg); //非纯虚函数
};
非纯虚函数的意义在于让子类继承该函数的接口和默认实现。在Shape类中的该接口说明,每个继承该基类的子类都必须支持一个“遇到错误可调用”的函数,每个子类可自行处理错误;在不想针对错误做出特殊处理时,可退回到Shape基类中的错误处理函数进行默认的错误处理操作。
这势必带来方便,对于不想提供错误处理的类也可直接调用基类的默认错误处理函数。但这样的设计也可能带来危险。我们看下面的场景:机场的A、B类型的飞机以相同的方式飞行,利用非纯虚函数的特点,设计如下的代码:
class Airplane {
public:
virtual void fly(const string& Airportname);
};
void Airplane::fly(const string& Airportname) {
cout << "fly to destination " <<Airportname<< endl;
}
class ModelA :public Airplane {
};
class ModelB :public Airplane {
};
int main()
{
Airplane* a = new ModelA;
Airplane* b = new ModelB;
a->fly("A");
b->fly("B");
return 0;
}
当A、B两种类型的飞机拥有相同的飞行方式时,这样的设计凸显共同性质,避免代码重复,可以算是很不错的设计方案。
但是当有C型飞机到来,且C型飞机的飞行方式与A、B飞机的飞行方式完全不同,我们需要为C型飞机另外添加一个类,并重新定义fly函数。但由于业务匆忙或粗心原因,代码最终实现中忘记在C的类代码中重新实现fly函数,导致重大的飞行灾难。
问题出现在哪里呢?不在于Airplane提供的fly函数的默认实现,在于ModleC未明确说明就被强制继承了默认实现。为了解决这个问题,有两种解决方案:
方案一:
将fly函数定义为纯虚函数,这样,在子类中必须给出fly纯虚函数的重新声明和定义,于是,编译器一再叮嘱C要给自己一个不同于基类的飞行方式。
A、B类型的飞机飞行方式一样,为避免代码重复,另外定义函数defaultFly实现统一的飞行方式,在子类的fly函数中调用即可。
class Airplane {
public:
virtual void fly(const string& Airportname) = 0; //接口
void defaultFly(const string& Airportname); //默认实现
};
protected:
void Airplane::defaultFly(const string& Airportname) {
cout << "fly to destination " <<Airportname<< endl;
}
class ModelA :public Airplane {
void fly(const string& Airportname){
Airplane::defaultFly("A"); //调用基类的默认实现函数
}
};
class ModelB :public Airplane {
void fly(const string& Airportname) {
Airplane::defaultFly("A"); //调用基类的默认实现函数
}
};
class ModelC :public Airplane {
void fly(const string& Airportname) { //C 飞机自行实现飞行模式
cout << "fly to destitation " << Airportname << " By Model C" << endl;
}
};
int main()
{
Airplane* a = new ModelA;
Airplane* b = new ModelB;
Airplane* c = new ModelC;
a->fly("A");
b->fly("B");
c->fly("C");
return 0;
}
方案一解决上述的问题,但是以不同的函数分别提供接口和缺省实现,可能因过度雷同的函数名称而引起class命名空间污染的问题,为了解决接口和实现分离的目标,可以借用纯虚函数的特性,以纯虚函数提供接口,并提供相对应的实现,方案二如下:
class Airplane {
public:
virtual void fly(const string& Airportname) = 0;//接口
};
void Airplane::fly(const string& Airportname) { //提供纯虚函数的默认实现
cout << "fly to destination " <<Airportname<< endl;
}
class ModelA :public Airplane {
void fly(const string& Airportname){
Airplane::fly("A"); //调用基类的默认实现函数
}
};
class ModelB :public Airplane {
void fly(const string& Airportname) {
Airplane::fly("B"); //调用基类的默认实现函数
}
};
class ModelC :public Airplane {
void fly(const string& Airportname) { //C 飞机自行实现飞行模式
cout << "fly to destitation " << Airportname << " By Model C" << endl;
}
};
int main()
{
Airplane* a = new ModelA;
Airplane* b = new ModelB;
Airplane* c = new ModelC;
a->fly("A");
b->fly("B");
c->fly("C");
return 0;
}
3.非虚函数:
声明非虚函数的目的在于使子类继承函数的接口和一份强制性实现,任何子类都不应该尝试修改该函数的行为。
那么问题来了,如果修改了,会有什么样事情发生呢?我们还是通过代码运行看结果,代码如下:
class Airplane {
public:
int objectID()const;
};
int Airplane::objectID()const {
return 1;
}
class ModelA :public Airplane {
public:
int objectID()const {
return 2;
}
};
int main()
{
ModelA a;
Airplane* a1 = &a;
cout << a1->objectID() << endl;
ModelA* a2 = &a;
cout << a2->objectID() << endl;
return 0;
}
我们在子类ModelA中重新实现了普通的函数objectID,在main函数中分别通过基类和子类的指针指向同一个子类对象,再通过指针调用该函数,却得到了不同的输出结果如下:
两者都是通过指向对象a的指针调用成员函数objectID,却得到了不同的结果。
原因在于普通函数的绑定是静态绑定,在编译期就决定了,a1声明为指向Airplane的指针,调用的永远是Airplane定义的版本;a2声明为指向ModelA的指针,调用的就是ModleA定义的版本。所以为了避免上述问题,绝不应该在子类中重新定义非虚函数。
4.总结:
1.纯虚函数只继承接口
2.非纯虚函数继承接口和一份默认的实现
3.普通函数继承接口和强制性的实现
明确3种函数的差异,有助于明确子类想要继承的东西。