[笔记]Effective C++改善程序与设计的55个具体做法_第六章 继承与面向对象设计

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

以C++进行面向对象编程,最重要的一个规则是:public inheritance(公开继承)意味“is-a"(是一种)的关系。

如果你令class D("Derived")以public形式继承class B("Base"),你便告诉C++编译器说每一个类型为D的对象同时也是一个类型为B的对象,反之不成立。B比D表现出更一般性的概念,而D比B表现出更特殊化的概念。

需要注意的是:

public继承主张,能够施行于base class对象身上的每件事情,也可以施行于derived class对象身上。

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

基础作用域名称掩盖例子:

int  x; // global 变量

void SomeFunc()
{
    double x; // local 变量
    std::cin >> x; 
}

这里的local变量x会覆盖global变量,而不关注其是否为不同类型。

在继承体系中,也有类似的名称遮掩和查找顺序。

考虑如下继承例子:

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

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

// 使用示例说明
Derived d;
int x;
...

d.mf1(); // 没问题调用Derived::mf1
d.mf1(x); // 错误! 因为Derived::mf1遮掩了Base::mf1
d.mf2(); // 没问题,调用Base::mf2
d.mf3(); //没问题,调用Derived::mf3
d.mf3(x); // 错误! 因为Derived::mf3遮掩了Base::mf3

如果你正在使用public继承而又不继承哪些重载函数,就是违反base和derived classes之间的is-a关系,is-a是public继承的基石。

比较丑陋的解决方案是在Derived Class中使用public类型的using

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

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

// 使用示例说明
Derived d;
int x;
...

d.mf1(); // 没问题调用Derived::mf1
d.mf1(x); // 现在没问题了,调用Base::mf1
d.mf2(); // 没问题,调用Base::mf2
d.mf3(); //没问题,调用Derived::mf3
d.mf3(x); // 现在没问题了,调用Base::mf3

而当类之间的继承关系为private继承时,情况略有不同

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

class Derived: private Base {
public:
    virtual void mf1() {
        Base::mf1(); // 转交函数 暗自成为inline
    }
    ...
};

// 使用示例说明
Derived d;
int x;
...

d.mf1(); // 没问题调用Derived::mf1
d.mf1(x); // 错误! Base::mf1()被遮掩了

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

几何形状绘图的例子:

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

pure virtual函数draw,将Shape成为一个抽象class,客户无法创建Shape的实体,而只能创建derived class的实体。

因为是纯虚函数,所以必须被具象的class重新声明,而且draw在基类中不会有定义。

  • 也就是说声明一个纯虚函数就是为了让derived class只继承函数接口。

以下是使用示例:

Shape* ps = new Shape; // 错误! Shape是抽象的
Shape* ps1 = new Rectangle; // 没问题
ps1->draw(); // 调用Rectangle::draw
Shape* ps2 = new Ellipse; // 没问题
ps2->draw(); // 调用Ellipse::draw
ps1->Shap::draw(); // 调用Shap::draw
ps2->Shap::draw(); // 调用Shape::draw
  • 声明impure virtual(非纯虚)函数的目的,是让derived class继承该函数的接口和缺省实现。

书中给出了一个有意思的例子,不同型号的飞机飞行的故事,基础的类继承关系如下:

class Airport {...}; // 用以表现机场
class Aireplane {
    public:
        virtual void fly(const Airport& destination);
        ...
};

void Aireplane::fly(const Airport& destination)
{
    缺省代码, 将飞机飞至指定的目的地
}

class ModelA: public Airplane { ... };
class ModelB: public Airplane { ... };

初始时候ModelA和ModelB都自行实现了fly覆盖基类的fly函数,这样可以正确的执行飞行任务。当公司盈利,引入了ModelC型飞机时,此时恰巧粗心的工程师没有覆盖基类的fly函数,那么:

Airport PDX(...);
Aireplane* pa = new ModelC;
...
pa->fly(PDX); // 呕吼 这将飞向哪里?

这也就是书中说的,如果子类没有明确说出我要,那么不要提供,否则就是错误的引子。下面是推荐的一种做法:

class Airplane {
    public:
        virtual void fly(const Airport& destination) = 0;
        ...
    protected:
        void defaultFly(const Airport& destination);
}

void Airplane::defaultFly(const Airport& destination)
{
    缺省行为,将飞机飞至指定的目的地。
}

这样,子类在需要时候可自行调用defaultFly函数,例子 如下:

class ModelA:: public Airplane {
    public:
        virtual void fly(const Airport& destination) {
            defaultFly(destination);
        }
        ...
}

这样,新增的ModelC型飞机就天然知道需要实现fly接口了。

需要注意的是Aireplane::defaultFly是一个non-virtual函数,这点也很重要。因为没有任何一个derived class应该重新定义此函数。

上述的修改方式由于多了一个函数defaultFly,仍然让一些人会产生困惑,那么可以借用pure virtual也可以提供一个默认实现的方式提供缺省行为。

class Airplane {
    public:
        virtual void fly(const Airport& destination) = 0;
        ...
}

void Airplane::fly(const Airport& destination)
{
    缺省行为,将飞机飞至指定的目的地。
}

class ModelA:: public Airplane {
    public:
        virtual void fly(const Airport& destination) {
            Airplane::fly(destiontion);
        }
        ...
}
  • 声明non-virtual函数的目的是为了令derived classes继承函数的接口及一份强制性实现。注意这里指代的是强制性的。non-virtual函数的不变形(invariant)凌驾特异性(specialization)。

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

书中以一个战斗类游戏为例,列举出当实现一个人物生命值的变化函数的设计策略,初始的类定义可能像这样:

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

子类型需要重新实现healthValue,初看很合理,每个任务的生命值计算方式肯定有差异,例如战士、法师、刺客...

这种设计本质上没问题,可以实现诉求,书中描述的思考是有没有更优的方式。至于书中提出的NVI(Non-Virtual Interface)手法,在这个例子中并不是最优方案,这里略过,会在条款37看到其用法。

我们来看第一个优化方式:使用函数指针和策略模式

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

这么做可以实现:

class EvilBadGuy: public GameCharacter {
    public:
        explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc) : GameCharacter(hcf)
        {
            ...
        }
        ...
};

int loseHealthQuickly(const GameCharacter&); // 健康指数计算函数1
int loseHealthSlowly(const GameCharacter&); // 健康指数计算函数2

EvilBadGuy ebg1(loseHealthQuickly); // 相同类型的任务搭配
EvilBadGuy ebg2(loseHealthSlowly); // 不同的健康计算方式

第二种优化方式是结合模板方法:

class GameCharacter; // 前置声明
// 以下函数是计算健康指数的缺省算法
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter {
   public:
        typedef std::function<int (const GameCharacter&)> HealthCalcFunc;
        explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf)
        {}
        int healthValue() const
        {
            return healthFunc(*this);
        }
        ...
    private:
        HealthCalcFunc healthFunc;
};

初看的时候会觉得,这两个有差异吗?

但是这样做会大大提升健康指数的调用灵活性,这里给出例子:

// 函数类型 健康计算函数 其返回值是非int
short calcHealth(const GameCharacter&); 

// 函数对象
struct HealthCalculator {
    int operator()(const GameCharacter&) const {...}
};

// 其他类型的类,但是提供相对应的接收const GameCharacter&并返回数值的类 注意后续的std::bind与之匹配的使用方式
class GameLevel {
    public:
        float health(const GameCharacter&) const; // 注意这里返回的是non-int类型
        ...
};

// 这与第一种方式并无差异
class EvilBadGuy: public GameCharacter {
    public:
        explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc) : GameCharacter(hcf)
        {
            ...
        }
        ...
};

// 另一个任务类型 假设其构造函数与EvilBadGuy通
class EyeCandyCaracter: public GameCharacter {
    ...
};

// 下面是使用
// 人物1 使用某个函数计算健康指数
EviBadGuy ebg1(calcHealth);

// 人物2 使用某个函数对象计算健康指数
EyeCandyCaracter ecc1(HealthCalculator());

GameLevel currentLevel;
...
// 人物3 使用某个成员函数计算健康指数
EvilBadGuy ebg2(std::bind(&GameLevel::health, currentLevel, _1));

第三种方式,就是使用古典Strategy模式,这个方式在了解设计模式的情况下很容易想到,这里不再列出。

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

本质还是在于在C++中,基类的non-virtual函数采用的是静态绑定而非动态绑定方式,如果重新定义了基类的non-virtual函数,那么实际上就是对基类同名函数的覆盖操作,这样在多态的形式调用时会表现出"精神分裂"或者说不符合直觉的调用结果。例如:

class B {
    public:
        void mf();
};

class D {
    public:
        void mf();
};

D x;
D *pD = &x;
B *pB = &x;

pD->mf(); // 调用D::mf
pB->mf(); // 调用B::mf 而非D::mf! 呕吼 你确认是你希望的结果吗?

这与virtual类型定义函数重写有区别。

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

此条款限定为重新定义带缺省参数的virtual函数。原因在于virtual函数系动态绑定,而缺省参数值却是静态绑定

静态类型就是在程序中被声明时所采用的类型,示例:

class Shape {
public:
    enum ShapColor {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:
    /*
    请注意,当客户以对象调用此函数,一定要指定参数值。因为静态绑定下这个函数并不从其base继承缺省参数值。
    但若以指针(或reference)调用此函数,可以不指定参数值。
    因为动态绑定下这个函数会从其base继承缺省参数值。
    */
    virtual void draw(ShapeColor color) const;
};

// 使用示例
Shape* ps; // 静态类型为Shape*
Shape* pc = new Circle; // 静态类型为Shape*
Shape* pr = new Rectangle; // 静态类型为Shape*

*在本例中,ps,pc和pr都被声明为pointer-to-Shape类型,所以它们都以它为静态类型。注意,不论它们真正指向什么,它们静态类型都是Shape。**

而动态类型则是所指对象的类型,是对象将会有什么行为。动态类型可在程序执行过程中改变。

由于virtual函数是动态绑定而来,调用哪一个函数实现代码,取决于发出调用的那个对象的动态类型。

例如:

pc->draw(Shape::Red); // 调用Circle::draw(Shape::Red);
pr->draw(Shape::Red); // 调用Rectangle::draw(Shape::Red);

但是,正如此条款所言,假设现在有如下的调用方式:

pr->draw(); // 调用Rectangle::draw(Shape::Red); 哦吼 怎么不是预期的Rectangle::draw(Shape::Green)呢?

C++这么实现的原因在于运行效率的权衡。

那么假设我不改变,遵循此条款编写,又会出现什么问题?

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

class Rectangle: public Shape {
public:
    // 注意,这里改成了与base相同的Red类型
    virtual void draw(ShapeColor color = Red) const;
};

那么又会有另外的问题,为什么要在基类和子类里写相同的代码,如果基类改了默认值的行为呢?这就带来了相依性(with dependencies)。

使用NVI优化方式:

class Shape {
public:
    enum ShapColor {Red, Green, Blue};
    void draw(ShapeColor color = Red) const { doDraw(color); }
private:
    virtual void doDraw(ShapeColor color) const = 0;
};
class Rectangle: public Shape {
public:
    // 注意,不须指定缺省参数值
    virtual void doDraw(ShapeColor color) const;
};

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

当实现类时,应当考虑是使用has-a的方式实现还是用has-a的方式实现。

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

private继承可以使用has-a的方式实现。对于EBO(empty base optimization)的慎用场景极少,可不做考虑。

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

多重继承会带来歧义问题,示例:

class BorrowableItem {
public:
    void checkOut();
    ...
};

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

class MP3Player:
    public BorrowableItem,
    public ElectronicGadget
{
    ...        
};

// 调用示例
MP3Player mp3;
mp3.checkOut(); // 调用的是哪一个checkOut?
// 可以尝试使用
mp3.ElectronicGadget::checkOut();

钻石型继承问题:

class File {...};
class InputFile: public File {...};
class OutputFile: public File {...};
class IOFile: Public InputFile, public OutputFile {...};

image-20250803180640253

钻石型继承带来的问题是:是否打算让base class内的成员变量经由每一条路径被复制?例如这里的File假设有一个fileName成员,那么IOFile内有几个这个成员呢?(实际会有两份,但是显然不是预期的场景,没有一个IOFile有两个fileName)。

解决的方式是使用virtual public方式继承:

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

image-20250803181814374

这里补充一个简单的例子作为验证:

#include <iostream>
using namespace std;

class A {
public:
    int x;
    A(int x=0) : x(x) { cout << "A构造: x=" << x << endl; }
};

// B虚继承A
class B : virtual public A {  // 关键:添加virtual
public:
    B(int x=0) : A(x) { cout << "B构造" << endl; }
};

// C虚继承A
class C : virtual public A {  // 关键:添加virtual
public:
    C(int x=0) : A(x) { cout << "C构造" << endl; }
};

// D继承B和C
class D : public B, public C {
public:
    // 关键:虚基类A的构造由最终派生类D直接初始化
    D(int x=0) : A(x), B(100), C(200) { cout << "D构造" << endl; }
};

int main() {
    D d(30);
    cout << "d.x=" << d.x << endl;  // 直接访问,无歧义
    return 0;
}

执行结果:

A构造: x=30
B构造
C构造
D构造
d.x=30

对于虚继承来说,虚基类的构造函数由最终派生类负责调用。

同时需要注意,如果是使用virtual继承,为避免继承带来的成员变量重复,编译器必须提供若干幕后戏法,而其后果是:使用virtual继承的哪些classes所产生的对象往往比使用non-virtual继承的兄弟们体积大,访问virtual base classes的成员变量时,也比访问non-virtual base classes的成员变量速度慢。

书中给出了多重继承和private实现的结合场景,通过private继承将现有类作为辅助类协助实现的场景。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇