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

条款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"的两者结合。

【C++】《Effective C++》第六章的更多相关文章

  1. C++Primer 第六章

    //1.我们通过调用运算符来执行函数.调用运算符的形式是一对圆括号,他作用于一个表达式,该表达式是一个函数或者指向函数的指针.圆括号之内是用逗号分隔的实参列表,用于初始化函数形参.调用表达式的类型就是 ...

  2. 【C++ Primer 第六章】 1. 定义模板

    类模板 题目描述:实现StrBlob的模板版本. /* Blob.h */ #include<iostream> #include<vector> #include<in ...

  3. 《C++Primer》第五版习题答案--第六章【学习笔记】

    <C++Primer>第五版习题答案--第六章[学习笔记] ps:答案是个人在学习过程中书写,可能存在错漏之处,仅作参考. 作者:cosefy Date: 2020/1/16 第六章:函数 ...

  4. C Primer Plus 学习笔记 -- 前六章

    记录自己学习C Primer Plus的学习笔记 第一章 C语言高效在于C语言通常是汇编语言才具有的微调控能力设计的一系列内部指令 C不是面向对象编程 编译器把源代码转化成中间代码,链接器把中间代码和 ...

  5. C primer plus 读书笔记第六章和第七章

    这两章的标题是C控制语句:循环以及C控制语句:分支和跳转.之所以一起讲,是因为这两章内容都是讲控制语句. 第六章的第一段示例代码 /* summing.c --对用户输入的整数求和 */ #inclu ...

  6. 精读《C++ primer》学习笔记(第四至六章)

    第四章: 重要知识点: 4.1 基础 函数调用是一种特殊的运算符,它对运算对象的数量没有限制. 重载运算符时可以定义运算对象的类型,返回值类型,但运算对象的个数,运算符的优先级,结合律无法改变. 当一 ...

  7. C++ Primer Plus学习:第六章

    C++入门第六章:分支语句和逻辑运算符 if语句 语法: if (test-condition) statement if else语句 if (test-condition) statement1 ...

  8. 【C++】《C++ Primer 》第十六章

    第十六章 模板与泛型编程 面向对象编程和泛型编程都能处理在编写程序时不知道类型的情况. OOP能处理类型在程序允许之前都未知的情况. 泛型编程在编译时就可以获知类型. 一.定义模板 模板:模板是泛型编 ...

  9. 【C++】《C++ Primer 》第六章

    第六章 函数 一.函数基础 函数定义:包括返回类型.函数名字和0个或者多个形参(parameter)组成的列表和函数体. 调用运算符:调用运算符的形式是一对圆括号 (),作用于一个表达式,该表达式是函 ...

  10. 精通Web Analytics 2.0 (8) 第六章:使用定性数据解答”为什么“的谜团

    精通Web Analytics 2.0 : 用户中心科学与在线统计艺术 第六章:使用定性数据解答"为什么"的谜团 当我走进一家超市,我不希望员工会认出我或重新为我布置商店. 然而, ...

随机推荐

  1. Linux Vi进入编辑模式后使用方向键的时候,并不会使光标移动,而是在命令行中出现A、B、C、D四个字母

    在linux下,初始使用Vi的时候有两个典型的问题: 1.在编辑模式下使用方向键的时候,并不会使光标移动,而是在命令行中出现A.B.C.D四个字母: 2.当编辑出现错误,想要删除时,发现Backspa ...

  2. 微信小程序api拦截器

    微信小程序api拦截器 完美兼容原生小程序项目 完美兼用小程序api的原本调用方式,无痛迁移 小程序api全Promise化 和axios一样的请求方式 小程序api自定义拦截调用参数和返回结果 强大 ...

  3. gitbook 安装和使用

    gitbook 安装和使用 安装nodejs  wget https://nodejs.org/dist/v10.22.0/node-v10.22.0-linux-arm64.tar.xz tar - ...

  4. css 15-Sass入门

    15-Sass入门 #Sass简介 大家都知道,js 中可以自定义发量,css 仅仅是一个标记语言,不是编程语言,因此不可以自定义发量.不可以引用等等. 面对这些问题,我们现在来引入 Sass,简单的 ...

  5. 装逼篇 | 抖音超火的九宫格视频是如何生成的,Python 告诉你答案

    1. 场景 如果你经常刷抖音和微信朋友圈,一定发现了最近九宫格短视频很火! 从朋友圈九宫格图片,到九宫格视频,相比传统的图片视频,前者似乎更有个性和逼格 除了传统的剪辑软件可以实现,是否有其他更加快捷 ...

  6. HCIP --- BGP属性

    传播范围                 默认值              大优或小优 1. Preference_Value     不传播                      0       ...

  7. HCIP --- BGP 总结

    AS:自治系统  --逻辑管理域(例如移动.电信.联通),AS号范围:0-65535,其中,1-64511:公有AS,64512-65535:私有AS IGP:内部网关协议,在一个AS之内传递的路由协 ...

  8. 如何查看打印机的IP地址和MAC地址

    1.  打开控制面板,选择设备和打印机: 2.  选中打印机,右键单机,选择打印机 "属性": 3. 选择web服务,可以直接查看打印机的IP地址或MAC地址,如下图所示: 4. ...

  9. 编程方式实现MySQL批量导入sql文件

    有时候需要在本地导入一些stage环境的数据到本地mysql,面对1000+的sql文件(包含表结构和数据,放在同一个文件夹下),使用navicat一个一个导入sql文件显然有点太慢了,于是考虑使用s ...

  10. 登录&单点登录介绍

    COOKIE & SESSION & TOKEN 主要用来跟踪会话,识别用户所用.cookie 是客户端,session 是服务端的. 因为 http 是无状态协议,每一次的访问都不知 ...