Loading

【C++】《Effective C++》第六章

第六章 继承与面向对象设计

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

public隐含的寓意:每个派生类对象同时也是一个基类对象,反之不成立。只不过基类比派生类表现出更一般化的概念,派生类比基类表现出更特殊化的概念。

因此,C++中,任何函数如果期望获得一个类型为基类的实参,都也愿意接受一个派生类对象,但反之不成立。

void eat(const Person &p);
void study(const Student &s);
Person p;
Student s;
eat(p); // Success
eat(s); // Success
study(p);   // Error
study(s);   // Success

谨记这种is-a关系以及背后隐藏的规则可以防止因为"经验主义"而使用不合理的继承:

  • 从经验主义上看,企鹅也是鸟,如果为鸟定义了虚拟的飞的方法,然后企鹅以public基类鸟类,那么显然不是合理的方式,因为不是所有鸟都能飞。
  • 从"经验主义"看,正方形也是长方形,如果长方形有成员方法会修改长或宽,那么正方形以public继承长方形,那么显然不是合理的方式,因为正方形长和宽必须同时变化。

所以,应该根据实际软件需求,合理使用public

请记住

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

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

继承中的作用域嵌套:名字查找会从内层作用域向外层作用域延伸。

名称遮掩会遮掩基类所有重载版本:派生类中同名的名称会遮掩基类中相同的名称,如果基类包含重载函数。这种行为背后基本理由是为了防止你在程序库或应用框架内建立新的derived class时附带地从疏远的base classes继承重载函数。

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

private:
    int x;
};

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

// 使用
Derived d;
int x;
d.mf1();
d.mf2(x);   // Error,因为Derived::mf1遮掩了Base::mf1
d.mf2();
d.mf3();
d.mf3(x);   // Error,因为Derived::mf3遮掩了Base::mf3

如果想继承重载函数,可以使用using声明式

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

private:
    int x;
};

class Derived: public Base {
public:
    using Base::mf1;    // 让Base class内名为mf1和mf3的所有东西在Derived作用域内都可见
    using Base::mf3;
    virtual void mf1();
    void mf3();
    void mf4();
};

// 使用
Derived d;
int x;
d.mf1();
d.mf2(x);   // Success,调用Base::mf1
d.mf2();
d.mf3();
d.mf3(x);   // Success,调用Base::mf3

请记住

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

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

类设计者最常犯的两个错误:

  • 将所有函数声明为non-virtual,这会使得derived classes没有剩余空间进行特殊化工作。
  • 将所有成员函数生命为virtual

请记住

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

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

当你为解决问题而寻找某个设计方法时,不妨考虑virtual函数的替代方案。

一个例子,在一个游戏人物的类中,存在一个健康值计算的函数,不同的角色提供不同的健康值计算方法,并且存在一个缺省实现。有以下几种方案可供选择:

  • 以传统public virtual函数实现
class GameCharacter {
public:
    virtual int healthValue() const;
};
  • 使用non-virtual interface(NVI)手法实现模板方法模式
class GameCharacter {
private:
    virtual int doHealthValue() const {
        // ...
    }

public:
    int healthValue() const {
        // ...  事前工作
        int retVal = doHealthValue();   // 做真正的工作
        // ...  事后工作
        return retVal;
    }
};

NVI手法的一个优点是可以在真正操作进行的前后保证一些"事前"和"事后"工作一定会进行。如"事前"进行一些锁的分配,日志记录,"事后"进行解锁等操作。

  • Function Pointers实现Strategy模式
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter {
private:
    HealthCalcFunc healthFunc;

public:
    typedef int (*HealthCalcFunc) (const GameCharacter&);
    explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc): healthFunc(hcf) {}
    int healthValue() const { return healthFunc(*this;)}
};
  • std::function实现Strategy模式
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter {
private:
    HealthCalcFunc healthFunc;

public:
    typedef std::function<int const GameCharacter&> HealthCalcFunc;
    explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc): healthFunc(hcf) {}
    int healthValue() const { return healthFunc(*this;)}
};
  • 传统的实现Strategy模式
class GameCharacter;
class HealthCalcFunc {
public:
    virtual int calc(const GameCharacter& gc) const {}
};
HealthCalcFunc defaultHealthCalc;
class GameCharacter {
private:
    HealthCalcFunc* pHC;

public:
    explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc): pHC(phcf){}
    int healthValue() const {
        return pHC->calc(*this);
    }
};

总的来说,

  • 使用non-virtual interface(NVI)手法,这是模板方法(Template Method)设计模式的一种特殊形式。它以public non-virtual成员函数包裹较低访问性(privateprotected)的vitual函数。
  • virtual函数替换为"函数指针成员变量",这是策略(Strategy)设计模式的一种分解表现形式。
  • std::function(在头文件functional中)成员变量替换virtual函数,因而允许任何可调用物(callable entity)搭配一个兼容于需求的签名式,这是策略设计模式的某种形式。
  • 将继承体系内的virtual函数替换为另一个继承体系内的virtual函数,这是策略设计模式的传统实现手法。

请记住

  • virtual函数的替代方案包括各种设计模式的不同表现形式。
  • 将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问classnon-public成员。
  • std::function对象的行为就像一般函数指针,这样的对象可接纳"与个给定之目标签名式兼容"的所有调用物(callable entity)。

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

如果某个操作系统在整个继承体系应该是不变的,那么使用non-virtual函数,此时派生类从基类继承接口以及一份强制实现。如果派生类希望表现出不同行为,那么应该使用virtual函数。

另一方面,假设真的重新定义了继承而来的non-virtual函数,会表现令人困惑的情况。

请记住

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

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

这个条款的原因在于,virtual函数是动态绑定,而缺省参数值是静态绑定。所以你可能调用了一个派生类的virtual函数,但是使用到的是缺省参数却是基类的。

class Shape{
private:
    // ...

public:
    enum ShapeColor {Red, Green, Blue};
    virtual void draw(ShapeColor color = Red) const = 0;
};

class Rectangle: public Shape {
private:
    // ...

public:
    virtual void draw(ShapeColor color = Green) const = 0;  // 
};

class Circle: public Shape {
private:
    // ...

public:
    virtual void draw(ShapeColor color) const = 0;
};


// 使用
Rectangle r;
Circle c;

r.draw();

Shape *pr = &r;
Shape *pc = &c;

// 引起困惑
pr->draw(); // 调用Rectangle::draw,但是静态类型为Shape,所以缺省参数为Shape::Red
pc->draw(); // 调用Circle::draw,但是静态类型为Shape,所以缺省参数为Shape::Red

即使派生类严格遵循基类的缺省参数,也存在问题,当基类的缺省参数发生变化时,派生类的所有参数也需要跟这改变。

为什么C++坚持以这种乖张的方式来运作呢?答案在于运行期效率。如果缺省参数是动态绑定,编译器就必须有某种方法在运行期为virtual函数决定适当的参数缺省值。

如果确实有这种需求,可以用NVI手法代替。

class Shape{
private:
    virtual void doDraw(ShapeColor color) const = 0;    // 真正完成工作的动作
    // ...

public:
    enum ShapeColor {Red, Green, Blue};
    void draw(ShapeColor color = Red) const {   // non-virtual
        doDraw(color);
    }
};

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

请记住

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

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

复合(compositon)是类型之间的一种关系,当某种类型的对象内含它种类型的对象,便是这种关系。

class Address{};
class PhoneNumber{};
class Person{
private:
    std::string name;
    Address address;
    PhoneNumber number;
}

那么如何区分is-a(继承,是一种)和is-implemented-in-terms-of(组合,根据某物实现出)这两种对象关系呢?

假设现在让你实现一个set,直觉可能是使用标准库的set template,但是不幸的是set的实现往往引出"每个元素耗用三个指针"的额外开销,因为set通常以平衡查找书实现而成,使他们在查找、插入、删除元素时保证对数时间效率。当时间比空间重要时,这是个好设计,但是当空间比时间重要时这就不一定了。

通常,可以使用标准程序库的list template来实现它。

template<typename T>
class Set: public std::list<T> {};

但是,上面的做法显然错误。因为list可以含重复元素,但是set不可以。由于它们之间并非is-a的关系,所以public继承显然不合适。

正确的做法是使用复合。

// set.h

#ifndef __SET_H__
#define __SET_H__

#include <iostream>
#include <algorithm>
#include <list>

template <class T>
class Set {
private:
    std::list<T> rep;   // 用来表述Set的数据

public:
    bool find(const T& item) const;
    void insert(const T& item);
    void remove(const T& item);
    std::size_t size() const;
};

#endif
#include "set.h"

template< class T>
bool Set<T>::find(const T& item) const {
    return std::find(rep.begin(), rep.end(), item) != rep.end();
}

template< class T>
void Set<T>::insert(const T& item) {
    if(!find(item)) {
        rep.push_back(item);
    }
}

template< class T>
void Set<T>::remove(const T& item) {
    auto it = std::find(rep.begin(), rep.end(), item);
    if(it != rep.end()) {
        rep.erase(it);
    }
}

template< class T>
std::size_t Set<T>::size() const {
    return rep.size();
}


int main(int argc, char* argv[]) {

    Set<int> s;
    s.insert(10);
    s.insert(5);
    std::cout << s.size() << std::endl;
    std::cout << s.find(5) << std::endl;
    s.remove(5);
    std::cout << s.size() << std::endl;

    return 0;
}

请记住

  • 复合的意义和public继承完全不同。
  • 在应用域,复合意味has-a(有一个),在实现域,复合意味is-implemented-in-terms-of(根据某物实现出)。

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

class Person {};
class Student: private Person {};
void eat(const Person& p);
Person p;
Student s;
eat(p); // Success
eat(s); // Error

privatepublic继承的不同之处:

  • 编译器不会把子类对象转换为父类对象。
  • 如果使用public继承,编译器在必要的时候可以将Student隐式转换成Person,但是private继承时不会。
  • 父类成员(即使是publicprotected)都变成了private
  • public表现出is-a的关系,private表现出is-implemented-in-terms-of的关系。

请记住

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

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

多重继承的意思是继承一个以上的base class,很容易导致要命的"钻石型多重继承":

class File {};
class InputFile: public File {};
class OutoputFile: public File {};
class IOfile: public InputFile, public OutoputFile {};

一般有两种方式使用多继承:

  • 一般的多重继承:
    • 如果某个基类到派生类之间存在多条路径,那么派生类会包含重复的基类成员。
  • 虚继承(此时基类是虚基类):
    • 如果某个基类到派生类之间存在多条路径,派生类只包含一份基类成员,但是这会带来额外的开销。

使用virtual继承的那些classes所产生的对象往往比使用non-virtual继承的兄弟们体积大,访问virtual base class的成员变量时,也比访问non-virtual base class的成员变量速度慢。并且,virtual继承的成本还不止这些,比如:

  • classes若派生自virtual bases而需要初始化,必须认知其virtual bases - 无论bases距离多远。
  • 当一个新的derived class加入继承体系中,它必须承担其virtual bases(不论直接还是间接)的初始化责任。

对使用virtual继承的建议:

  • 非必须不要使用virtual bases,平常请使用non-virtual继承。
  • 如你必须使用virtual base classes,尽可能避免在其中放置数据。

请记住

  • 多重继承比单一继承复杂,它可能导致新的歧义性,以及对virtual继承的需要。
  • virtual继承会增加大小、速度、初始化复杂度等等成本。如果virtual base classes不带任何数据,将是最具有实用价值的情况。
  • 多重继承的确有正当用途。其中一个情节涉及"public继承某个Interface class"和"private继承某个协助实现的class"的两者结合。
posted @ 2020-08-20 16:19  Parzulpan  阅读(226)  评论(0)    收藏  举报