Effective C++第6章 继承与面向对象设计(条款36-40)

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


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

由于条款36,所以这个问题归结到继承virtual函数,不应该该重新定义这个函数的参数值,原因是virtual函数是动态绑定的,而缺省值却是静态绑定。对象的静态类型,就是指它在声明时被采用的类型。动态类型是指“目前所指对象的类型”,动态类型表现出一个对象会有什么行为。动态类型可以在执行过程中改变。

如以下继承体系:

//以下有缺省参数值调用时可不指定参数
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:
	//客户端如果调用下面这个函数,有两种情况:1 以对象调用,需指名参数值,
	//因为对象调用是静态绑定并不会从基类继承缺省参数值 2 以指针或reference
	//调用,可以不指定参数值,因为动态绑定下这个函数会从其基类继承缺省参数值
	virtual void draw(ShapeColor color) const;
};

一些函数调用情况:

//以下静态类型都是Shape*
Shape* ps;                //动态:未确定,未指向任何对象
Shape* pc = new Circle;   //动态类型是Circle, 
Shape* pr = new Rectangle;//动态类型是Rectangle

//动态类型在程序执行过程中可以改变
ps = pc;
ps = pr;

pc->draw(Shape::Red)  //Circle::Draw(Shape::Red)
pr->draw(Shape::Red)  //Rectange::Draw(Shape::Red)

//因为Reactangle的draw有缺省参数值,所以这个调用正确,但是因为pr的静态值是
//Shape*,而缺省参数是静态调用的,所以这个的默认参数不是Rectangle中的Green,
//而是Shape中draw的Red,结果造成这个函数基类与继承类各出一半力,奇怪的行为
pr->draw();

以上这个情况不止局限于指针的情况,即使把指针换成reference问题仍然存在。重点在于draw是个virtual函数,而它有个缺省参数值在derived class中重定义了!C++之所以这样实现,是为了运行期的效率,如果缺省值也是动态绑定,则会比目前编译期决定的机制更慢且更复杂。

如果你想使用缺省参数值,这个情况就是发生了virtual函数表现出了你所想要的行为却遭遇麻烦,聪明的做法就是考虑virtual的缺省设计。参考35的virtual的替代策略,可以采用NVI手法:令base class内一个public non-virtual函数调用private virtual函数,后者可被继承类重新定义。而non-virtual函数提供缺省参数值,而private virtual负责真正的工作,如下:

//NVI手法实现缺省参数值,防止虚函数这种表现出异常的行为
class Shape
{
public:
	enum ShapeColor {Red, Green, Blue};
	//非虚调用一个虚函数完成人物,提供缺省参数
	void draw(ShapeColor color = Red) const = 0
	{
		doDraw(color);
	}
private:
	//继承类需提供实现
	virtual void doDraw(ShapeColor color) const = 0;
};

class Rectangle : public Shape
{
public:
	//这里不指定缺省参数值
	virtual void doDraw(ShapeColor color) const;
};

         条款36表明非虚函数不应该被derived类重写,上面的设计使得draw函数的color缺省参数总是Red

         总结:绝不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定的,而virtual函数---唯一应该覆写的东西---却是动态绑定的。

 

        条款38:通过复合塑膜出has-a或"根据某物实现出"

        复合的意义和public继承完全不同

      符合意味着has-a或is-implemented-in-terms-of(根据某物实现出),在应用领域,即程序中的对象其实相当于正塑造的世界中的某些事务,这种是has-a关系(有一个),例如人有名称,地址,语音,传真等等这些对象,即表明人有XXX,这是has-a; 在实现域中,某些对象是实现细节上的人工制品,像缓冲区,互斥器,查找树等,是根据这些对象实现出你的对象,这种是根据某物实现出的关系。例如当需要一个sets,而STL中set会导致空间耗费较多(因为采用红黑树实现,每个元素耗用三个指针(左右孩子,父节点)),但是查找删除等速度较优,如果你的空间要求大于你的速度要求,则STL中原有的set不合适,则可以由其他容器作为其底层容器实现,如list,而set的实际操作都是简单的通过list的操作来实现。


         条款39:明智而审慎的使用private继承

         私有继承首要规则 : 1 编译器不会自动将一个继承类对象转换为一个基类对象,即如果D私有继承B,则B的引用或指针是不能指向D的!多态木有用,这点和public继承是不一样的。2 由private base class继承而来的所有成员,在derived class中会变成private属性,不管是public或protected属性。

        私有继承意味着implement-in-terms-of(根据某物实现出)。如果D私有继承B,则意思是采用B内已经备妥的某些特性,而不是因为B对象和D对象存在有任何观念上的关系。private继承纯粹只是一种实现技术(所以基类中的所有东东在继承中都是私有的,因为它们只是用来实现继承类)。私有继承意味着只有实现部分被继承,接口继承被省去。如果D以private形式继承B,只表示D对象根据B对象实现而得,再没有其他意义。私有继承在软件“设计”层面上没有意义,其意义只在于软件实现层面。

       复合也有根据某物实现出的意思,私有继承也是,那怎么选择:尽可能使用复合,必要时菜使用private继承。必要时是指当protected成员或virtual函数牵扯进来时,例如你可能需要利用虚函数的多态机制时,还有一种激进情况是,当空间方面的厉害关系足以踢翻private集成的指针时。

       例如以下一个例子,一个widget类,决定修改它,让它记录每个成员函数的被调用次数。运行期间将周期性的检查,为了完成任务,需要某个定时器,使我们知道收集统计数据的时候到了,例如有一个现成的Timer类:

class Timer
{
public:
	explicit Timer(int tickFrequency);
	//定时器每滴答一次,函数就自动被调用
	virtual void onTick() const;       
};

         我们可以调整为以我们需要的任何频率滴答前进,每次滴答就调用某个virtual函数(onTick)。我们可以重新定义那个virtual函数,让后者取出Widget的当时状态,为了让

Widget重新定义Timer中的virtual函数,Widget必须继承自Timer.但public不适合,因为Widget不是Timer,所以必须以私有继承Timer,如下:

//Widget不是Timer,Timer只是帮助实现它,所以私有继承,函数onTick在Widget变成
//私有的,且把它放在私有域中,如果public则有可能调用阿
class Widget : private Timer
{
private:
	virtual void onTick() const; //查看Widget的数据等等, 
};

        以上可以实现,但是private并非绝对必要,可以使用复合来实现,方法是在Widget内声明一个潜逃式private类,后者以public形式继承Timer并重新定义onTick,然后放一个这种类型的对象在widget中,如下:

//使用复合实现以上方法
class Widget
{
private:
	//使用一个私有的嵌套类,而这个类共有继承Timer,并实现onTick方法
	class WidgetTimer : public Timer
	{
	public:
		virtual void onTick() const;
	};
    WidgetTimer timer;   //在包含一个嵌套类对象,则可以调用了呀
};

          虽然这个设计复杂些,但这说明解决一个设计问题的方法不止一种,且有两个理由使你选择这种设计而不是私有继承。

         1 这种方法可以实现,阻止derived classes重新定义virtual函数,即如果Widget还有继承类,但你不想让这些继承类重新定义onTick.但如果使用继承,则不能实现,即使是私有继承,因为继承类可以重新定义virtual函数,即使它们不得调用它,虚函数的实现是与作用域无关的,无论基类中的虚函数是共有的还是私有的,你都可以在子类中重定义它,私有的只是你不能访问它,但它不能阻止你重新定义这个虚函数,而且无论你是私有继承还是共有继承,都可以重新定义,只是使用上的意义和情况的问题!但如果使用嵌套类,则derived classes则不能取用widgetTimer的任何成员,也无法继承或重写定义它的virtual函数。

         2 可以将Widget的编译依存性降至最低。私有继承Widget与Timer是相互依存的,而嵌套类可以将WidgetTimer的定义移出Widget外而Widget内含一个指针指向一个WidgetTimer,则Widget可以只带一个简单的WidgetTimer声明式,不需要include任何与Timer有关的东西。

        还有一种选择私有继承的激进情况,空间最优化,这个之发生在你所处理的类不带任何数据,没有non-static成员变量,没有virtual,也没有虚基类(会导致额外体积上的开销),这相当于是一个empty classes类,C++规定凡是独立(非附属,就是一个单独的类,木有继承等等神马的)对象必须有非零大小,所以对已一个这样的空类,C++会安插一个char到空对象中,所以空类的大小变为一个char字节。如下一个例子:

//空类例子
class Empty {};   //C++插入一个char成为一个字节的大小

//以下这个类不再是一个int大小,还包括empty的1字节,如果还有对齐,那就会有
//3个padding了,多一个int了,
class HoldsAnInt
{
private:
	int x;
	Empty e;
};

         上面sizeof(HoldsAnInt) > sizeof (int), 但上面这个独立非附属对象的约束并不适用于derived类对象的基类成员,因为他们并非独立,如果继承一个empty,而不是内含一个这种类型的对象,如下:

//如果继承一个空类,则有空白基类最优化的情况发生EBO
//以下empty不占空间,sizeof(HoldAnInt) == sizeof(int)
class HoldsAnInt : private Empty
{
private:
	int x;
};

          EBO一般只在单一继承(而非多重继承)下才可行,空类虽然不拥有non-static成员,但是却往往内含typedefs, enums,static成员变量,或non-virtual函数,STL就有很多技术用途的empty类,其中内含有用的成员(通常是typedefs),包括base classes  unary_function和binary_function,可以继承这两个类实现函数对象,这就是EBO的实践,这样的继承很少增加derived类的大小。

         复合和private继承都意味is-implemented-in-terms-of, 但复合比较容易理解,且一般情况下选用复合,当存在“并不存在is-a关系”的两个类,其中一个需要访问另外一个protected成员,或需要重新定义其一或多个virtual函数,private可能比较正确。

         和复合不同,private继承可以造成empty base最优化,这对于致力于对象尺寸最小化的成员库开发者而言,可能很重要。


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

         多重继承比单一继承复杂,它可能导致新的歧义性,以及对virtual继承的需要,

          virtual继承会增加大小,速度,初始化(及)赋值复杂度等等成本,如果virtual base classes不带任何数据,将是最具使用价值的情况

         多重继承的确有正当用途。其中一个情况就是“public继承某个Interfaca class”和“private继承某个协助实现的class”的两者组合。

         多重继承有可能从一个以上的base classes继承相同名字(如函数,typedef等等),会导致歧义,如下:

class BorrowableItem
{
public:
    void chechOut();
};

class ElectronicGadget
{
private:
    bool checkOut() const;   
};

//多重以上两个类,继承了两个checkOut,两个是重载的。
class MP3Player: public BorrowableItem, public ElectronicGadget
{};

MP3Player mp;    //mp是非常量变量,对于这两个函数都可以调用。
mp.checkOut();  //这里有歧义,不知道调用哪个checkOut

          以上checkOut调用产生歧义,即使两个函数中一个是public一个是private.这是因为C++解析重载函数调用的规则是:在看到是否有个函数可取用之前,C++首先确认这个函数对此调用而言是最佳匹配。找出最佳匹配函数后才检验其可取用性。而上面的checkOuts有相同的匹配程度,所以没有最佳匹配。解决办法是明确指出调用哪个基类的函数:

//解决以上问题的方法是明确指出调用哪个基类的函数
mp.BorriwableItem::checkOut();
         

            virtual base classes基类初始化的人物是由继承体系中的最底层(most derived)负责。对于虚基类的使用的忠告:

           1 非必要不使用virtual class。平常请使用non-virtual;

           2 如果必须使用virtual bases classes,尽可能避免在其中放置数据,这样就不用担心这些类身上的初始化(和赋值)带来的诡异事情了

           以下为使用一个多重继承的例子,IPerson定义为人的类的接口类,如下:     

//使用多重继承的一个例子
class IPerson
{
public:
    virtual ~IPerson();
    virtual std::string name() const = 0;
    virtual std::string birthDate() const = 0;
};
          只能使用IPerson的指针或references来编写程序,可以创建一个工厂函数来产生由继承IPerson接口实现的具体类,假设一个类名为CPerson,这个类需要实现IPerson的接口,现在假设存在一个相关的类PersonInfo,提供了CPerson所需要的实质东西,即CPerson要实现的接口可以调用这个类中的函数实现,如下:      

//可以帮助IPerson的具体的类来实现基类所定义的各种函数,而且有虚函数,允许
//它的继承类重定义虚函数实现不同的行为
class DatabaseID {};
class PersonInfo
{
public:
    explicit PersonInfo(DatabaseID pid);
    virtual ~PersonInfo();
    virtual const char* theName() const;
    virtual const char* theBirthDate() const;
    //代表值的左右两边的修饰符,继承类可以改变,使用不同的符号
    virtual const char* valueDelimOpen() const;
    virtual const char* valueDelimClose() const;
};
//以下是PersonInfo的实现,子类可以改变
const char* PersonInfo::valueDelimOpen() const 
{
        return "[";
}
const char* PersonInfo::valueDelimClose() const
{
        return "]";
}

const char* PersonInfo::theName() const
{
    static char value[Max_Formatted_Len];
    //写入起始字符
    std::strcpy(value, valueDelimOpen());
    //将value中的字符添加到对象的name成员中
    //写入结尾字符
    std::strcat(value, valueDelimClose());
    return value;
}

           CPerson和PersonInfo的关系是,PersonInfo刚好有若干函数可帮助CPerson比较容易实现出来,关系就是is-implemented-in-terms-of(根据某物实现出),这种关系的实现方法一个是复合,一个是private继承,复合比较受欢迎,但是如果要重新定义virtual函数,那么需要用private继承。本例中CPerson需要重新定义valueDelimOpen和valueDelimClose,所以复合不行,使用私有继承。CPerson必须实现IPerson接口,需要以public继承完成。这就是多重集成的一个应用:将“public继承自某接口”和“private继承某实现”结合在一起,以下CPerson定义:

         

//CPerson类,共有继承Iperson实现接口,私有继承PersonInfo继承其实现
class CPerson : public IPerson, private PersonInfo
{
public:
    explicit CPerson(DatabaseID pid) : PersonInfo(pid) {}
    //实现必要的Iperson成员函数,借用PersonInfo的成员函数
    virtual std::string name() const  
    {
        return PersonInfo::theName();
    }
    virtual std::string birthDate() const
    {
        return PersonInfo::theBirthDate();
    }
private:
    //重新定义继承而来的virtual“界限函数”
    const char* valueDelimOpen() const 
    {
        return "";
    }
    const char* valueDelimClose() const
    {
        return "";
    }
};

            有时候多重继承是完成任务的最简单,最易维护,最合理的做法,如果是这样,也不要害怕使用它,所以要明智而审慎的使用它。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值