【effective c++读书笔记】【第6章】继承与面向对象设计(1)

条款32:确定你的public继承塑模出is-a关系

1、以C++进行面向对象编程,最重要一个规则是:public inheritance(公开继承)意味着“is-a”(是一种)的关系。如果令class D(“Derived”)以public形式继承class B(“Base”),便告诉C++编译器(及代码读者)说,每一个类型为D的对象同时也是一个类型为B的对象反之不成立。B比D表现出更一般化的概念,而D比B表现出更特殊化的概念。B对象可派上用场的任何地方,D对象一样可以派上用场。

2、public继承和is-a之间的等价关系听起来很简单,但有时直觉会误导你。比如企鹅是一种鸟,鸟会飞,如果我们用C++描述:

class Bird{
public:
	virtual void fly();
	...
};
class Penguin : public Bird{
	……
};

上述继承体系中说企鹅会飞,但我们知道,企鹅不会飞。这个问题的原因是语言(英语)不严谨。当我们说鸟会飞时,我们表达的意思是一般的鸟都会飞。我们还应该承认一个事实:有些鸟不会飞。这样可以塑造以下继承关系:

class Bird{
	...//没有声明fly函数
};
class FlyingBird : public Bird{
public:
	vitual void fly();
	...
};
class Penguin : public Bird{
	...//没有声明fly函数
};

还有一个方法来处理所有鸟都会飞,企鹅是鸟,但企鹅不会飞这个问题,就是为企鹅类重新定义fly函数,令它在产生一个运行期错误:

void error(const std::string& msg);//定义于另外某处
class Penguin : public Bird {
public:
	virtual void fly(){ error("Attemp to make a penguin fly"); }
	...
};

这种方法和前面的解决方法不同,这里不说“企鹅不会飞“,而是说“企鹅会飞,但尝试那么做是一种错误”。两种解决方法之间有什么差异?从错误被侦测出来的时间来看,第一种解决方法企鹅不会飞这个限制条件在编译期强制实施;第二个解决方法企鹅尝试飞行,是一种错误只有在运行期才能检测出来。条款18说过,好的接口可以防止无效的代码通过编译,因此应该采用第一种解决方法。

请记住:

  • “public继承”意味is-a。适用于base classes身上的每一件事情一定也使用于derived classes身上,因为每一个derived classes对象也都是一个base classes对象。 

条款33:避免掩盖继承而来的名称

1、C++的名称遮掩规则所做的唯一事情就是:遮掩名称。至于名称是否是相同或不同的类型,并不重要。

int x;
void someFunc(){
	double x;
	std::cin >> x;
}

上面函数内的变量x遮掩了global变量x。

2、对于继承的父类中的函数,如果在子类中有同名的函数,则父类中的同名函数都将被隐藏。

例子:

#include<iostream>
using namespace std;

class Base{
public:
	virtual void mf1() = 0;
	virtual void mf1(int x){ cout << "Base::mf1(int x)" << endl; }
	virtual void mf2(){ cout << "Base::mf2()" << endl; }
	void mf3(){ cout << "Base::mf3()" << endl; }
	void mf3(double d){ cout << "Base::mf3(double d)" << endl; }
};
class Derived : public Base{
public:
	virtual void mf1(){ cout << "Derived::mf1()" << endl; }
	void mf3(){ cout << "Derived::mf3()" << endl; }
};
int main(){
	Derived d;
	int x=0;
	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

	system("pause");
	return 0;
}

3、为了防止父类中的同名函数被隐藏,我们可以在子类中使用用using声明式来引入父类

的成员函数。

例子:

#include<iostream>
using namespace std;

class Base{
public:
	virtual void mf1() = 0;
	virtual void mf1(int x){ cout << "Base::mf1(int x)" << endl; }
	virtual void mf2(){ cout << "Base::mf2()" << endl; }
	void mf3(){ cout << "Base::mf3()" << endl; }
	void mf3(double d){ cout << "Base::mf3(double d)" << endl; }
};
class Derived : public Base{
public:
	using Base::mf1;
	using Base::mf3;
	virtual void mf1(){ cout << "Derived::mf1()" << endl; }
	void mf3(){ cout << "Derived::mf3()" << endl; }
};
int main(){
	Derived d;
	int x=0;
	d.mf1();//调用Derived::mf1
	d.mf1(x);//调用Base::mf1(int x)
	d.mf2();//调用Base::mf2
	d.mf3();//调用Derived::mf3
	d.mf3(x);//调用Base::mf3(double d)

	system("pause");
	return 0;
}

上述例子说明如果你继承基类并加上重载函数,而你又希望重新定义或覆写其中一部分,那么必须为那些原本会遮掩的每个名称引入一个using声明式,否则某些你希望继承的名称会被遮掩。

3、using声明式会令继承而来的某给定名称的所有同名函数在继承类中都可见,如果我只想使用基类的mf3的某个特定版本,该怎么办?可以使用转交函数的技术:
#include<iostream>
using namespace std;

class Base{
public:
	virtual void mf1() = 0;
	virtual void mf1(int x){ cout << "Base::mf1(int x)" << endl; }
	virtual void mf2(){ cout << "Base::mf2()" << endl; }
	void mf3(){ cout << "Base::mf3()" << endl; }
	void mf3(double d){ cout << "Base::mf3(double d)" << endl; }
};
class Derived : private Base{
public:
	virtual void mf1(){ cout << "Derived::mf1()" << endl; }
	void mf3(){ Base::mf3(); }
};
int main(){
	Derived d;
	double dd=0;
	d.mf3();//调用Derived::mf3
	//d.mf3(dd);//错误,Base::mf3(double d)被遮掩了

	system("pause");
	return 0;
}

请记住:

  • drived classes内的名称会遮盖带base class内的名称。在public继承下从来没有人希望如此。
  • 为了让被遮掩的名称那个再见天日,可以使用using声明或转交函数。

条款34:区分接口继承和实现继承

1、身为类的设计者,有时候希望派生类只继承基类成员函数的接口,有时候希望派生类同时继承基类的接口和实现,但又希望能够覆写他们继承的实现,有时希望派生类老老实实的继承这些函数的接口和实现,并且不允许覆写任何东西。这三种不同的策略是通过声明基类成员函数为纯虚函数、虚函数和非虚函数来实现的。

例子:
//Shape.h
#ifndef SHAPE_H
#define SHAPE_H

#include<iostream>
#include<string>

class Shape{
public:
	virtual void draw() const = 0;
	virtual void error(const std::string& msg);
	void objectName(const std::string& name);
};
void Shape::error(const std::string& msg){
	std::cout << "error information:" << msg << std::endl;
}
void Shape::objectName(const std::string& name){
	std::cout << "objectName:" << name << std::endl;
}

#endif

//Rectangle.h
#ifndef RECTANGLE_H
#define RECTANGLE_H

#include"Shape.h"

class Rectangle:public Shape{
public:
	void draw() const;
	void error(const std::string& msg);
};
void Rectangle::draw() const{
	std::cout << "draw rectangle" << std::endl;
}
void Rectangle::error(const std::string& msg){
	std::cout << "rectangle error:" << msg << std::endl;
}

#endif

//Ellipse.h
#ifndef ELLIPSE_H
#define ELLIPSE_H

#include"Shape.h"

class Ellipse :public Shape{
public:
	void draw() const;
};
void Ellipse::draw() const{
	std::cout << "draw ellipse" << std::endl;
}

#endif

//main.cpp
#include"Rectangle.h"
#include"Ellipse.h"
#include<iostream>

int main(){
	Rectangle rectangle;
	rectangle.draw();
	rectangle.error("rectangle");
	rectangle.objectName("rectangle");

	Ellipse ellispe;
	ellispe.draw();
	ellispe.error("ellispe");
	ellispe.objectName("ellispe");

	system("pause");
	return 0;
}

考虑virtual voiddraw() const = 0;,这个纯虚函数必须被继承类重新声明,并且在抽象类中没有定义。因此声明一个纯虚函数的目的是为了让继承类只继承函数接口。

考虑virtual voiderror(const std::string&msg);,这个虚函数在基类中提供了一份实现代码,继承类可以覆写它,若继承类不覆写它,则调用基类的缺省实现代码。因此声明非纯虚函数的目的是让继承类继承该函数的接口和缺省实现。

考虑voidobjectName(const std::string&name);,这是个非虚函数,表明它并不打算在继承类中有不同的行为,继承类不管多么特异化,它的行为都不可以改变。因此声明非虚函数的目的是为了令继承类继承函数的接口及一份强制性实现。

2、可以为上述1中例子的纯虚函数提供定义,然后调用时明确指出其class名称,不过这项性质用途有限。

//Shape.h
#ifndef SHAPE_H
#define SHAPE_H

#include<iostream>
#include<string>

class Shape{
public:
	virtual void draw() const = 0;
	virtual void error(const std::string& msg);
	void objectName(const std::string& name);
};
void Shape::draw() const{
	std::cout << "Shape draw" << std::endl;
}
void Shape::error(const std::string& msg){
	std::cout << "error information:" << msg << std::endl;
}
void Shape::objectName(const std::string& name){
	std::cout << "objectName:" << name << std::endl;
}

#endif

//Rectangle.h
#ifndef RECTANGLE_H
#define RECTANGLE_H

#include"Shape.h"

class Rectangle:public Shape{
public:
	void draw() const;
	void error(const std::string& msg);
};
void Rectangle::draw() const{
	std::cout << "draw rectangle" << std::endl;
}
void Rectangle::error(const std::string& msg){
	std::cout << "rectangle error:" << msg << std::endl;
}

#endif

//Ellipse.h
#ifndef ELLIPSE_H
#define ELLIPSE_H

#include"Shape.h"

class Ellipse :public Shape{
public:
	void draw() const;
};
void Ellipse::draw() const{
	std::cout << "draw ellipse" << std::endl;
}

#endif

//main.cpp
#include"Rectangle.h"
#include"Ellipse.h"
#include<iostream>

int main(){
	Rectangle rectangle;
	rectangle.Shape::draw(); //注意,通过指出其class名称调用
	rectangle.draw();
	rectangle.error("rectangle");
	rectangle.objectName("rectangle");

	Ellipse ellispe;
	ellispe.draw();
	ellispe.error("ellispe");
	ellispe.objectName("ellispe");

	system("pause");
	return 0;
}

3、上述1中例子的非纯虚函数同时指定函数声明和函数缺省行为,有可能导致危险。应该让继承类在说出我要的情况下才继承,而在没说明的情况下不要继承缺省实现。下面是解决上述问题的一种做法:

//Shape.h
#ifndef SHAPE_H
#define SHAPE_H

#include<iostream>
#include<string>

class Shape{
public:
	virtual void draw() const = 0;
	virtual void error(const std::string& msg) = 0;
	void objectName(const std::string& name);
protected:
	void defaultError(const std::string& msg);
};

void Shape::defaultError(const std::string& msg){
	std::cout << "default error information:" << msg << std::endl;
}
void Shape::objectName(const std::string& name){
	std::cout << "objectName:" << name << std::endl;
}

#endif

//Rectangle.h
#ifndef RECTANGLE_H
#define RECTANGLE_H

#include"Shape.h"

class Rectangle:public Shape{
public:
	void draw() const;
	void error(const std::string& msg);
};
void Rectangle::draw() const{
	std::cout << "draw rectangle" << std::endl;
}
void Rectangle::error(const std::string& msg){//不可能继承基类error的实现代码了
	std::cout << "rectangle error:" << msg << std::endl;
}

#endif

//Ellipse.h
#ifndef ELLIPSE_H
#define ELLIPSE_H

#include"Shape.h"

class Ellipse :public Shape{
public:
	void draw() const;
	void error(const std::string& msg);
};
void Ellipse::draw() const{
	std::cout << "draw ellipse" << std::endl;
}
void Ellipse::error(const std::string& msg){
	defaultError(msg);
}

#endif

//main.cpp
#include"Rectangle.h"
#include"Ellipse.h"
#include<iostream>

int main(){
	Rectangle rectangle;
	rectangle.draw();
	rectangle.error("rectangle");
	rectangle.objectName("rectangle");

	Ellipse ellispe;
	ellispe.draw();
	ellispe.error("ellispe");
	ellispe.objectName("ellispe");

	system("pause");
	return 0;
}

首先将error设为纯虚函数,并设定defaultError函数,那么在派生类的error中,如果我们要使用defaultError,必须指明,否则必须自己重新定义error函数。

4、有人反对以不同的函数分别提供接口和缺省实现。他们关系因过度雷同的函数名称而导致类命名空间污染问题,可以用以下方法解决:

//Shape.h
#ifndef SHAPE_H
#define SHAPE_H

#include<iostream>
#include<string>

class Shape{
public:
	virtual void draw() const = 0;
	virtual void error(const std::string& msg) = 0;
	void objectName(const std::string& name);
};

void Shape::error(const std::string& msg){
	std::cout << "error information:" << msg << std::endl;
}
void Shape::objectName(const std::string& name){
	std::cout << "objectName:" << name << std::endl;
}

#endif

//Rectangle.h
#ifndef RECTANGLE_H
#define RECTANGLE_H

#include"Shape.h"

class Rectangle:public Shape{
public:
	void draw() const;
	void error(const std::string& msg);
};
void Rectangle::draw() const{
	std::cout << "draw rectangle" << std::endl;
}
void Rectangle::error(const std::string& msg){
	std::cout << "rectangle error:" << msg << std::endl;
}

#endif

//Ellipse.h
#ifndef ELLIPSE_H
#define ELLIPSE_H

#include"Shape.h"

class Ellipse :public Shape{
public:
	void draw() const;
	void error(const std::string& msg);
};
void Ellipse::draw() const{
	std::cout << "draw ellipse" << std::endl;
}
void Ellipse::error(const std::string& msg){
	Shape::error(msg);
}

#endif

//main.cpp
#include"Rectangle.h"
#include"Ellipse.h"
#include<iostream>

int main(){
	Rectangle rectangle;
	rectangle.draw();
	rectangle.error("rectangle");
	rectangle.objectName("rectangle");

	Ellipse ellispe;
	ellispe.draw();
	ellispe.error("ellispe");
	ellispe.objectName("ellispe");

	system("pause");
	return 0;
}

此时,error仍为纯虚函数,但给出了它的实现。如果想调用默认动作,必须显式指明使用的是基类中的error

请记住:

  • 接口继承和实现继承不同。在pubilc继承下,derived classes总是继承Base class 的接口。
  • pure virtual函数只具体指定接口继承。
  • impure virtual 函数具体指定接口继承及缺省实现继承。
  • non-virtual函数具体指定接口继承以及强制性实现继承。

你可能感兴趣的:(【effective c++读书笔记】【第6章】继承与面向对象设计(1))