Effective C++ 第六章—— 继承与面向对象设计

Effective C++ 第六章—— 继承与面向对象设计

c++的OOP的特点:继承可以是单一继承或多重继承;每一个继承的链接可能是public、protected或private,也可是是virtual或non-virtual;成员函数可以是virtual或non-virtual的。

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

    公有继承(public inheritance)意味着一种“is-a”的关系。
    在C++领域中,在公有继承时,任何一个函数如果期望获得一个基类实参的话,都也愿意接受一个公有派生类对象。
    public继承主张,能够施行于base class对象上的每件事情,也可以施行于derived class对象身上。
    class之间的关系除了is-a、还有两种,分别为has-ais-implemented-in-terms-of(根据某物实现出)

请记住:

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


条款33——避免遮掩继承而来的名称

    C++的名称遮掩规则是指局部作用局和全局作用域名称相同时,在局部作用域内的同名变量会遮掩全局作用域的同名变量。
    derived class作用域被嵌套在base class对象中。
    在公有继承时,如果base class 和derived class中有同名函数,derived class的同名函数会遮掩掉base class的同名函数,例如:

class Base{
private:
	int x;
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();
	...
};

Dervied d;
int x;
d.mf1();  //ok,调用Derived::mf1
d.mf1(x);  //worry, Derived::mf1遮掩了Base::mf1
d.mf2();  //ok,调用Base::mf2
d.mf3();  //ok,调用Derived::mf3;
d.mf3(x); //worry,Derived::mf3遮掩了Base::mf3

    遮掩规则不论base class和derived class函数同名且有不同参数,是不是virtual或non-virtual,都适用。
    上述规则导致没有办法继承重载函数,然而有时候,我们希望继承重载函数。可以适用using声明式来解决:

class Base{
private:
	int x;
public:
	virtual void mf1 () =0;
	virtual void mf1(int);
	virtual void mf2();
	void mf3();
	void mf3(double);
	...
};
class Derived: public Base {
public:
	using Base::mf1;//让Base class中名为mf1和mf3的所有东西在Derived作用域内可见(并且public)
	using Base::mf3;
	virtual void mf1();
	void mf3();
	void mf4();
	...
};

Dervied d;
int x;
d.mf1();  //ok,调用Derived::mf1
d.mf1(x);  //ok,调用Base::mf1
d.mf2();  //ok,调用Base::mf2
d.mf3();  //ok,调用Derived::mf3;
d.mf3(x); //ok,调用了Base::mf3

    如果你继承并加上重载函数,而你又希望重新定义或覆写(推翻)其中的一部分,你必须为哪些原来会被遮掩的每个名称引入一个using声明式,否则某些你希望继承的名称会被遮掩。
    如果我们只想唯一继承上述例子中base class中的mf1的无参数版本,使用using声明式是没有用的,需要从新定义一个转交函数

class Base{
private:
	int x;
public:
	virtual void mf1 () =0;
	virtual void mf1(int);
	...
};
class Derived: public Base {
public:
	virtual void mf1()//转交函数,定义为inline
	{ Base::mf1(); }
	...
};
Dervied d;
int x;
d.mf1();  //ok,调用Derived::mf1
d.mf1(x);  //worry,Base::mf1被Derived::mf1遮掩了

    inline转交函数的目的是将继承而得的名称汇入到derived class作用域内。
   当继承结合template时又会出现其他的问题。

请记住:

  • derived class内的名称会遮掩base class内的名称。在public继承下从没有人希望如此。
  • 为了让被遮掩的名称重见天日,可以使用using声明式或者转交函数。


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

    public继承由两部分组成:函数接口继承和函数实现继承。
    一个例子用于区分继承时继承接口、继承实现或两者都继承但希望能覆写继承的实现,希望同时继承但不允许覆写:

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 {...};
  • 成员函数的接口总是会被继承。
  • 声明一个pure virtual函数的目的是让derived class只继承函数接口;pure virtual函数一般不想要逃提供实现,如果提供了,调用他的唯一途径是明确指出其class的名称。derived class必须提供实现。
Shape* ps = new Shape;//worry,Shape是抽象基类
Shape* ps1 = new Rectangle;//ok
ps1->draw();//Rectangle的draw
Shape* ps2 = new Ellipse;
ps1->Shape::draw();//调用Shape::draw
ps2->Shape::draw();//调用Shape::draw
  • 声明impure virtual函数(非纯虚函数,但是是虚函数)的目的是让derived class继承该函数的接口和缺省实现。可以让不同的派生类接口一样,但是行为不一样。derived class会提供不同的实现覆盖他或者如果需要与基类相同的实现这需要定义一个同名函数调用基类的函数;derived class会覆盖base class中的实现。
  • 声明non-virtual 函数的目的是为了让derived classes继承函数的接口及一份强制性实现。 声明为non-virtual 函数是希望在derived classes绝不会重新定义该函数,本身行为不改变。

请记住:

  • 接口继承和实现继承不同。在public继承之下,dervied class总是继承base class的接口。
  • pure virtual 函数只具体指定接口继承。
  • 简朴的(非纯)impure virtual 函数具体指定接口的继承及缺省实现继承。
  • non-virtual 函数具体指定接口的继承以及强制性实现继承。


条款35——考虑virtual函数以外的其他选择

由Non-Virtual Interface手法实现Template 模式
    Non-Virtual Interface(NVI)手法:通过public non-virtual 成员函数间接调用private virtual 函数

class GameCharacter{ 
public:
	int healthValue() const  //这里声明为inline只是为了方便展示,并不是真的inline,下同
	{
		...
		int retVal = doHealthValue();
		...
		return retVal;
	}
	...
private:
	virtual int doHealthValue() const
	{...}
};

    通常称这个non-virtual函数为virtual函数的外覆器(wrapper)。
    NVI手法涉及在derived class内重新定义private virtual函数。

由Function Pointers 实现Strategy模式
    将算法与类型分开,上述例子我们可以给每一个人物构造函数接受一个指针,该指针指向一个计算健康指数的函数,例子如下:

class GameCharcter;//前置声明
int defaultHealthCalc(const GameCharcter& gc);
class GameCharcter{
public:
	typedef int (*HealthCalcFunc)(const GameCharacter&);
	explicit GameCharcter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf)
	{}
	int hueathValue() const
	{return healthFunc(*this);}
	...
private:
	HealthCalcFunc healthFunc(*this);
};

    运用函数指针替换virtual函数,其优点是每个对象可各自拥有自己的方法以及在运行期可以改变自己的方法,缺点是如果方法需要访问class的non-public部分需要降低封装性实现。
由tr1::function完成Strategy模式
可调用对象:函数,函数指针,lambda表达式,bind创建的对象,以及重载了函数调用符的类。

class GameCharcter;//前置声明
int defaultHealthCalc(const GameCharcter& gc);
class GameCharcter{
public:
	//HealthCalcFunc可以是任何可调用对象,可被调用并接受任何兼容GameCharacter之物,返回任何兼容于int的东西
	typedef std::tr1::function<int (const GameCharcter&)> HealthCalFunc;
	explicit GameCharcter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf)
	{}
	int hueathValue() const
	{return healthFunc(*this);}
	...
private:
	HealthCalcFunc healthFunc(*this);
};

与之前相同,这里的tr1::function对象相当于一个指向函数的泛化指针。
古典的Strtegy模式

class GameCharacter;
class HealthCalcFunc{
public:
	virtual int calc(const GameCharater& gc) const
	{...}
	...
};
HealthCalcFunc defaultHealthCalc;
class GameCharacter{
public:
	explicit GameCharater(HealthCalcFunc* phcf = &defaultHealthCalc): pHealthCalc(phcf)
	{}
	int healthValue() const { return pHealthCalc->calc(*this); }
	...
private:
	HealthCalcFunc* pHealthCalc;
};

请记住:

  • virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。
  • 将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class的non-public成员。
  • tr1::function 对象的行为就像一般函数指针。这样的对象可以接纳“与给定之目标签名式(target signature)兼容”的所有可调用物。

条款36——绝不重新定义继承而来的non-virtual函数

    non-virtual函数是静态绑定的,当声明一个类型为指向基类的指针p时,p调用的non-virtual函数都是基类的版本,即使p指向的是一个派生类对象。
    virtual函数是动态绑定,当一个指针不管是类型是指向基类的指针还是指向派生类的指针,其调用virtual函数的版本是根据其所指向的对象来的。
    基类指针可以指向基类对象和派生类对象,但派生类指针只能指向派生类对象,如果想用派生类指针指向基类指针必须进行转型操作;指向基类对象的基类指针可以调用基类中的所有公有成员,指向派生类对象的基类指针可以调用继承来的基类公有成员;当基类和派生类定义了同名函数且为virtual时,通过基类指针调用该函数时到底调用哪一个版本取决于该基类指针指向的是基类对象还是派生类对象,指向基类对象则调用基类中定义的版本。当基类和派生类定义了同名函数且为non-virtual时,派生类对象中的版本会遮掩基类的版本。

请记住:

  • 绝对不要重新定义继承而来的non-virtual函数。


条款37——绝不重新定义继承而来的缺省参数值

    缺省参数,就是在声明函数的某个参数的时候为之指定一个默认值,在调用该函数的时候如果采用该默认值,你就无须指定该参数。带缺省值的参数必须放在参数表的最后面。
    静态类型(static type): 它在程序中被声明的类型。
    动态类型(dynamic type):目前所指对象的类型,动态类型表现出一个对象将会有什么行为。

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;
	//请注意,以上这么写组当客户以对象调用此函数时一定要指定参数因为静态绑定下这个函数
	//不从base class继承缺省参数值。
	//若以指针或引用调用此函数,可以不指定参数,因为动态绑定下这个函数会从其base继承缺省参数值。
};
Shape* ps; //静态类型为Shape*
Shape* pc = new Circle;//静态类型为Shape*
Shape* pr = new Rectangle;//静态类型为Shape*
ps = pc;//ps的动态类型如今为Circle*
ps = pr;//ps的动态类型如今为Rectangle*

    virtual函数系动态绑定而来,意思是调用一个virtual函数时,究竟调用哪一版本的函数实现代码,取决于发出调用的那个对象的动态类型。

pc->draw(Shape::Red);//调用的是Circle::draw(Shape::Red)
ps->draw(Shape::Red);//调用的是Rectangle::draw(Shape::Red)

    virtual函数是动态绑定,而缺省参数值是静态绑定的。这意味着你可能在调用一个定义域derived class内的virtual函数时使用的是base class中所指定的缺省参数值。pr->draw();这里本来希望默认值为Green,但实际上使用的却是base中指定的默认参数值Red;
    为了解决上述问题,可以采用NVI手法:令base class 内的一个public non-virtual 函数调用private virtual函数,private virtual函数可以被重新定义。这里让public non-virtual函数指定缺省参数值,而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 Circle: public Shape {
public:
	...
private:
	virtual void doDraw(ShapeColor color) const;
};

请记住:

  • 绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值是静态绑定的,而virtual函数是动态绑定的(virtual函数是你唯一需要覆写的东西)。


条款38——通过复合塑模出has-a或根据某物实现出(Model “has-a” or “is-implemented-in-terms-of” through composition)

    复合(composition):某种类型的对象含有它种类型的对象。复合又被称为包含、聚合、内嵌等等。其含义是“has-a”或“is-implemented-in-terms-of(根据某物实现出)”。

请记住:

  • 复合(composition)的意义与public继承完全不同;
  • 在应用域(application domain),复合意味着has-a(有一个);在实现域(implementation domian),复合意味着“is-implemented-in-terms-of(根据某物实现出)”。


条款39——明智而审慎地使用private继承

    private 继承的两条规则:

  • class之间的继承关系是private的话,编译器不会自动将一个derived class对象转换为一个base class对象
  • 由private base class 继承而来的所有成员,在derived class中都会变成private属性,即使他们在base class中是protected或public;
        private继承意味着is-implemented-in-terms-of(根据某物实现出)。其意义与复合一样,我们应当尽量采用复合,必要时(当有protected 成员或virtual函数牵扯进来时)才使用private继承。
        EBO(empty base optimization,空白基类最优化),EBO只在单一继承下才可行。这种情况下才有可能使用private继承一个空类,以达到EBO。

请记住:

  • Private继承意味着is-implemented-in-terms-of(根据某物实现出)。它通常比复合的级别低。但是当derived class需要访问 protected base class的成员,或需要重新定义继承而来的virtual函数是,这么设计是合理的。
  • 和复合不同,private继承可以造成empty base 最优化。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。


条款40——明智而审慎地使用多重继承

    多重继承(multiple inheritance; MI)。使用MI可能导致程序从一个以上的base class继承相同名称(如函数,typedef),这也会导致更多的歧义。为了解决歧义问题,你必须明确指出你调用的是哪一个base class内的函数。

class BorrowableItem{
public:
	void CheckOut();
	...
};
class ElectronicGadget{
private:
	void CheckOut();
	...
};
class MP3Plaryer: public BorrowableItem, public ElectronicGadget
{...};
MP3Plaryer mp;
mp.CheckOut();//这里会出现歧义不知道调用哪一个baseclass中的CheckOut()函数;
mp.BorrowableItem::CheckOut();//指明你调用的是哪一个base class内的函数,这里是BorrowableItem这一个基类

    多重继承中可能碰到这种情况,继承一个以上的基类,这些基类又有更高级的同一个基类,即“钻石型多重继承”

class File {...};
class InputFile: public File {...};
class OutputFile: public File {...};
class IOFile:public InputFile
			 public InputFile
{...};
//这是如果File有某个成员变量,IOFile就会有两份该成员变量,这是错误的

    为了解决上述问题,令带有此数据的class为virtual base class,令所有直接继承自它的class采用“virtual继承”。

```cpp
class File {...};
class InputFile: virtual public File {...};//virtual继承
class OutputFile: virtual public File {...};//virtual继承
class IOFile:public InputFile
			 public InputFile
{...};

    关于virtual base class的忠告:尽量不要使用virtual bases,平常请使用non-virtual 继承;如果必须使用virtual base class,则尽可能避免在其中放置数据。

请记住:

  • 多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承的需要。
  • virtual继承会增加大小、速度、初始化(及赋值)复杂度等等的成本。如果base virtual class不带任何数据,将是最具有实用价值的情况。
  • 多重继承的确有正当用途。其中一个情节涉及“public 继承某个Interface class”和“private 继承某个协助实现的class”的两相组合的情况。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值