effective c++ 笔记 条款32-40

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

public inheritance(公开继承)意味 “is-a”(是一种)的关系。子是父,父不是子,父具有一般性,子具有特殊性“public继承”意味is-a。适用于base classes身上的每一件事情一定也适用于derived classes身上,因为每一个derived class对象也都是一个base class对象。
要搞清楚子类是否必须拥有父类每一个特性,如果以生活经验判断,可能会出现错误
例1,企鹅是鸟,如果企鹅了继承了鸟,一般鸟的基类会默认能飞,但是企鹅不能飞。
例2,正方形是特殊的矩形,但是矩形拥有两个变量宽高,但是正方形只能有一个变量,因为语法层面没法保证两个变量永远相等。
在确定是否需要public继承的时候,我们首先要搞清楚子类是否必须拥有父类每一个特性,如果不是,则无论生活经验是什么,都不能视作”is-a”的关系。public继承关系不会使父类的特性或接口在子类中退化,只会使其扩充。

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

类似名称查找法则,继承体系中,当在子类使用一个名字时,编译器优先查找子类覆盖的作用域,未找到,再查找父类作用域,最后查找全局作用域

class Base {
public:
    void mf();
    void mf(double);
};

class Derived : public Base {
public:
    void mf();
};

此时,子类的实例无法调用父类的重载函数,因为子类的mf覆盖了父类的mf
一种解决方式是,using关键字

class Derived : public Base {
public:
    using Base::mf;
};

会将父类中所有使用到名称mf的函数全部包含在子类中,包括其重载版本
如果不想要全部版本,只想要单一版本,尤其private继承时,使用转发函数

class Base {
public:
    virtual void mf();
    virtual void mf(double);
};

class Derived : public Base {
public:
    virtual void mf() {
        Base::mf();
    }
};

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

不同类型的函数代表了父类对子类实现过程中不同的期望。

  1. 在父类中声明纯虚函数,是为了强制子类拥有一个接口,并强制子类提供一份实现。
  2. 在父类中声明虚函数,是为了强制子类拥有一个接口,并为其提供一份缺省实现。
  3. 在父类中声明非虚函数,是为了强制子类拥有一个接口以及规定好的实现,并不允许子类对其做任何更改(条款36要求我们不得覆写父类的非虚函数)
    普通虚函数可能出现的一个问题是,子类不适用父类的缺省实现,但如果忘记定制自己的实现。普通虚函数无法从代码层面提醒开发者。解决方式是,使用纯虚函数,并提供默认实现
class Airplane {
public:
    virtual void Fly() = 0;
};

void Airplane::Fly() {
        // 缺省实现
}

class Model : public Airplane { 
public:
    virtual void Fly() override {
        Airplane::Fly();
    }
};

从这里我们可以看出,将纯虚函数、虚函数区分开的并不是在父类有没有实现——纯虚函数也可以有实现,其二者本质区别在于父类对子类的要求不同,前者在于从编译层面提醒子类主动实现接口,后者则侧重于给予子类自由度对接口做个性化适配

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

  1. 藉由非虚接口手法实现 template method NVI:用public的非虚函数来调用private的虚函数具体实现,非虚函数必须为子类继承且不得更改
    注:C++ 允许 derived class 覆写 base class 的 private virtual 方法
    优点:可以在调用 private virtual 函数前后做一些额外的事情。调用之前可以做的工作:锁定互斥器,制造运转日志记录项,验证 class 约束条件,验证函数先决条件等等。调用之后可以做的工作:互斥器解除锁定,验证函数的事后条件,再次验证 class 约束条件等等
    缺点:某些class继承体系中,virtual函数必须调用其base class的版本,这就导致virtual函数必须是protected而不能是private,有些时候virtual函数甚至一定得是public
  2. 藉由函数指针实现 Strategy 模式:以对同一种子类对象赋予不同的函数实现,或者在运行时更改某对象对应的函数实现
    优点:同一类不同实例,可以用不同的实现函数,只要提供不同的初始化函数指针,叶可能通过添加set函数在运行期变更
    缺点:实现的函数只能访问public成分,如果需要non-public信息,就有问题。除非弱化封装,例如声明未friend,或者提供public访问函数
  3. 藉由 std::function 完成 Strategy 模式:允许包括函数指针在内的任何可调用物搭配一个兼容于需求的签名式。可以是函数对象,可以是某个成员函数,也可以用不同返回值的(只要能转换成需求的返回值类型)
// 使用返回值不同的函数
short CalcHealth(const GameCharacter&);
// 使用函数对象(仿函数)
GameCharacter chara1(CalcHealth);
struct HealthCalculator {
    int operator()(const GameCharacter&) const { ... }
};
GameCharacter chara2(HealthCalculator());
// 使用某个成员函数
class GameLevel {
public:
    float Health(const GameCharacter&) const;
    ...
};
GameLevel currentLevel;
GameCharacter chara2(std::bind(&GameLevel::Health, currentLevel, std::placeholders::_1));
  1. 古典的 Strategy 模式:将虚函数也做成另一个继承体系类,然后添加一个指针来指向该继承体系的对象。容易辨认,想添加新的方法,添加子类即可。

条款 36:绝不重新定义继承而来的非虚函数

非虚函数执行的是静态绑定,由对象类型本身(静态类型,声明时的类型)决定调用的函数,编译时确定
虚函数执行的是动态绑定,决定因素不在对象本身,在于目标所指的对象类型(称之动态类型),运行期决定
虚函数的意思是“接口一定被继承,但实现可以在子类更改”,而非虚函数的意思是“接口和实现都必须被继承”

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

条款 36 中我们已经否定了重新定义非虚函数的可能性,因此此处我们只讨论带有缺省参数值的虚函数
虚函数是动态绑定而来,但缺省参数值却是静态绑定
因此可能会在“调用一个定义于子类的虚函数”的同时,却使用父类为它所指定的缺省参数值(如果是父类指针/引用指向子类对象)
所以虚函数不要写缺省参数值,子类自然也不要改,虚函数要从始至终保持没有缺省参数值
可以使用条款35 NVI手法,让非虚函数外壳拥有缺省参数
绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数——你唯一应该覆写的东西——却是动态绑定。

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

两个类的关系除了继承,还有类的复合
public继承是is-a,复合意味着has-a(有一个)或 is-implemented-in-terms-of (根据某物实现出)
注意is-a与is-implemented-in-terms-of 区分
如果对象只是你的类的某个东西,这个对象属于应用域部分,是has-a。如果对象负责你的类的实现细节,是规则的制定者和执行者,那这个的对象属于实现域,是 is-implemented-in-terms-of
has-a:如 一个人可以拥有名字,地址

class Address;
class Person {
public:
private:
    std::string name;  //复合对象
    Address& Address;
};

is-implemented-in-terms-of:如用list实现一个set

template <typename T>
class Set {
public:
    bool Contains(const T& Item) const;
    //...
private:
    std::list<T> Rep;
};
template <typename T>
bool Set<T>::Contains(const T& Item) const {
    bool Result=std::find(Rep.begin(),Rep.end(),Item)!=Rep.end();
    return Result;
}

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

private不是is-a关系

class Person{...};
class Student: private Person{...};
void eat(const Person& p);
void study(const Student& s);
Person p;
Student s;
eat(p);
eat(s); //error! public继承的话没问题,编译器会暗自转换类型
  1. 对于private继承,编译器不回子的将一个子类对象转成一个父类对象。
  2. private继承的所有父类成员,在子类中都会变成private属性

private 继承意味着:is-implemented-in-terms-of (根据某物实现出)。private 继承可以看作纯粹是为了实现细节,需要的不是类似 public 继承可以向外提供接口,仅仅是为了让derived class采用base class中已经具备的某种特性。derived和base之间并没有什么直接意义上的联系
当需要用一个类去实现另一个类,尽可能用复合,除非必要,不要采用private继承

  1. 当你的private子类继续派生时,可能想阻止重定义虚函数,类似final关键字,但因为private继承,无法实现(derived class可以重新定义private virtual函数,即使它们不能调用它,条款35),但如果是复合的形式将复合的类作为一个private成员,当前的子类继续派生时,可以防止复合类被继承后重新定义虚函数
  2. 降低编译依存性,如果是继承,子类被编译时,父类的定义必须可见。如果是复合的方式内含其他类的指针,只需要带一个简单的前置声明

当需要对工具类的某些方法(虚函数)做重载时,应选择private继承,这些方法一般都是工具类内专门为继承而设计的调用或回调接口,需要用户自行定制实现。
另一个极端案例是适用于你所处理的 class 不带任何数据时。这样的 class 不存在任何成员函数或变量

class Empty {};
class HoldsAnInt {
private:
    int x;
    Empty e;
};

此时 sizeof(HoldsAnInt) > sizeof(int) , 因为一个不含任何成员的class大小为1,编译器会默认放一个char。经过内存对其后,前置大小为8

class Empty {};

class HoldsAnInt :private Empty{
private:
    int x;
};

此时 sizeof(HoldsAnInt) == sizeof(int) ,这种表现就是所谓的 EBO(empty base optimization)空白基类最优化

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

问题1:多重继承可能会引发歧义(ambiguity)行为。
解决办法:指明调用

mp.BorrowableItem::checkOut();
mp.ElectronicGadget::checkOut(); //报错,private函数

问题2:钻石型多重继承
解决办法:virtual继承
是否打算让棱形顶部的父类内的成员变量经过每一条路径被复制,如果不想要这样,应当使用虚继承,指出其愿意共享父类
但虚继承会在子类中额外存储信息来确认成员来自于哪个父类,会付出更多空间和速度的代价。虚基类的初始化责任是由继承体系中最底层的派生类负责,就导致了虚基类必须认知其虚基类并且承担虚基类的初始化责任
因此;非必要不使用虚继承。如果必须使用虚继承,尽可能避免在虚基类中放置数据

多重继承的确有正当用途。其中一个情节涉及“public 继承某个 Interface class” 和"private 继承某个协助实现的 class"

// IPerson 类指出要实现的接口
class IPerson {
public:
    virtual ~IPerson();
    virtual std::string Name() const = 0;
    virtual std::string BirthDate() const = 0;
};

class DatabaseID {  ...  };

// PersonInfo 类有若干已实现的函数
// 可用以实现 IPerson 接口
class PersonInfo {
public:
    explicit PersonInfo(DatabaseID pid);
    virtual ~PersonInfo();
    virtual const char* TheName() const;
    virtual const char* TheBirthDate() const;
    virtual const char* ValueDelimOpen() const;
    virtual const char* ValueDelimClose() const;
    ...
};

// CPerson 类使用多重继承
class CPerson: public IPerson, private PersonInfo {
public:
    explicit CPerson(DatabaseID pid): PersonInfo(pid) {}

    virtual std::string Name() const {       // 实现必要的 IPerson 成员函数
        return PersonInfo::TheName();  
    }

    virtual std::string BirthDate() const {  // 实现必要的 IPerson 成员函数
        return PersonInfo::TheBirthDate();  
    }
private:
    // 重新定义继承而来的虚函数
    const char* ValueDelimOpen() const {  return "";  }
    const char* ValueDelimClose() const {  return "";  }
};

PersonInfo刚好有若干函数可以帮助CPerson比较容易实现出来,IPerson是接口类

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值