欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Effective C++条款40:继承与面向对象之(明智而审慎地使用多重继承)

程序员文章站 2022-07-15 12:37:19
...

一、多重继承中,接口调用的歧义性

  • 当一个类继承自两个基类时,两个基类包含有相同的名称(如函数、typedef等),那么调用时就会产生歧义性

演示案例

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

class ElectronicGadget {
private:
    bool checkOut()const; //注意,此处的为private
};

//多重继承
class MP3Player :public BorrowableItem, public ElectronicGadget { };

int main()
{
    MP3Player mp;
    mp.checkOut(); //错误,歧义性
    return 0;
}
  • 上面的代码中,虽然ElectronicGadget中的checkOut()函数为private的,但是调用仍然会产生歧义性。因为在调用checkOut()之前,C++会解析代码,发现在两个基类中都存在,因此报错
  • 正确的做法是:明确指出调用哪一个base class内的函数,例如:
MP3Player mp;

mp.BorrowableItem::checkOut();  //正确
mp.ElectronicGadget::checkOut();//错误,ElectronicGadget中的checkOut()为private

二、菱形继承与虚(virtual)继承

三、virtual继承的代价

  • 虽然virtual继承有优点,但是还是会付出一定的代价,例如:
    • virtual继承所创建的对象比non-virtual继承创建的对象体积大
    • 访问virtual base class的成员变量时,也比访问non-virtual base class的成员变量速度慢
    • virtual base的初始化责任是由继承体系中最底层的class负责的。因此:
      • 派生类必须为virtual base进行初始化,不论它们距离有多远
      • 当一个新的派生类加入继承体系中,它也必须承担起virtual base的初始化责任

四、多重继承演示案例

  • 现在我们来重新塑模“人”的C++ Interface class(参阅条款31)

IPerson类

  • 下面是一个抽象类:其中包含纯虚函数name()和birthDate()
class IPerson {
public:
    virtual ~IPerson();
    virtual std::string name()const = 0;      //返回人的名称
    virtual std::string birthDate()const = 0; //返回生日
};
  • name()和birthDate()两个虚函数返回人物的名称和生日
  • IPerson必须使用pointera或references指向于派生类来编写程序,因为抽象类无法实例化。下面创建一个factory functions(工厂函数,见条款31),在其中使用IPerson的派生类创建一个对象,然后返回这个对象(返回值类型为IPerson)。代码如下:
class DatabaseID {};

//参数为一个数据库ID对象
shared_ptr<IPerson> makePerson(DatabaseID personIdentifier)
{
    //在其中使用IPerson的派生类创建一个对象
    //然后返回该对象
}

DatabaseID askUserForDatabaseID()
{
    //该函数返回一个DatabaseID对象
}

int main()
{
    //创建一个DatabaseID对象
    DatabaseID id(askUserForDatabaseID());

    //使用makePerson函数创建一个IPerson对象
    shared_ptr<IPerson> pp(makePerson(id));

    return 0;
}

PersonInfo类

  • 现在假设有一个和数据库相关的类,提供一些CPerson类(定义在下面)所需要的实质东西
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;
};
  • valueDelimOpen()、valueDelimClose():
    • 功能:每个字段值的起点和结尾都以特殊字符串为界
    • 缺省的头尾界限符号是方括号。例如Ring-tailed Lemur将被格式化为:[Ring-tailed Lemur]
    • 每个人可能喜欢不同的界限符号,所以这两个virtual函数允许派生类自己定义不同的头尾界限符号。例如可能PersonInfo的派生类可能会重写这两个虚函数,代码如下:
//缺省的虚函数,派生类可以重写
const char* valueDelimOpen()const
{
    return "[";
}

const char* valueDelimClose()const
{
    return "]";
}
  • theName()、theBirthDate():用来返回相关的数据库字段(名字、生日等)。下面以theName()为例:
const char* theName()const {
    static char value[Max_Formatted_Field_Value_Length];
    std::strcpy(value, valueDelimOpen());

    //将名字添加进value
    
    std::strcat(value, valueDelimClose());
    return value;
}

CPerson类

  • CPerson是最终的表示“人”的类,其继承于IPerson和PersonInfo
  • 公有继承于IPerson:
    • 因为IPerson的name()和birthDate()两个虚函数返回未经修饰的人物的名称和生日,并且IPerson为抽象类,因此CPerson以public继承于IPerson
  • 私有继承于PersonInfo:
    • PersonInfo已经提供了返回修饰的人名和生日的虚函数,因此CPerson可以利用PersonInfo来实现,这是一种is-implemented-in-terms-of(根据某物实现出)模式
    • 在前几篇文章中,我们介绍了:复合与private继承都可以实现is-implemented-in-terms-of模式。但是由于PersonInfo中有虚函数,派生类可以重写其虚函数,因此我们建议使用private继承
    • 所以最终CPerson私有继承于PersonInfo
  • 最终的代码如下:
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;
private:
    virtual const char* valueDelimOpen()const;
    virtual const char* valueDelimClose()const;
};

class CPerson :public IPerson, private PersonInfo {
public:
    explicit CPerson(DatabaseID pid) :PersonInfo(pid) {}

    virtual std::string name()const = 0{
        return PersonInfo::theName();
    }
    virtual std::string birthDate()const = 0 {
        return PersonInfo::theBirthDate();
    }
private:
    virtual const char* valueDelimOpen()const;
    virtual const char* valueDelimClose()const;
};
  • 在CPerson中:
    • 其重写了IPerson中的name()和birthDate(),在其中返回人的名字和生日
    • 由于private继承于PersonInfo,并且PersonInfo中已经实现了从数据库中读取并格式化人的名字和生日的功能,因此在name()和birthDate()中分别调用PersonInfo的theName()、theBirthDate()即可
    • 并且自己可以重写valueDelimOpen()、valueDelimClose()函数,来重写格式化人的名字和生日的格式

Effective C++条款40:继承与面向对象之(明智而审慎地使用多重继承)

  • 当单一继承和多重继承可以实现相同的功能时,尽量选择单一继承

五、总结

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