ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Chpater 6

2022-08-24 03:00:18  阅读:216  来源: 互联网

标签:函数 Chpater 继承 virtual class private public


6 继承与面向对象设计

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

“Derived is a Base!”

​ 当一个类可以描述成 is-a 这样的概念的时候,就应该用 public 继承。

例. 每个学生都是人,但人不一定是学生。因此学生类应该 public 继承自 “人” 类。

请记住 :

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

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

​ “Derived class 作用域被嵌套在 base class 作用域内。”

class Base {
private:
    int x;
public:
    virtual void mf1() = 0;
    virtual void mf2();
    void mf3();
    ...
}

class Derived : public Base {
public:
    virtual void mf1();
    void mf4();
    ...
}

void Derived::mf4() {
    ...
    mf2();
    ...
}

当程序调用 mf4() 并且遇到其内部的mf2()的时候,这是程序内部发生的事情 :

  1. 编译器首先查找 local 作用域,也就是 mf4()所覆盖的的作用域,很遗憾,并未找到任何名为 mf2()的东西。

  2. 因此转到 mf4()外部也就是derived class所覆盖的作用域去查找,但还是没有找到一个名为mf2()的东西。

  3. 于是便继续移到外围去查找,这次来到了base class,并且在此找到了virtual void mf2();

  4. 至此,查找完毕。若未找到,便会继续向外查找。

“继承类内的重载函数会遮掩基类中的同名函数

注意哦,是同名函数,不是同签名的函数哦,也就是说就算其参数不同,也是会被遮盖的。

class Base {
private:
    int x;
public:
    virtual void mf1() = 0;
    virtual void mf1(int);
    virtual void mf2();
    void mf3();
    void mf3(double);
    ...
};

class Dericed : public Base {
public:
    virtual void mf1();
    void mf3();
    void mf4();
    ...
};

调用

Derived d;
int x;
...
d.mf1();	//调用Derived::mf1()
d.mf1();	//错误,Derived::mf1()遮掩了Base::mf1()和Base::mf1(int),但它自己没有参数所以会错误
d.mf2();	//调用缺省版本的Base::mf2()
d.mf3();	//调用Derived::mf3()
d.mf3(x);	//错误,Derived::mf3()遮掩Base::mf3(double)和Base::mf3()

解决方法 :

实际上,如果你在使用 public 继承而又不继承那些重载函数,就是违反了 is-a 关系,所以一下解决方法还是少用。

  • 使用 using 声明

...

class Derived : public Base {
public:
    using Base::mf1;
    using Base::mf3;
    ...
};

调用

Derived d;
int x;
...
d.mf1();	//匹配派生类的Derived::mf1()
d.mf1(x);	//匹配Base::mf1(int)
d.mf2();	//匹配缺省版本的Base::mf2()
d.mf3();	//匹配Derived::mf3()
d.mf3(x);	//匹配Base::mf3(x)
  • 使用 inlining 转交函数(forwarding function)

...
class Derived : private Base {
public:
    virtual void mf1()
    { Base::mf1(); }
    ...
};

Derived d;
int x;
d.mf1();	//调用Derived:mf1(),进而调用Base::mf1()
d.mf1(x);	//错误,被遮掩

注: 因为这种做法是违反 public 继承原则的,所以我们选择使用 private 继承。

请记住 :

1. derived classes 内的名称会遮掩 base classes 内的名称。在 public 继承下从来没有人希望如此。

*2. 为了让遮掩的名称再见天日。可以使用 using 声明式或转交函数 (forwarding function)。*

34 区分接口继承和实现继承

public 继承的概念由两部分组成:

1. 函数接口( function interfaces )继承
2. 函数实现( function implementations )继承

先看书上的例子

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{...};

显而易见,Shape 是一个抽象基类,因此它不能过创建实体。但是按照条款 32 的规则,其成员函数接口总是会被继承的。

该类共声明三个函数。

  1. virtual void draw() = 0

    pure virtual 函数 : 必须被 “继承了它们” 的具象类重新声明,而且通常在抽象基类中是没有定义的。

    声明一个 pure virtual 函数的目的是为了让 derived classes 只继承函数接口。

    另:可以为 pure virtual函数提供的定义,调用它的唯一途径就是明确指出类的名称。但尽量不去用它。提供缺省版本有更好的做法。

  2. virtual void error()

    impure virtual 函数 : derived class 继承其函数接口,但同时 impure virtual 函数会提供一份实现代码。

    声明一个 impure virtual 函数的目的,是让 derived classes 继承该函数的接口和缺省实现。

  3. int ObjectID() const

    non-virtual 函数 : 不变性凌驾于特异性,意味着 derived class 不希望做出不同的行为,而是继承该函数的行为。

    声明 non-virtual 函数的目的是为了令 derived classes 继承函数的接口及一份强制的实现。

请记住 :

1. 接口继承和实现继承不同。在 public 继承之下,derived classes 总是继承 base class 的接口。

2. pure virtual 函数只具体指定接口继承。

3. impure virtual 函数具体指定接口继承及缺省实现继承。

4. non-virtual 函数具体指定接口继承以及强制性实现继承。

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

前提 :假设我们在写一个游戏人物类,其中有一个生命值降低的函数,它返回一个整数来表示不同的健康程度。不同的人物有不同的方式计算生命健康度

class GameCharacter {
public:
    virtual int healthValue() const;
    ...
};

正如标题所言,我们一般的做法就是将其声明为 virtual 函数,再有不同的派生类去继承它。除此之外,还有其他的做法:

  1. 藉由 Non-Virtual Interface 手法实现 Template Method 模式

概括 : 保留 healthValue() 但让它变成 non-virtual 函数,并调用一个 private virtual 函数。

class GameCharacter {
public:
    int healthValue() const {
        ...								//事前工作
        int retVal = deHealthValue();	//真正的工作
        ...								//事后工作
        return retVal;    
    }
private:
    virtual int deHealthValue() const {	//derived class 可以重新定义它
        ...
    }
};

通常我们把这个 non-virtual 函数称为外覆器(wrapper)。

NVI 手法的优点就是可以保证在调用函数的前后做一些处理,不需要用户来自己处理,普遍适用于像锁定互斥器、制造运转日志记录项等等。

另外,没必要让 virtual 函数一定是 private。可以把它定义为 protected ,但如果函数必须定义为 public 则不能实施 NVI 手法了。

  1. 藉由 Function Pointers 实现的 Strategy 模式

概述 : 如果我们要求人物的健康指数的计算与人物无关,而令每个人物的构造函数接受一个指针,该指针指向一个健康计算函数,这样我们便可以给不同的人物赋予不同的健康计算函数指针,以此来计算人物的健康程度。

class GameCharacter;			//前置声明
int defaultHealthCalc(const GameCharacter& gc);
class GamaCharacter {
public:
    typedef int (*HealthCalcFunc)(const GameCharacter &);
    explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
        : healthFunc(hef)
        {}
    int healthValue() const {
        return healthFunc(*this);
    }
    ...
private:
    HealthCalcFunc healthFunc;
};
  1. 藉由 tr1::function 完成 Strategy 模式
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter {
public:
    typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
    explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
        :healthFunc(hcf)
        {}
    int healthValue() const
    { return healthFunc(*this); }
    ...
private:
    HealthCalcFUnc healthFunc;
};

此时构造函数接受任何兼容的可调用物,包括但不限于函数对象、成员函数。

  1. 古典的 Strategy 模式
class GameCharacter;
class HealthCalcFunc {
public:
    ...
    virtual int calc(const GameCharacter& gc) const
    {...}
    ...
};

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

注意如果想添加一个健康计算方法,只需要为 HealthCalcFunc 类添加一个派生类即可。

请记住 :

1. virtual 函数的替代方案包括 NVI 手法及 Strategy 设计模式的多种形式。NVI 手法自身是一个特殊形式的 Template Method 设计模式。

2. 将机能从成员函数移到 class 外部函数,带来的一个缺点是,非成员函数无法访问 class 的 non-public 成员。

3. tr1::function 对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式兼容”的所有可调用物。

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

这个没什么好讲的,如果重新定义继承而来的 non-virtual 函数,就会遮掩基类的同名函数。那这不叫继承了,而且违背了两个原则 :

  1. 使用 B 对象的每一件事,也适用与 D 对象,因为每个 D 对象都是一个 B对象。

    若此时 D 重新定义了该函数,则每次需要该函数时,由于每个 D 对象都是一个 B对象,所以该函数的行为与基类一致,以至于每次都得调用基类的函数,而不是自己的(函数实现继承)函数,那么“每个 D 对象都是一个 B对象”明显是错的。

  2. B 的 derived classes 一定会继承 mf 的接口和实现,因为 mf 是 B 的一个 non-virtual 函数。

    因为 non-virtual 函数的继承“不变性凌驾于特异性之上”,如果你重新定义继承而来的函数,那这个函数明显应该是 virtual 函数才对。

请记住 :

1. 绝不重新定义继承而来的 non-virtual 函数。

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

“本条款讨论“继承一个带有缺省参数值的 virtual 函数” ”

我们先明确本条款的理由,这也是本条款的核心 :

virtual 函数是动态绑定,而缺省参数值却是静态绑定。

区别 :

  • 对象的静态绑定,就是它在程序中被声明时所采用的类型。
  • 对象的动态类型则指的是“目前所指对象的类型。”

由于缺省(默认)是静态绑定的,因此使用基类指针调用函数时,并不会根据多态性去选择缺省参数值,而是直接使用基类声明时指定的缺省参数值。因此重新定义是无效的,同时也会起到误导的后果。

请记住 :

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

38 通过复合塑模出 has-a 或 “根据某物实现出”

​ 什么是复合?

复合是类型之间的一种关系,当某种类型的对象内含其他的类型的对象,便叫复合。

之前介绍的 public 继承带有 is-a 的含义,而复合意味着 has-a 或者 is-implemented-in-terms-of(根据某物实现)。

区分 :

  • has-a : 复合发生在应用域内的对象之间,应用域就是说你在程序中将现实生活中的事物抽象成一个个的类,例如一辆汽车,一个人。
  • is-implemented-in-terms-of : 复合发生在实现域内的对象之间,实现域就是类似域缓冲区、互斥器等实现细节的人工制品,可以理解成以某种数据结构为基础而实现的一种新的数据结构。

请记住 :

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

2. 在应用域,复合意味着 has-a 。在实现域,复合意味着 is-implemented-in-terms-of。

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

private 继承的特点 :

  • 编译器不会将一个 derived class 对象转换成一个 base class 对象。(丧失了多态性)
  • 由 private base class 继承而来的所有成员,在 derived class 中都会变成 private 属性。

因此如果我们让 class D 以 private 形式继承 class B,你的用意是为了采用 class B 内已经具备的某些特性,而不是因为 B 和 D 之间存在某种逻辑关系。

根据上述讨论,我们很容易看出 private 继承和之前提到的 is-implemented-in-terms-of 是一样的概念,而我们在前面是利用复合来实现的,那目前为止我们就有了了两种实现方式,一种是 private 继承, 一种是复合。

所以说我们该怎样选择?

  1. 使用 private 继承

假设我们现在有一个类 Widget,其中需要用到定时器,目前现有一个定时器类 :

class Timer {
public:
    explicit TImer(int tickFrequency);
    virtual void onTick() const;
    ...
};

由于 class Timer 中有 virtual 函数,所有 Widget 必须继承自 Timer。显然两者不符合 is-a 关系,因此我们不能使用 public 继承。

因此我们使用 private 继承

class Widget : private TImer {
private:
    virtual void onTIck() const;
    ...
};

这样用户就不会解除到 Timer 相关的函数。

  1. 使用复合

class Widget {
private:
    class WidgetTimer : public Timer {
    public:
        virtual void onTick() const;
        ...
    };
    
    WidgetTimer timer;
};

我们将内嵌类 WidgetTimer 放在 private 中,因为这样可以让类 Widget 的派生类无法访问。

有一种情况成为我们使用 private 继承的理由 :

​ private 继承主要用于 “当派生类想要访问基类的 protected 成分,或为了重新定义一个或多个 virtual 函数”。其实这个使用复合也可以做到,但如果我们所继承的基类不携带任何数据也就是没有 non-static 成员变量,没有 virtual 函数,也没有 virtual base classes。但其实在C++中也是会占有一定的开销,因为C++会默认地安插一个char到空对象中。真正的不占用任何开销的行为就是使用 private 继承也就是所谓的 EBO(empty base optimization)。

class Empty {};

//复合做法
class HoldAnInt {
private:
    int x;
    Empty e;
};//sizeof(HoldAnInt) > sizeof(int),空白类还是占用了开销

//private 继承
class HoldAnInt : private Empty {
private:
    int x;
};//sizeof(HoldAnInt) == sizeof(int),不占用开销

但实际上,大多数类都不是空白类,所以无论什么时候,只要可以,你还是选择复合

请记住 :

1. private 继承意味 is-implemented-in-terms-of 。通常比复合的级别低。但是当 derived class 需要访问 protected base class的成员,或需要重新定义继承而来的 virtual 函数时,这么设计是合理的。

2. 和复合不同,private 继承可渔鸥造成 empty base 最优化。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很需要。

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

image-20211005065436638

使用多重继承时,有两种方案,如图,IOFile 和 File 之间有多条相通路径。两种方案分别为

  1. IOFile 从其中每一个直接基类中继承一份数据
  2. IOFile 只继承一份数据

一般我们使用第二种方案,需要用到 virtual 继承

class File {...};
class InputFile : virtual public File{...};
class OutputFIle : virtual public File{...};
class IOFile : public InputFile,
			   public OutputFIle
{...};                   

缺点 :

  • 使用 virtual 继承的那些 classes 所产生的对象体积往往很大
  • 访问 virtual base classes 的成员变量时速度也比较慢

什么时候使用:

  • 非必要时不使用
  • 如果要使用,尽可能避免在其中virtual base class 中放置数据

再看一个例

class IPerson {
public:
    virtual ~IPerson();
    virtual std::string name() const = 0;
    virtual std::string birthDate() const = 0;
};

IPerson 的客户必须以 IPerson 的 pointers 和 references 来编写程序,因为这是抽象基类,所以我们使用一个工厂函数。

std::shared_ptr<IPerson> makePerson(DatabaseID personIdedntifier);
DatebaseID askUserForDatabaseID();
DatabaseID id(askUserForDatabaseID());
std::shared_ptr<IPerson> pp(makePerson(id));

我们假设该类的派生类叫 CPerson , 我们要做的是继承抽象基类的所有函数,再假设我们有一个既有的类 PersonInfo 可以帮助我们重写抽象积累的函数。

class PersonInfo {
public:
    explicit PersonInfo(Database pid);
    virtual ~PersonInfo();
    virtual const char* theName() const;
    virtual const char* theBirthDate() const;
    ...
private:
    virtual const char* valueDelimOpen() const;
    virtual const char* valueDelimClose() const;
    ...
};

其中 private 里的那两个方法是用来格式化输出相应的字符串的,但是不同的类当然有不同的格式化,所以我们可以去继承它,通过重写该方法从而达到目的。

缺省实现

virtual const char* valueDelimOpen() const;
{ return "["; }
virtual const char* valueDelimClose() const;
{ return "]"; }

const char* PersonInfo::theName() const {
    static char value[Max_Length];
    std::strcpy(value, valueDelimOpen());
    //将value中的字符串附加到 name 成员中
    std::strcat(value, valueDelimClose());
    return value;
}

分析 :

​ 显然,CPerson 的实现需要用到 PersonInfo 这个类,因此它们之间的关系应该是 is-implemented-in-terms-of , 一共有 private 继承和复合两种方法,因为我们要继承重写 PersonInfo 中的某些方法,所以应该使用 private 继承。而又因为 CPerson 和 IPerson 是一种 is-a 的关系,所以使用 public 继承。

class CPerson : public IPerson, private PersonInfo {
public:
    explicit CPerson(DatabaseID pid) : PersonInfo(pid) {}
    virtual std::string name() const
    { return PersonInfo::theName; }
    virtual std::string theBirthDate()
    { return PersonInfo::theBirthDate; }
private:
    virtual const char* valueDelimOpen() const {return "";}
    virtual const char* valueDelimClose() const {return "";}
};

请记住 :

1. 多重继承比单一继承复杂。它可能导致新的歧义性,以及对 virtual 的维护。

2. virtual 继承会增加大小、速度、初始化复杂度等等成本。如果 virtual base classes 不带任何数据,将是最具实用价值的请款、

3. 多重继承的确有正当用途。其中一个情节涉及“public继承某个 Interface“ class 和 “private”和 “private 继承某个”协助实现的 class“的两相组合。

标签:函数,Chpater,继承,virtual,class,private,public
来源: https://www.cnblogs.com/Lingh/p/16618440.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有