Effective C++ Item34 的思考:区分接口继承和实现继承

本文探讨了C++中纯虚函数、非纯虚函数及非虚函数的区别,包括它们如何影响子类的行为,以及在实际编程中如何选择使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

我们有一个shape类,在该类中有三种不同类型的函数:

  1. class Shape {   
  2. public:   
  3.     virtual void draw() const = 0;             //纯虚函数  
  4.     virtual void error(const string& msg);     //普通虚函数  
  5.     int objectID() const;                      //普通函数  
  6. };   
  7. class Rectangle : public Shape {...};   
  8. 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种函数的差异,有助于明确子类想要继承的东西。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值