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

Effective C++ 中文版 第三版》读书笔记

身为 class 设计者,有时候你希望 derived class 只继承成员函数的接口(也就是声明):有时候你又希望 derived class 同时继承函数的接口和实现,但又希望能够覆写(override)它们所继承的实现:又有时候你希望 derived class 同时继承函数的接口与实现,并且不允许覆写任何东西。

让我们考虑一个展现绘图程序中各种几何形状的 class 继承体系:

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 的 pure virtual 函数 draw 使它成为一个抽象 class。Shape 强烈影响了所有以 public 形式继承它的 derived classes。因为:

成员函数的接口总是会被继承。

首先考虑 pure virtual 函数 draw:pure virtual 函数有两个最突出的特性:它们必须被任何 “继承了它们” 的具象 class 重新声明,而且它们在抽象 class 中通常没有定义。所以:

声明一个 pure virtual 函数的目的是为了让 derived class 只继承函数接口。Shape::draw 的声明式乃是对具象 derived class 设计者说 “你必须提供一个 draw 函数,但我不干涉你怎么实现它”

令人意外的是,我们竟然可以为 pure virtual 函数提供定义。但调用它的唯一途径是 “调用时明确指出其 class 名称”:

Shape* ps = new Shape; // wrong! abstract class Shape 
Shape* ps1 = new Rectangle; 
ps1->draw(); // Rectangle::draw() 
Shape* ps2 = new Ellipse; 
ps2->draw(); // Ellipse::draw() 
ps1->Shape::draw(); 
ps2->Shape::draw();

它可以提供一种机制,为简朴 impure virtual 函数提更平常更安全的缺省实现。

impure virtual 函数会提供一份实现代码,derived classes 可能覆写(override)它:

生命简朴的(非纯)impure virtual 函数的目的,是让 derived class 继承该函数的接口和缺省实现。

考虑 Shape::error 这个例子。

其接口表示,每个 class 都必须支持一个 “当遇上错误时可调用” 的函数,但每个 class 可自由处理错误。如果某个 class 不想针对错误作出任何特殊行为,可以退回到 Shape class 提供的缺省错误处理行为。

但是允许 impure virtual 函数同时指定函数声明和函数缺省行为,却有可能造成危险。考虑 xyz 航空公司设计的继承体系。该公司只有 A 和 B 两种飞机,相同方式飞行:

class Airport {...}; 

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

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

class ModelA: public Airplane {...}; 

class ModelB: public Airplane {...};

为表示所有飞机都一定能飞,并阐明 “不同型飞机原则上需要不同的 fly 实现” Airplane::fly 被声明为 virtual。然而为了避免在 ModelA 和 ModelB 中撰写相同代码,缺省飞行行为有 Airplane::fly 提供,它同时被 ModelA 和 ModelB 继承。

现在,xyz 决定购买一种新式 C 型飞机,这种飞机的飞行方式不同。

xyz 的程序员在继承体系中对 C 型飞机添加了一个 class,但由于他们急着让新飞机上线服务,忘了重新定义器 fly 函数:

class ModelC: public Airplane {...};

然后在代码中有一些诸如此类的动作:

Airport PDX(...); 
Airplane* pa = new ModelC; 
... 
pa->fly(PDX);

这将酿成大灾难:这个程序试图用 ModelA 和 ModelB 的飞行方式来飞 ModelC。

问题不在 Airplane::fly() 有缺省行为,而在于 ModelC 在未明白说出 “我要” 的情况下就继承了该缺省行为。我们可以做到 “提供缺省实现给 derived classes,但除非它们明确要求,否则免谈”。此间伎俩在于切断 “virtual 函数接口” 和其 “缺省实现” 之间的连接。下面是一种做法:

class Airplane { 

public: 
    virtual void fly(const Airport& destination) = 0; 
    ... 

protected: 
    void defaultFly(const Airport& destination); 
}; 

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

fly 已被改成为一个 pure virtual 函数,只提供飞行接口。缺省行为以 defaultFly 出现在 Airplane class 中。若想使用缺省实现(例如 ModelA 和 ModelB),可以在 fly 中对 defaultFly 做一个 inline 调用:

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

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

现在 ModelC 不可能意外继承不正确的 fly 实现代码了,因为 Airplane 中的 pure virtual 函数迫使 ModelC 必须提供自己的 fly 版本:

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

void ModelC::fly(const Airport& destination) 
{ 
    //将C型飞机飞至指定的目的地 
}

这个方案并非安全无虞,程序员还是可能因为剪贴(copy-and-paste)代码而招来麻烦,但它比原来的设计值得依赖。

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

有些人反对以不同的函数分别提供接口和缺省实现,向上述的 fly 和 defaultFly 那样。我们可以利用 “pure virtual 函数必须在 derived class 中重新声明,但它们可以拥有自己的实现” 这一事实。下面是 Airplane 继承体系如何给 pure virtual 函数一份定义:

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

void Airplane::fly(const Airport& destination) // pure virtual 函数实现 
{ 
    // 缺省行为,将飞机飞至指定目的地 
} 

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

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

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

void ModelC::fly(const Airport& destination) 
{ 
    // 将 C 型飞机飞至指定目的地 
}

身为 class 设计者,有时候你希望 derived class 只继承成员函数的接口(也就是声明):有时候你又希望 derived class 同时继承函数的
接口和实现,但又希望能够覆写(override)它们所继承的实现:又有时候你希望 derived class 同时继承函数的接口与实现,并且不允许覆写任何东西。
让我们考虑一个展现绘图程序中各种几何形状的 class 继承体系:

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 的 pure virtual 函数 draw 使它成为一个抽象 class。Shape 强烈影响了所有以 public 形式继承它的 derived classes。因为:成员函数的接口总是会被继承。

首先考虑 pure virtual 函数 draw:pure virtual 函数有两个最突出的特性:它们必须被任何 “继承了它们” 的具象 class 重新声明,而且它们在抽象 class 中通常没有定义。所以:声明一个 pure virtual 函数的目的是为了让 derived class 只继承函数接口。

Shape::draw 的声明式乃是对具象 derived class 设计者说 “你必须提供一个 draw 函数,但我不干涉你怎么实现它”

令人意外的是,我们竟然可以为 pure virtual 函数提供定义。但调用它的唯一途径是 “调用时明确指出其 class 名称”:

Shape* ps = new Shape; //wrong! abstract class Shape 
Shape* ps1 = new Rectangle; 
ps1->draw(); //Rectangle::draw() 
Shape* ps2 = new Ellipse; 
ps2->draw(); //Ellipse::draw() 
ps1->Shape::draw(); 
ps2->Shape::draw();

它可以提供一种机制,为简朴 impure virtual 函数提更平常更安全的缺省实现。
impure virtual 函数会提供一份实现代码,derived classes 可能覆写(override)它:
生命简朴的(非纯)impure virtual 函数的目的,是让 derived class 继承该函数的接口和缺省实现。

考虑 Shape::error 这个例子。

其接口表示,每个 class 都必须支持一个 “当遇上错误时可调用” 的函数,但每个 class 可自由处理错误。如果某个 class 不想针对错误作出任何特殊行为,可以退回到 Shape class 提供的缺省错误处理行为。
但是允许 impure virtual 函数同时指定函数声明和函数缺省行为,却有可能造成危险。考虑 xyz 航空公司设计的继承体系。该公司只有 A 和 B 两种飞机,相同方式飞行:

class Airport {...}; 

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

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

class ModelA: public Airplane {...}; 

class ModelB: public Airplane {...};

为表示所有飞机都一定能飞,并阐明 “不同型飞机原则上需要不同的 fly 实现” Airplane::fly 被声明为 virtual。然而为了避免在 ModelA 和 ModelB 中撰写相同代码,缺省飞行行为有 Airplane::fly 提供,它同时被 ModelA 和 ModelB 继承。

现在,xyz 决定购买一种新式 C 型飞机,这种飞机的飞行方式不同。

xyz 的程序员在继承体系中对 C 型飞机添加了一个 class,但由于他们急着让新飞机上线服务,忘了重新定义器 fly 函数:

class ModelC: public Airplane {...};

然后在代码中有一些诸如此类的动作:

Airport PDX(...); 
Airplane* pa = new ModelC; 
... 
pa->fly(PDX);

这将酿成大灾难:这个程序试图用 ModelA 和 ModelB 的飞行方式来飞 ModelC。
问题不在 Airplane::fly() 有缺省行为,而在于 ModelC 在未明白说出 “我要” 的情况下就继承了该缺省行为。我们可以做到 “提供缺省实现给 derived classes,但除非它们明确要求,否则免谈”。此间伎俩在于切断 “virtual 函数接口” 和其 “缺省实现” 之间的连接。下面是一种做法:

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

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

fly 已被改成为一个 pure virtual 函数,只提供飞行接口。缺省行为以 defaultFly 出现在 Airplane class 中。若想使用缺省实现(例如 ModelA 和 ModelB),可以在 fly 中对 defaultFly 做一个 inline 调用:

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

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

现在 ModelC 不可能意外继承不正确的 fly 实现代码了,因为 Airplane 中的 pure virtual 函数迫使 ModelC 必须提供自己的 fly 版本:

class ModelC: public Airplane { 
public: 
    virtual void fly(const Airport& destination); 
    ... 
};
 
void ModelC::fly(const Airport& destination) 
{ 
    // 将 C 型飞机飞至指定的目的地 
}

这个方案并非安全无虞,程序员还是可能因为剪贴(copy-and-paste)代码而招来麻烦,但它比原来的设计值得依赖。

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

有些人反对以不同的函数分别提供接口和缺省实现,向上述的 fly 和 defaultFly 那样。我们可以利用 “pure virtual 函数必须在 derived class 中重新声明,但它们可以拥有自己的实现” 这一事实。下面是 Airplane 继承体系如何给 pure virtual 函数一份定义:

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

void Airplane::fly(const Airport& destination) // pure virtual 函数实现 
{ 
    //缺省行为,将飞机飞至指定目的地 
}
 
class ModelA: public Airplane { 
public: 
    virtual void fly(const Airport& destination) 
    { 
        Airplane::fly(destination); 
    } 
    ... 
}; 

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

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

void ModelC::fly(const Airport& destination) 
{ 

    // 将C型飞机飞至指定目的地 
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,980评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,422评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,130评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,553评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,408评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,326评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,720评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,373评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,678评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,722评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,486评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,335评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,738评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,283评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,692评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,893评论 2 335

推荐阅读更多精彩内容