《Effective C++》学习笔记——条款40

来源:互联网 发布:悦木之源泥娃娃知乎 编辑:程序博客网 时间:2024/05/23 19:31


六、继承与面向对象


条款40、明智而审慎的使用多重继承




两个阵营

一旦提到多重继承,C++社群便会分成两个基本阵营:

  • 如果单一继承(SI - single inheritance)是好的,那么多重继承(MI - multiple inheritance)一定更好
  • 单一继承是好的,但多重继承不值得使用

本条款的目的并不是讨论两者好坏,支持or反对哪个阵营,而是来了解这两个阵营。

两个观点

  1. 多重继承 可能导致歧义
    尤其是当程序可能从多个基类继承相同名称(函数、typedef等)

    class BorrowableItem {
    public:
    void checkOut();

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

    };
    class MP3Player: public BorrowableItem, public ElectronicGadget
    { … };
    MP3Player mp;
    mp.checkOut(); // 此处的checkOut是BorrowableItem类的还是ElectronicGadget类的呢?

注释①

为了解决这种歧义,首先要明确调用哪个基类的函数,然后 带上那个基类

mp.BorrowableItem::checkOut();

当然,此处也可以将 BorrowableItem换成ElectronicGadget,但是会报 无法调用private成员 的错误。

  1. 钻石型多重继承 问题
    当所继承的基类在它们体系中又有共同的基类,会产生钻石型多重继承问题。

    class File { … };
    class InputFile: public File { … };
    class OutputFile: public File { … };
    class IOFile: public InputFile, public OutputFile
    { … };

IOFile继承自InputFile与OutputFile类,但它们各自又有共同的基类File。
对于IOFile类中有多少 File类的成分呢?

  • IOFile应该从每个基类中获取一份成员函数,所以,它应该有两份
  • IOFile对象只该有一个文件名称,所以继承而来的函数不应该重复

C++ 对上述两种都支持,但缺省的方法是 执行复制,也就是有两份。
如果想让其是第二种方法,就需要将高级的基类(也就是File)设置为virtual base class

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


public继承 应该总是 virtual

理由:

  • 使用virtual继承的那些类所产生的对象往往比使用non-virtual继承的兄弟们体积大
  • 访问virtual base class的成员变量时,比访问non-virtual base class的成员变量速度慢
  • 支配 virtual base class 初始化的规则比 non-virtual base情况复杂且不直观

忠告:

  • 非必要,不适用 virtual base,平常使用non-virtual继承
  • 如必须使用virtual base class,尽可能避免在内放置数据


多重继承 的 合理用途

一个关于人的接口类:

class IPerson {public:    virtual ~IPerson();    virtual std::string name() const = 0;    virtual std::string birthDate() const = 0;};

如果想使用IPerson,就必须用它的 指针 或者 引用,因为 抽象类无法被实体化创建对象,为了创建一些可以被使用的对象,就需要用工厂函数将 派生自IPerson的具象类实体化

std::tr1::shared_ptr<IPerson> makePerson(DatabaseID personIdentifier);DatabaseID askUserForDatabaseID();DatabaseID id(askUserForDatabaseID);// 创建一个对象支持Iperson接口,通过成员函数处理*ppstd::tr1::shared_ptr<IPerson> pp(makePerson(id));...

如果这个类名为CPerson,就像具象类一样,CPerson必须提供 继承自IPerson的 pure virtual函数的实现代码:

class PersonInfo {public:    explicit PersonInfo(DatabaseID pid);    virtual ~PersonInfo();    virtual const char* theName() const;    virtual const char* theBirthDate() const;    ...private:    virtual const char* valueDelimOpen() const;    virtual const char* valueDelimClose() const;    ...};

PersonInfo被设计来协助打印各种格式的数据库字段,每个字段值的起点与终点都以特殊字符串为界。缺省的是方括号,比如

[Ring-tailed Lemur]

当然,可以更换界限符号,通过 valueDelimOpen 与 valueDelimClose

const char* PersonInfo::valueDelimOpen() const{    return "[";}const char* PersonInfo::valueDelimClose() const{    return "]";}const char* PersonInfo::theName() const{    // 保留缓冲区给返回值使用    static char value[Max_Formatted_Field_Value_Length];    // 写入起始符号    std::strcpy(value, valueDelimOpen());    ...    // 将value内字符串添加到此对象的name成员变量中(注意,不要超出限制)    // 写入结尾符号    std::strcat(value, valueDelimClose());    return value;}

CPerson和PersonInfo的关系是,PersonInfo有若干函数帮助CPerson实现,所以是 is-implemented-in-terms-of。
本例中,CPerson需要重定义valueDelimOpen与valueDelimClose,所以 单纯的复合无法满足,如果非要用复合,就要用 复合+继承 的形式(令CPerson以private形式继承PersonInfo)

如果 使用private继承

class IPerson {public:    virtual ~IPerson();    virtual std::string name() const = 0;    virtual std::string birthDate() const = 0;};class DatabaseID {  ...  };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;    ...};class CPerson: public IPerson, private PersonInfo {public:    explicit CPerson(DatabaseID pid): PersonInfo(pid) {}    // 实现必要的IPerson成员函数    virtual std::string name() const    {  return PersonInfo::theName();  }    virtual std::string birthDate() const    {  return PersonInfo::theBirthDate();  }private:    // 重定义 继承而来的virtual界限函数    const char* valueDelimOpen() const {  return "";  }    const char* valueDelimClose() const {  return "";  }};


请记住

  • 多重继承比单一继承复杂。它可能导致歧义,以及对virtual继承的需要。
  • virtual继承会增加大小、速度、初始化(或者 赋值)复杂度等等成本。如果virtual base class不带任何数据,将是最具实用价值的情况。
  • 多重继承的确有正当用途。其中一个情节涉及“public 继承某个 Interface class” 和 “private继承某个协助实现的class” 的两相组合。




注释
- ① C++解析重载函数调用的规则:在看到是否有函数可调用之前,C++首先确认这个函数对此调用是否是最佳匹配。找出最佳匹配才去检验科取用性。

0 0
原创粉丝点击