网站开发人员岗位要求招聘平面设计
web/
2025/10/3 12:10:46/
文章来源:
网站开发人员岗位要求,招聘平面设计,南昌装修网站建设,网站建设参考文献资料【六】继承与面向对象设计
条款32 #xff1a; 确保public继承是is a的关系 Item 32: Make sure public inheritance models “is-a”. C面向对象程序设计中#xff0c;最重要的规则便是#xff1a;public继承应当是is-a的关系。当Derived public继…
【六】继承与面向对象设计
条款32 确保public继承是is a的关系 Item 32: Make sure public inheritance models “is-a”. C面向对象程序设计中最重要的规则便是public继承应当是is-a的关系。当Derived public继承自Base时 相当于你告诉编译器和所有看到你代码的人Base是Derived的抽象Derived就是一个Base任何时候Derived都可以代替Base使用。 当然这只适合public继承如果是private继承那是另外一回事了见 Item 39。 比如一个Student继承自Person那么Person有什么属性Student也应该有接受Person类型参数的函数也应当接受一个Student
void eat(const Person p);
void study(const Person p);Person p; Student s;
eat(p); eat(s);
study(p); study(s);语言的二义性
上述例子也好理解也很符合直觉。但有时情况却会不同比如Penguin继承自Bird但企鹅不会飞
class Bird{
public:
vitural void fly();
};
class Penguin: public Bird{
// fly??
};这时你可能会困惑Penguin到底是否应该有fly()方法。但其实这个问题来源于自然语言的二义性 严格地考虑鸟会飞并不是所有鸟都会飞。我们对会飞的鸟单独建模便是
class Bird{...};
class FlyingBird: public Bird{
public:virtual void fly();
};
class Penguin: public Bird{...};这样当你调用penguin.fly()时便会编译错。当然另一种办法是Penguin继承自拥有fly()方法的Bird 但Penguin::fly()中抛出异常。这两种方式在概念是有区别的前者是说企鹅不能飞后者是说企鹅可以飞但飞了会出错。 哪种实现方式好呢Item 18 中提到接口应当设计得不容易被误用最好将错误从运行时提前到编译时。所以前者更好
错误的继承
生活的经验给了我们关于对象继承的直觉然而并不一定正确。比如我们来实现一个正方形继承自矩形
class Rect{...};
void makeBigger(Rect r){int oldHeight r.height();r.setWidth(r.width()10);assert(r.height() oldHeight);
}
class Square: public Rect{...};Square s;
assert(s.width() s.height());
makeBigger(s);
assert(s.width() s.height());根据正方形的定义宽高相等是任何时候都需要成立的。然而makeBigger却破坏了正方形的属性 所以正方形并不是一个矩形因为矩形需要有这样一个性质增加宽度时高度不会变。即Square继承自Rect是错误的做法。 C类的继承比现实世界中的继承关系更加严格任何适用于父类的性质都要适用于子类 本节我们谈到的是is-a关系类与类之间还有着其他类型的关系比如has-a, is-implemented-in-terms-of等。这些在Item-38和Item-39中分别介绍。
条款33 避免隐藏继承而来的名称 条款33Avoid hiding inherited names 简单变量的作用域 这里我们先引入作用域的情况在以下代码简单变量中作用域是这样的 继承类的作用域 那么继承的作用域是如何的呢看以下代码 我们假定derived class内的mf4实现如下 void Derived::mf4(){...mf2();...
}编译器看到名称mf2查找顺序如下 先查看local作用域(也就是mf4覆盖的作用域)——外围作用域Derived覆盖作用域——再外围查找这里是base class的mf2——base class所在的namespace作用域—— global作用域 注上述箭头是在当前没有找到的情况下进行下一步箭头操作
我们再假定重载mf1mf3,并添加一个新版mf3到Derived去。如下图 这里以作用域为基础的“名称遮掩规则”并没有改变因此base class所有名为mf1 mf3都被derived class的mf1 mf3遮掩掉了。 处理“继承而来”的遮掩行为 那如果使用才能搞定C的“继承而来”的缺省遮掩行为 如果Derived以private形式继承Base而Derived唯一想继承的mf1是那个无参版本。using声明式这里就不起作用了因为using声明式会令继承而来的某给定名称之所有同名函数在derived class都可见这里可以使用一个简单的转交函数搞定forwarding function: 注意
derived class 内名称会遮掩base class内的名称。在public继承下没有人希望如此。为了让被遮掩的名称重见天日可使用using声明式和转交函数forwarding function
条款34区分接口继承和实现继承 Item 34: Dirrerentiate between inheritance of interface and inheritance of implementation. 不同于Objective C或者JavaC中的继承接口和实现继承是同一个语法过程。 当你public继承一个类时接口是一定会被继承的见Item32你可以选择子类是否应当继承实现
不继承实现只继承方法接口纯虚函数。继承方法接口以及默认的实现虚函数。继承方法接口以及强制的实现普通函数。
一个例子
为了更加直观地讨论接口继承和实现继承的关系我们还是来看一个例子Rect和Ellipse都继承自Shape。
class Shape{
public:
// 纯虚函数
virtual void draw() const 0;
// 不纯的虚函数impure...
virtual void error(const string msg);
// 普通函数
int id() const;
};
class Rect: public Shape{...};
class Ellipse: public Shape{...};纯虚函数draw()使得Shape成为一个抽象类只能被继承而不能创建实例。一旦被public继承它的成员函数接口总是会传递到子类。
draw()是一个纯虚函数子类必须重新声明draw方法同时父类不给任何实现。id()是一个普通函数子类继承了这个接口以及强制的实现方式子类为什么不要重写父类方法参见 Item 33。error()是一个普通的虚函数子类可以提供一个error方法也可以使用默认的实现。
因为像ID这种属性子类没必要去更改它直接在父类中要求强制实现
危险的默认实现
默认实现通常是子类中共同逻辑的抽象显式地规约了子类的共同特性避免了代码重复方便了以后的增强也便于长期的代码维护。 然而有时候提供默认实现是危险的因为你不可预知会有怎样的子类添加进来。例如一个Airplane类以及它的几个Model子类
class Airplane{
public:
virtual void fly(){// default fly code
}
};
class ModelA: public Airplane{...};
class ModelB: public Airplane{...};不难想象我们写父类Airplane时其中的fly是针对ModelA和ModelB实现了通用的逻辑。如果有一天我们加入了ModelC却忘记了重写fly方法
class ModelC: public Airplane{...};
Airplane* p new ModelC;
p-fly();虽然ModelC忘记了重写fly方法但代码仍然成功编译了这可能会引发灾难。。这个设计问题的本质是普通虚函数提供了默认实现而不管子类是否显式地声明它需要默认实现。
安全的默认实现
我们可以用另一个方法来给出默认实现而把fly声明为纯虚函数这样既能要求子类显式地重新声明一个fly当子类要求时又能提供默认的实现。
class Airplane{
public:
virtual void fly() 0;
protected:
void defaultFly(){...}
}
class ModelA: public Airplane{
public:
virtual void fly(){defaultFly();}
}
class ModelB: public Airplane{
public:
virtual void fly(){defaultFly();}
}这样当我们再写一个ModelC时如果自己忘记了声明fly()会编译错因为父类中的fly()是纯虚函数。 如果希望使用默认实现时可以直接调用defaultFly()。 注意defaultFly是一个普通函数如果你把它定义成了虚函数那么它要不要给默认实现子类是否允许重写这是一个循环的问题。。
优雅的默认实现
上面我们给出了一种方法来提供安全的默认实现。代价便是为这种接口都提供一对函数fly, defaultFly, land, defaultLand, … 有人认为这些名字难以区分的函数污染了命名空间。他们有更好的办法为纯虚函数提供函数定义。 确实是可以为纯虚函数提供实现的编译会通过。但只能通过Shape::draw的方式调用它。
class Airplane{
public:
virtual void fly() 0;
};
void Airplane::fly(){// default fly code
}class ModelA: public Airplane{
public:
virtual void fly(){Airplane::fly();
}
};上述的实现和普通成员函数defaultFly并无太大区别只是把defaultFly和fly合并了。 合并之后其实是有一定的副作用的原来的默认实现是protected现在变成public了。在外部可以访问它
Airplane* p new ModelA;
p-Airplane::fly();在一定程度上破坏了封装但Item 22我们提到protected并不比public更加封装。 所以也无大碍毕竟不管defaultFly还是fly都是暴露给类外的对象使用的本来就不能够封装。 注意
接口继承和实现继承不同。在public继承下derived class总是继承base class的接口pure virtual函数只具体指定接口继承impure virtual函数具体指定接口继承及缺省实现继承non-virtual函数具体指定接口继承以及强制性实现继承
条款 35 考虑virtural函数以外的其他替代设计
补 俩个 设计模式 然后改 Item 35: Consider alternatives to virtual functions. 比如你在开发一个游戏每个角色都有一个healthValue()方法。很显然你应该把它声明为虚函数可以提供默认的实现让子类去自定义它。 这个设计方式太显然了你都不会考虑其他的设计方法。但有时确实存在更好的本节便来举几个替代的所涉及方法。
非虚接口范式NVI idiom可以实现模板方法设计模式Template Method用非虚函数来调用更加封装的虚函数。用函数指针代替虚函数可以实现策略模式。用tr1::function代替函数指针可以支持所有兼容目标函数签名的可调用对象。用另一个类层级中的虚函数来提供策略是策略模式的惯例实现。
NVI实现模板方法模式
模板方法设计模式我们知道实现某个业务的步骤但具体算法需要子类分别实现。 使用非虚接口Non-Virtual Interface Idiom可以实现模板方法模式。比如上面的healthValue声明为普通函数它调用一个私有虚函数doHealthValue来实现。 实现起来是这样的
class GameCharacter{
public:
// 子类不应重新定义该方法见Item 36
int healthValue() const{// do sth. beforeint ret doHealthValue();// do sth. afterreturn ret;
}
private:
// 子类可以重新定义该方法
virtual int doHealthValue() const{// 默认实现
}
}NVI Idiom的好处在于在调用doHealthValue前可以做一些设置上下文的工作调用后可以清除上下文。 比如在调用前给互斥量mutex加锁、验证前置条件、类的不变式调用后给互斥量解锁、验证后置条件、类的不变式等。 上述C代码也有奇怪的地方你可能已经发现了。doHealthValue在子类中是不可调用的然而子类却重写了它。 但C允许这样做是有充分理由的父类拥有何时when调用该接口的权利子类拥有如何how实现该接口的权利。 有时为了继承实现方式子类虚函数会调用父类虚函数这时doHealthValue就需要是protected了。 有时比如析构函数虚函数还必须是public那么就不能使用NVI了。
函数指针实现策略模式
上述的NVI随是实现了模板方法但事实上还是在用虚函数。我们甚至可以让healthValue()完全独立于角色的类只在构造函数时把该函数作为参数传入。
class GameCharacter;int defaultHealthCalc(const GameCharacter gc);class GameCharacter{
public:
typedef int (*HealthCalcFunc)(const GameCharacter);
explicit GameCharacter(HealthCalcFunc hcf defaultHealthCalc): healthFunc(hcf){}
int healthValue() const{return healthFunc(*this);
}
private:
HealthCalcFunc healthFunc;
}这便实现了策略模式。可以在运行时指定每个对象的生命值计算策略比虚函数的实现方式有更大的灵活性
同一角色类的不同对象可以有不同的healthCalcFunc。只需要在构造时传入不同策略即可。角色的healthCalcFunc可以动态改变。只需要提供一个setHealthCalculator成员方法即可。
我们使用外部函数实现了策略模式但因为defaultHealthCalc是外部函数所以无法访问类的私有成员。 如果它通过public成员便可以实现的话就没有任何问题了如果需要内部细节 我们只能弱化GameCharacter的封装。或者提供更多public成员或者将defaultHealthCalc设为friend。 弱化的封装和更灵活的策略是一个需要权衡的设计问题取决于实际问题中动态策略的需求有多大。
tr1::function实现策略模式
C std::tr1::function使用-CSDsN博客
如果你已经习惯了模板编程可能会发现函数指针实现的策略模式太过死板。 为什么不能接受一个像函数一样的东西呢比如函数对象为什么不能是一个成员函数呢为什么一定要返回int而不能是其他兼容类型呢 tr1中给出了解决方案使用tr1::function代替函数指针tr1::function是一个对象 他可以保存任何一种类型兼容的可调用的实体callable entity例如函数对象、成员函数指针等。 看代码 现在tr1在C11标准中已经被合并入std命名空间啦叫做多态函数对象包装器不需要std::tr1::function了可以直接写std::function。
class GameCharacter;
int defaultHealthCalc(const GameCharacter gc);class GameCharacter{
public:
typedef std::functionint (const GameCharacter) HealthCalcFunc;
explicit GameCaracter(HealthCalcFunc hcf defaultHealthCalc): healthCalcFunc(hcf){}
int healthValue() const{return healthFunc(*this);
}
private:
HealthCalcFunc healthFunc;
};注意std::function的模板参数是int (const GameCharacter)参数是GameCharacter的引用返回值是int 但healthCalcFunc可以接受任何与该签名兼容的可调用实体。即只要参数可以隐式转换为GameCharacter返回值可以隐式转换为int就可以。 用function代替函数指针后客户代码可以更加灵活
// 类型兼容的函数
short calcHealth(const GameCharacter);
// 函数对象
struct HealthCalculator{
int operator()(const GameCharacter) const{...}
};
// 成员函数
class GameLevel{
public:
float health(const GameCharacter) const;
};无论是类型兼容的函数、函数对象还是成员函数现在都可以用来初始化一个GameCharacter对象
GameCharacter evil, good, bad;
// 函数
evil(calcHealth);
// 函数对象
good(HealthCalculator());
// 成员函数
GameLevel currentLevel;
bad(std::bind(GameLevel::health, currentLevel, _1));最后一个需要解释一下GameLevel::health接受一个参数const GameCharacter 但事实上在运行时它是需要两个参数的const GameCharacter以及this。只是编译器把后者隐藏掉了。 那么std::bind的语义就清楚了首先它指定了要调用的方法是GameLevel::health第一个参数是currentLevel this是_1即currentLevel细节略过啦这里的重点在于成员函数也可以传入。 如果你写过JavaScript你会发现这就是Function.prototype.bind嘛
经典的策略模式
可能你更关心策略模式本身而不是上述的这些实现现在我们来讨论策略模式的一般实现。 在UML表示中生命值计算函数HealthCalcFunc应当定义为一个类拥有自己的类层级。 它的成员方法calc应当为虚函数并在子类可以有不同的实现。 实现代码可能是这样的
class HealthCalcFunc{
public:
virtual int calc(const CameCharacter gc) const;
};
HealthCalcFunc defaultHealthCalc;
class GameCharacter{
public:
explicit GameCharacter(HealthCalcFunc *phcf defaultHealthCalc): pHealthCalc(phcf){}
int healthValue() const{return pHealthCalc-calc(*this);
}
private:
HealthCalcFunc *pHealthCalc;
};熟悉策略模式的人一眼就能看出来上述代码是策略模式的经典实现。可以通过继承HealthCalcFunc很方便地生成新的策略。 总结
条款 36 不要重写重新定义继承来的noo-vitrual非虚函数 Item 36: Never redefine an inherited non-virtual function. 我们还是在讨论public继承比如Derived继承自Base。如果Base有一个非虚函数func那么客户会倾向认为下面两种调用结果是一样的
Derived d;
Base* pb d;
Derived* pd d;
// 以下两种调用应当等效
pb-func();
pd-func();为什么要一样呢因为public继承表示着is-a的关系每个Derived对象都是一个Base对象Item 32 确保public继承是is a的关系。 然而重写override非虚函数func将会造成上述调用结果不一致
class Base{
public:
void func(){}
};
class Derived: public Base{
public:
void func(){} // 隐藏了父类的名称func见Item 33
};因为pb类型是Base*pd类型是Derived*对于普通函数func的调用是静态绑定的在编译期便决定了调用地址偏移量。 总是会调用指针类型定义中的那个方法。即pb-func()调用的是Base::funcpd-func()调用的是Derived::func。 当然虚函数不存在这个问题它是一种动态绑定的机制。 在子类中重写父类的非虚函数在设计上是矛盾的
一方面父类定义了普通函数func意味着它反映了父类的不变式。子类重写后父类的不变式不再成立因而子类和父类不再是is a的关系。另一方面如果func应当在子类中提供不同的实现那么它就不再反映父类的不变式。它就应该声明为virtual函数。
条款 37 绝不要重新定义继承父类函数的缺省参数值默认参数 Item 37: Never redefine a function’s inherited default parameter value. 不要重写父类函数的默认参数。 因为虽然虚函数的是动态绑定的但默认参数是静态绑定的。只有动态绑定的东西才应该被重写。
静态绑定与动态绑定
静态绑定是在编译期决定的又称早绑定early binding 动态绑定是在运行时决定的又称晚绑定late binding。 举例来讲Rect和Circle都继承自ShapeShape中有虚方法draw。那么
Shape* s1 new Shape;
Shape* s2 new Rect;
Shape* s3 new Circle;
s1-draw(); // s1的静态类型是Shape*动态类型是Shape*
s2-draw(); // s2的静态类型是Shape*动态类型是Rect*
s3-draw(); // s3的静态类型是Shape*动态类型是Circle*在编译期是不知道应该调用哪个draw的因为编译期看到的类型都是一样的Shape*。 在运行时可以通过虚函数表的机制来决定调用哪个draw方法这便是动态绑定。
静态绑定的默认参数
虚函数是动态绑定的但为什么参数是静态绑定的呢这是出于运行时效率的考虑如果要动态绑定默认参数则需要一种类似虚函数表的动态机制。 所以你需要记住默认参数的静态绑定的否则会引起困惑。来看例子吧
Class Shape{public:virtual void draw(int top 1){couttopendl;}
};
class Rect: public Shape{
public:
virtual void draw(int top 2){ // 赋予不同的缺省参数值 couttopendl;
}
};class Circle: public Shape{
public:
virtual void draw(int top){ // 赋予不同的缺省参数值 couttopendl;
}
};Rect* rp new Rect;
Shape* sp rp;
Circle* cp new Circle;sp-draw(); // 调用 Shape::draw()
rp-draw(); // 调用 Rect::draw()
cp-draw(); // 调用 Shape::draw() 一样缺省 但是调用基类的func 各出一半的力气 在Rect中重定义了默认参数为2上述代码的执行结果是这样的 输出 1 2 1 默认参数的值只和静态类型有关是静态绑定的。
最佳实践
为了避免默认参数的困惑请不要重定义默认参数。但当你遵循这条规则时却发现及其蛋疼
class Shape{
public:
virtual void draw(Color c Red) const 0;
};
class Rect: public Shape{
public:
virtual void draw(Color c Red) const;
};代码重复相依性如果父类中的默认参数改了我们需要修改所有的子类。所以最终的办法是避免在虚函数中使用默认参数。可以通过 Item 35 的NVI范式来做这件事情
class Shape{
public:void draw(Color c Red) const{doDraw(color);}
private:virtual void doDraw(Color c) const 0;
};class Rect: public Shapxe{
...
private:virtual void doDraw(Color c) const; // 虚函数没有默认参数啦
};我们用普通函数定义了默认参数避免了在动态绑定的虚函数上定义静态绑定的默认参数。 如标题所见 你唯一应该覆写的东西 —— 动态绑定
条款 38 通过复合模型数模出 has-a 或 根据某物实出现 Item 38: Model “has-a” or “is-implemented-in-terms-of” through composition. 一个类型包含另一个类型的对象时我们这两个类型之间是组合关系。组合是比继承更加灵活的软件复用方法。 Item 32 确保public继承是is a的关系 提到 public继承的语义是is-a的关系。对象组合也同样拥有它的语义就对象关系来讲组合意味着一个对象拥有另一个对象是has-a的关系 复合模型就实现方式来讲组合意味着一个对象是通过另一个对象来实现的是is-implemented-in-terms-of的关系。 (eg set 利用 list实现)
拥有 has-a
拥有的关系非常直观比如一个Person拥有一个name
class Person{
public:string name;
};以…实现 is-implemented-in-terms-of
假设你实现了一个List链表接着希望实现一个Set集合。因为你知道代码复用总是好的于是你希望Set能够继承List的实现。 这时用public继承是不合适的List是可以有重复的这一性质不适用于Set所以它们不是is-a的关系。 这时用组合更加合适Set以List来实现的。
templateclass T // the right way to use list for Set
class Set {
public:bool member(const T item) const;void insert(const T item);void remove(const T item);std::size_t size() const;
private:std::listT rep; // representation for Set data
};Set的实现可以很大程度上重用List的实现比如member方法
templatetypename T bool SetT::member(const T item) const {return std::find(rep.begin(), rep.end(), item) ! rep.end();
}复用List的实现使得Set的方法都足够简单它们很适合声明成inline函数见Item 30。
条款 39 明智而谨慎地使用 private 继承 Item 39: Use private inheritance judiciously. Item 32提出public继承表示is-a的关系这是因为编译器会在需要的时候将子类对象隐式转换为父类对象。 然而private继承则不然
class Person { ... };
class Student: private Person { ... }; // inheritance is now private
void eat(const Person p); // anyone can eatPerson p; // p is a Person
Student s; // s is a Student
eat(p); // fine, p is a Person
eat(s); // error! a Student isnt a PersonPerson可以eat但Student却不能eat。这是private继承和public继承的不同之处
编译器不会把子类对象转换为父类对象父类成员即使是public、protected都变成了private
子类继承了父类的实现而没有继承任何接口因为public成员都变成private了。 因此private继承是软件实现中的概念与软件设计无关。 private继承和对象组合类似都可以表示is-implemented-in-terms-with的关系。那么它们有什么区别呢 在面向对象设计中对象组合往往比继承提供更大的灵活性只要可以使用对象组合就不要用private继承。
private继承
我们的Widget类需要执行周期性任务于是希望继承Timer的实现。 因为Widget不是一个Timer所以我们选择了private继承
class Timer {
public:explicit Timer(int tickFrequency);virtual void onTick() const; // automatically called for each tick
};
class Widget: private Timer {
private:virtual void onTick() const; // look at Widget usage data, etc.
};在Widget中重写虚函数onTick使得Widget可以周期性地执行某个任务。为什么Widget要把onTick声明为private呢 因为onTick只是Widget的内部实现而非公共接口我们不希望客户调用它Item 18 指出接口应设计得不易被误用。 private继承的实现非常简单而且有时只能使用private继承
当Widget需要访问Timer的protected成员时。因为对象组合后只能访问public成员而private继承后可以访问protected成员。当Widget需要重写Timer的虚函数时。比如上面的例子中由于需要重写onTick单纯的对象组合是做不到的。
对象组合
我们知道对象组合也可以表达is-implemented-in-terms-of的关系 上面的需求当然也可以使用对象组合的方式实现。但由于需要重写overrideTimer的虚函数所以还是需要一个继承关系的
class Widget {
private:class WidgetTimer: public Timer {public:virtual void onTick() const;};WidgetTimer timer;
};内部类WidgetTimerpublic继承自Timer然后在Widget中保存一个WidgetTimer对象。 这是public继承对象组合的方式比private继承略为复杂。但对象组合仍然拥有它的好处
你可能希望禁止Widget的子类重定义onTick。在Java中可以使用finel关键字在C#中可以使用sealed。 在C中虽然没有这些关键字但你可以使用public继承对象组合的方式来做到这一点。上述例子便是。减小Widget和Timer的编译依赖。如果是private继承在定义Widget的文件中势必需要引入#includetimer.h。 但如果采用对象组合的方式你可以把WidgetTimer放到另一个文件中在Widget中保存WidgetTimer的指针并声明WidgetTimer即可 见Item 31。
EBO特性
我们讲虽然对象组合优于private继承但有些特殊情况下仍然可以选择private继承。 需要EBOempty base optimization的场景便是另一个特例。 由于技术原因C中的独立空对象也必须拥有非零的大小请看
class Empty {};
class HoldsAnInt {
private:int x;Empty e;
};Empty e是一个空对象但你会发现sizeof(HoldsAnInt) sizeof(int)。 因为C中独立空对象必须有非零大小所以编译器会在Empty里面插入一个char这样Empty大小就是1。 由于字节对齐的原因在多数编译器中HoldsAnInt的大小通常为2*sizeof(int)。更多字节对齐和空对象大小的讨论见Item 7。 但如果你继承了Empty情况便会不同
class HoldsAnInt: private Empty {
private:int x;
};这时sizeof(HoldsAnInt) sizeof(int)这就是空基类优化empty base optimizationEBO。 当你需要EBO来减小对象大小时可以使用private继承的方式。 继承一个空对象有什么用呢虽然空对象不可以有非静态成员但它可以包含typedef, enum, 静态成员非虚函数 因为虚函数的存在会导致一个徐函数指针它将不再是空对象。 STL就定义了很多有用的空对象比如unary_function, binary_function等。
总结
private继承的语义是is-implemented-in-terms-of通常不如对象组合。但有时却是有用的比如方法protected成员、重写虚函数。不同于对象组合private继承可以应用EBO库的开发者可以用它来减小对象大小 对象尺寸最小化。 条款 40 明智而审慎地使用多重继承 Item 40: Use multiple inheritance judiciously. 多继承Multiple InheritanceMI是C特有的概念在是否应使用多继承的问题上始终争论不断。一派认为单继承Single InheritanceSI是好的所以多继承更好 另一派认为多继承带来的麻烦更多应该避免多继承。本文的目的便是了解这两派的视角。具体从如下三个方面来介绍
多继承比单继承复杂引入了歧义的问题以及虚继承的必要性虚继承在大小、速度、初始化/赋值的复杂性上有不小的代价当虚基类中没有数据时还是比较合适的多继承有时也是有用的。典型的场景便是public继承自一些接口类private继承自那些实现相关的类。
歧义的名称
多继承遇到的首要问题便是父类名称冲突时调用的歧义。如
class A{
public:void func();
};
class B{
private:bool func() const;
};
class C: public A, public B{ ... };C c;
c.func(); // 歧义c.B::func(); // 没有歧义 需要明确指出 但是B::func 是 private的 多继承菱形
当多继承的父类拥有更高的继承层级时可能产生更复杂的问题比如多继承菱形deadly MI diamond。如图
class File{};
class InputFile: public File{};
class OutputFile: public File{};
class IOFile: public InputFile, public OutputFile{};这样的层级在C标准库中也存在例如basic_ios, basic_istream, basic_ostream, basic_iostream。 IOFile的两个父类都继承自File那么File的属性比如filename应该在IOFile中保存一份还是两份呢 这是取决于应用场景的就File::filename来讲显然我们希望它只保存一份但在其他情形下可能需要保存两份数据。 C还是一贯的采取了自己的风格都支持默认是保存两份数据的方式。如果你希望只存储一份可以用virtual继承
class File{};
class InputFile: virtual public File{};
class OutputFile: virtual public File{};
class IOFile: public InputFile, public OutputFile{};可能多数情况下我们都是希望virtual的方式来继承。但总是用virtual也是不合适的它有代价
虚继承类的对象会更大一些虚继承类的成员访问会更慢一些虚继承类的初始化更反直觉一些。继承层级的最底层most derived class负责虚基类的初始化而且负责整个继承链上所有虚基类的初始化。
基于这些复杂性Scott Meyers对于多继承的建议是
如果能不使用多继承就不用他如果一定要多继承尽量不在里面放数据也就避免了虚基类初始化的问题。
接口类
这样的一个不包含数据的虚基类和Java或者C#提供的Interface有很多共同之处这样的类在C中称为接口类 我们在Item 31中介绍过。一个Person的接口类是这样的
class IPerson {
public:virtual ~IPerson();virtual std::string name() const 0;virtual std::string birthDate() const 0;
};由于客户无法创建抽象类的对象所以必须以指针或引用的方式使用IPerson。 需要创建实例时客户会调用一些工厂方法比如
shared_ptrIPerson makePerson(DatabaseID personIdentifier);同时继承接口类与实现类
在Java中一个典型的类会拥有这样的继承关系
public class A extends B implements IC, ID{}继承B通常意味着实现继承继承IC和ID通常意味着接口继承。在C中没有接口的概念但我们有接口类 于是这时就可以多继承
class CPerson: public IPerson, private PersonInfo{};PersonInfo是私有继承因为Person是借助PersonInfo实现的。 Item 39提到对象组合是比private继承更好的实现继承方式。 但如果我们希望在CPerson中重写PersonInfo的虚函数那么就只能使用上述的private继承了这时就是一个合理的多继承场景。 现在来设想一个需要重写虚函数的场景 比如PersonInfo里面有一个print函数来输出name, address, phone。但它们之间的分隔符被设计为可被子类定制的
class PersonInfo{
public:
void print(){char d delimiter();coutnamedaddressdphone;
}
virtual char delimiter() const{ return ,; }
};CPerson通过private继承复用PersonInfo的实现后便可以重写delimiter函数了
class CPerson: public IPerson, private PersonInfo{
public:
virtual char delimiter() const{ return :; }
...
};至此完成了一个合理的有用的多继承MI的例子。
总结
我们应当将多继承视为面向对象设计工具箱中一个有用的工具。相比于单继承它会更加难以理解 如果有一个等价的单继承设计我们还是应该采用单继承。但有时多继承确实提供了清晰的、可维护的、合理的方式来解决问题。 此时我们便应该理智地使用它。
多继承比单继承复杂引入了歧义的问题以及虚继承的必要性虚继承在大小、速度、初始化/赋值的复杂性上有不小的代价当虚基类中没有数据时还是比较合适的多继承有时也是有用的。典型的场景便是public继承自一些接口类private继承自那些实现相关的类。
参考 https://zhuanlan.zhihu.com/p/536534500 https://zhuanlan.zhihu.com/p/63609476 http://gapex.web.fc2.com/c_plusplus/book/EffectiveC3rdEdition.pdf https://harttle.land/effective-cpp.html
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/86210.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!