美文网首页Effective C++
【Effective C++(6)】继承与面向对象设计

【Effective C++(6)】继承与面向对象设计

作者: downdemo | 来源:发表于2018-01-05 16:16 被阅读4次

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

  • public inheritance意味着is-a关系,适用于基类的每件事一定适用于派生类
  • 除了is-a另外常见的两个关系是has-a和is-implemented-in-terms-of(根据某物实现出)

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

详见此文中的访问控制与继承部分

  • 派生类成员指涉基类内的某物,编译器可以找到所指涉的东西,因为派生类继承了基类的所有东西,实际的运作方式是派生类作用域嵌套在基类中,在public继承下不要有派生类覆盖基类名称的情况,如果覆盖了可以通过using声明式或inline转交函数访问
class A {
private:
    int x;
public:
    virtual void f1() = 0;
    virtual void f1(int);
    virtual void f2();
    void f3();
    void f3(double);
    ...
};
class B : public A {
public:
    using A::f1; // 让基类中名为f1和f3的所有东西在派生类中可见
    using A::f3;
    virtual void f1();
    void f3();
    void f4();
    ...
};
B b;
int x;
b.f1(); // B::f1
b.f1(x); // A::f1,如果没有using就会出错,因为被B::f1覆盖了找不到
b.f2(); // A::f2
b.f3(); // B::f3
b.f3(x); // A::f3,如果没有using就会出错,因为被B::f3覆盖了

// 如果只希望继承一部分,比如继承A::f1()而非A::f1(int),using声明式就不管用了
// 这时需要用到转交函数
class A {
public:
    virtual void f1() = 0;
    virtual void f(int);
    ...
};
class B : public A {
public:
    virtual void f1() // 转交函数
    { A::f1(); } // 隐式inline
    ...
};
B b;
int x;
b.f1(); // A::f1()
b.f1(x); // 错误,A::f1()被覆盖了

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

  • 其实这部分就是虚函数的使用,public继承的概念包括函数接口继承和函数实现继承,它们的差异类似于函数的声明和定义。设计class时,有时希望派生类只继承接口,有时候又希望继承接口和实现,有时还希望重写实现,详见此文中的虚函数和纯虚函数部分
  • 接口继承和实现继承不同。在public继承下,derived classes总是继承base classes的接口
  • pure virtual函数只具体指定接口继承
  • impure virtual函数具体指定接口继承和缺省实现继承
  • non-virtual函数具体指定接口继承和强制性实现继承

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

  • 这章旨在说明virtual函数有NVI(non-virtual interface)手法和Strategy设计模式等多种形式
  • 假设在做一个游戏,提供一个返回人物健康程度的成员函数healthValue,不同的人物可以根据不同方式计算健康程度,将其声明为virtual函数看起来十分合理
class GameCharacter {
public:
    virtual int healthValue() const;
    ...
};
  • 换个角度这反而是个缺点,因为这个设计如此明显,你可能因此没考虑其他方案。我们来考虑一下其他解法,首先借由Non-Virtual Interface手法实现Template Method模式,它主张virtual函数总为private,较好的设计是保留healthValue为public但成为non-virtual,并调用一个private virtual函数进行实际工作
class GameCharacter {
public:
    int healthValue() const // virtual函数的wrapper(外覆器)
    {
        ... // 事前工作
        int retVal = doHealthValue(); // 真正的工作
        ... // 事后工作
        return retVal;
    }
    ...
private:
    virtual int doHealthValue() const
    {
        ...
    }
};  
  • NVI手法是Template Method设计模式的一个独特表现形式,它的优点隐身在上述代码注释事前事后工作中,来告诉你调用情况,事前工作可包括锁定互斥器、制造运转日志记录项、验证class约束条件、验证函数先决条件等等,事后工作可包括解锁互斥器、验证函数事后条件、再次验证class约束条件等等,如果客户直接调用virtual函数就没法做到这些
  • 借由函数指针实现Strategy模式。NVI手法对public virtual是一个替代方案,但从设计角度来说,另一个更戏剧性的设计主张人物健康程度和人物类型无关,这样的计算完全不需要人物。例如可能要求每个人物的构造函数接受一个指向健康计算函数的指针,而我们可以调用该函数进行实际计算
class GameCharacter; // 前置声明
int defaultHealthCalc(const GameCharacter& gc); // 缺省算法

class GameCharacter {
public:
    typedef int (*HealthCalcFunc)(const GameCharacter&);
    explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
    : healthFunc(hcf)
    {}
    int healthValue() const
    { return healthFunc(*this); }
    ...
private:
    HealthCalcFunc healthFunc;
};
  • 这是常见的Strategy设计模式的简单应用,它比起virtual函数的做法多了一些灵活性
class EvilBadGuy : public GameCharacter {
public:
    explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc)
    : GameChacter(hcf)
    { ... }
};

// 两个不同的计算函数
int loseHealthQuickly(const GameCharacter&);
int loseHealthSlowly(const GameCharacter&);

// 相同类型人物搭配不同计算方式
EvilBadGuy ebg1(loseHealthQuickly);
EvilBadGuy ebg2(loseHealthSlowly);

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

  • 基类指针指向派生类对象时,调用普通的重写方法会调用基类的方法,所以不要重新定义继承而来的non-virtual函数。调用虚函数时会调用派生类重写的虚函数,如果没有重写就往上调用基类的虚函数
class Base {
public:
    virtual int f();
};
chass D1 : public Base {
public:
    int f(int); // 隐藏了基类的f,此f(int)不是虚函数
    virtual void f2(); // 新的虚函数
};
class D2 : public D1 {
public:
    int f(int); // 非虚函数,隐藏了D1::f(int)
    int f(); // 覆盖Base的虚函数f()
    void f2(); // 覆盖D1的虚函数f2
};

Base b;
D1 d1;
D2 d2;

Base* bp1 = &b;
Base* bp2 = &d1;
Base* bp3 = &d2;
// 编译器在运行时确定虚函数版本,判断依据是该指针绑定对象的真实类型
bp1->f(); // 运行时调用Base::f()
bp2->f(); // 运行时本来调用D1::f(),但D1没有这个虚函数,往上调用Base::f()
bp2->f2(); // 错误,Base没有f2()
bp3->f(); // 运行时调用D2::f()

D1* d1p = &d1;
D2* d2p = &d2;
d1p->f2(); // 运行时调用D1::f2()
d2p->f2(); // 运行时调用D2::f2()

// 再看看对非虚函数f(int)的调用
Base* p1 = &d2;
D1* p2 = &d2;
D2* p3 = &d2;
p1->f(42); // 错误,Base没有f(int)
p2->f(42); // 静态绑定,调用D1::f(int)
p3->f(42); // 静态绑定,调用D2::f(int)
  • 具体规则是:先看左侧类型(静态绑定),在左侧类型的类中查找调用的函数,若不存在则出错,若存在
    • 为非虚函数,则直接调用左侧类型中的此函数
    • 为虚函数,则在右侧类型(动态绑定)中,调用此同名的虚函数,若不存在则在上一级基类中查找,直到找到同名虚函数并调用

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

  • 类的头文件中指定默认实参后,在源文件中就无需写上默认实参,否则出现重定义默认实参的错误
// widget.h
class widget : public QMainWindow
{
    Q_OBJECT

public:
    explicit widget(QWidget* parent = 0);
    ~widget();
private:
    Ui::Form* ui;
}

// widget.cpp
widget::widget(QWidget* parent) // 此处不能再指定默认实参
: QMainWindow(parent), ui(new Ui::Form)
{
    ui->setupUi(this);
}
  • 默认实参是静态绑定,而virtual函数是动态绑定的
class A {
public:
    virtual void f(int i = 42)
    {
        cout << "A:" << i << endl;
    }
};

class B : public A {
public:
    virtual void f(int i = 55)
    {
        cout << "B:" << i << endl;
    }
};

int main()
{
    A* b = new B;
    b->f(); // B:42
}
  • 一种解决方法是,派生类和基类使用相同的默认实参,但这样的问题在于代码重复,且有依赖型,如果修改其中一个就要修改另一个
  • 另一种替代的方法是使用NVI手法,令基类的一个public non-virtual函数调用private virtual函数,后者可被派生类重写。non-virtual函数指定默认实参,private virtual函数负责实际工作
class A {
public:
    void g(int i = 42) { f(i); }
private:
    virtual void f(int i) = 0;
};

class B : public A {
private:
    virtual void f(int i) // 传入了来自A::g的实参,即使指定默认实参也会被忽略
    {
        cout << "B:" << i << endl;
    }
};

int main()
{
    A* b = new B;
    b->g(); // B:42
}

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

  • 复合是一种关系,当某种类型的对象包含其他类型对象便是这种关系
class Address { ... };
class PhoneNumber { ... };
class Person {
public:
    ...
private:
    std::string name;
    Address address;
    PhoneNumber voiceNumber;
    PhoneNumber faxNumber;
};
  • 区分is-a和复合关系,假如希望用std::list来实现Set template
template<typename T>
class Set : public std::list<T> { ... };
  • 看起来很好,但实际上完全错误,list中可以内含重复元素而Set不行,“Set是一种list”并不为真,所以不是is-a关系,因此不能用public继承来塑模,正确的做法是用list对象实现Set
template<calss T>
class Set {
public:
    bool member(const T& item) const;
    void insert(const T& item);
    void remove(const T& item);
    std::size_t size() const;
private:
    std::list<T> rep;
};
template<typename T>
bool Set<T>::member(const T& item) const
{
    return std::find(rep.begin(). rep.end(), item) != rep.end();
}
template<typename T>
void Set<T>::insert(const T& item)
{
    if (!member(item)) rep.push_back(item);
}
template<typename T>
void Set<T>::remove(const T& item)
{
    typename std::list<T>::iterator it = 
        std::find(rep.begin(), rep.end(), item);
    if (it != rep.end()) rep.erase(it);
}
template<typename T>
std::size_t Set<T>::size() const
{
    return rep.size();
}

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

  • public继承是is-a关系,而private继承呢?如果是private继承,编译器不会自动将派生类对象转换为基类对象,这和public继承不同,另外由private继承而来的所有成员,在派生类中都会变成private属性
// class Student : private Person
void eat(const Person& p);
Person p;
Student s;
eat(p); // OK,人会吃东西
eat(s); // 错误,学生不会吃东西?
  • private继承意味着implemented-in-terms-of,目的是采用基类中已经备好的某些特性,不是因为基类和派生类在任何观念上的关系,它只是一种实现技术,在设计层面上没有意义,基类中的每样东西在派生类中都是private,因为它们只是实现细节

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

class A {
public:
    void f();
    ...
};
class B {
public:
    bool f() const;
    ...
};
class C : public A, public B
{ ... };
C c;
c.f(); // 调用哪个f
  • 这里两个函数匹配程度相同,所以会产生二义性,报错,所以必须明确指出用的是哪一个
c.A::f();
  • 如果A<-B<-D,A<-C<-D,D有两条路径继承到A,那是不是A中的变量在D中有两份呢?简单的逻辑告诉我们,不该有重复,C++两个做法都支持,缺省的做法是复制两份,如果不希望如此,必须令那个带数据的class成为一个virtual base class,为此必须令所有直接继承自它的class采用virtual继承
class A { ... };
class B : virtual public A { ... };
class C : virtual public A { ... };
class D : public B, public C
{ ... };
  • virtual继承的代价是,产生的对象体积更大,访问也更慢,所以非必要不用,如果要用,尽可能避免在其中放置数据,如果virtual base class不带任何数据,将是最具是有价值的情况
  • MI(multiple inheritance)只是个工具,比起单一继承,它通常比较复杂,使用上也较难理解,如果你唯一能提出的设计方案涉及多重继承,一定会有某些方案让单一继承行得通,然而有时多重继承确实是最简洁,最易维护,最合理的做法,此时不要害怕使用它

相关文章

网友评论

    本文标题:【Effective C++(6)】继承与面向对象设计

    本文链接:https://www.haomeiwen.com/subject/mejsnxtx.html