其他分享
首页 > 其他分享> > Chpater 6

Chpater 6

作者:互联网

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 关系,所以一下解决方法还是少用。

...

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)

...
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(根据某物实现)。

区分 :

请记住 :

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

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

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

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

缺点 :

什么时候使用:

再看一个例

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