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

  以C++进行面向对象编程,最重要的一个规则是:public inheritance(公开继承)意味“is-a”(是一种)的关系。请务必牢记。当你令class D 以public形式继承class B,你便是告诉C++编译器,每一个类型为D的对象同时也是一个类型为B的对象,反之不成立。你的意思是B比D表现出更一般化的概念,而D比B表现出更特殊化的概念。is-a关系只对public继承才成立,private继承的意义于此完全不同(条款39),至于protected继承,至今仍困惑着我。考虑如下例子:

class Person { ...  };
class Student : public Person { ... };

  每个学生都是人,但并非每一个都是学生。对人可以成立的每一件事,对学生也都成立。任何函数如果期望获得一个类型为Person(或pointer-to-Person 或 reference-to-Person)的实参,也都愿意接受一个Student对象(或pointer-to-Student或 reference-to-Student)。

  然而,考虑这种情况:企鹅是一种鸟,这是事实。鸟可以飞,这也是事实。如此,下面代码:

class Bird
{
public:
virtual void fly(); // 鸟可以飞
.....
}; class Penguin:public Bird // 企鹅是一种鸟
{
.....
};

  有问题:这个继承体系说企鹅可以飞!!我们对上述代码作出如下修改(Method1):

class Bird
{
..... // 没有声明fly函数
};
class FlyingBird:public Bird
{
public:
virtual void fly();
.....
};
class Penguin:public Bird // 企鹅是一种鸟
{
..... // 没有声明fly函数
};

  此刻,这样的继承体系较好的满足了我们的真正意思。然而,对某些系统而言,可能不需要区分会飞的鸟和不会飞的鸟,原先修改之前的“双classes继承体系”就已经满足需求了。这反映出一个事实:世界上并不存在一个“适用于所有软件”的完美设计。所谓最佳设计,取决于系统希望做什么事,包括现在与未来。

  对原先代码的令一种修改如下(Method2):

void error(const std::string& msg);     // 定义于另外某处
class Penguin:public Bird // 企鹅是一种鸟
{
virtual void fly() { error("Attempt to make a penguin fly!"); } // 运行期发生错误
.....
};

  注意:这里并不是说“企鹅不会飞”,而是说“企鹅会飞,但尝试那么做是一种错误”。Method2是在程序运行期发生错误,然而,我们知道,把错误提前到编译器是较佳的选择:

class Bird
{
........ // 没有声明fly函数
};
class Penguin:public Bird
{
........ // 没有声明fly函数
};
// 那么下面调用会使编译器报错
Penguin p;
p.fly(); // 错误

  is-a并非是唯一的classes之间的关系。另两个常见的关系是has-a(有一个)和is-implemented-in-terms-of(根据某物实现出)。这些关系将在条款38和39讨论。将上述这些重要的相互关系中的任何一个误塑为is-a而造成的错误设计,在C++中并不罕见,所以,你务必了解这些“classes相互关系”之间的差异。

故而:

  “public继承”意味着is-a。适用于base classes身上的每一件事情一定也适用于derived class身上,因为每一个derived class对象也都是一个base class对象。

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

  derived classes内的名称会遮掩base classes内的名称,即使base classes 和 derived classes 内的函数有不同的参数类型也适用,而且不论函数式virtual 或 non-virtual一体适用。在public继承下从来没有人希望如此。考虑如下代码:

class Base
{
private:
int x;
public:
virtual void mf1() = ;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
.....
};
class Derived : public Base
{
public:
virtual void mf1();
void mf3();
void mf4();
.....
}; //考虑下面调用
Derived d;
int x;
....
d.mf1(); // ok,调用Derived::mf1
d.mf1(x); // error, 因为Derived::mf1遮掩了Base::mf1
d.mf2(); // ok, 调用Base::mf2
d.mf3(); // ok, 调用Derived::mf3
d.mf3(x); //error, 因为Derived::mf3遮掩了Base::mf3

  Method1:如果你继承base class 并加上重载函数,而你又希望重新定义或覆写其中一部分,那么你必须为那些原本会被遮掩的每个名称引入一个using声明式,否则某些你希望继承的名称会被遮掩(using声明式会令继承而来的某给定名称之所有同名函数在derived class 中都可见。)。如下修改上述代码:

class Base
{
private:
int x;
public:
virtual void mf1() = ;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
.....
};
class Derived : public Base
{
public:
using Base::mf1; // 让Base class内名为mf1 和mf3的所有东西
using Base::mf3; // 在Derived 作用域内都可见
virtual void mf1();
void mf3();
void mf4();
.....
}; //考虑下面调用
Derived d;
int x;
....
d.mf1(); // ok,调用Derived::mf1
d.mf1(x); // ok, 调用Base::mf1
d.mf2(); // ok, 调用Base::mf2
d.mf3(); // ok, 调用Derived::mf3
d.mf3(x); //ok, 调用Base::mf3

  Method2:有时你并不想继承base classes的所有函数(当然,这在public继承下是绝对不允许的,因为它违反了public继承的“is-a”关系),这时可以采用一个简单的转交函数:

class Base
{
..... // 与前同
};
class Derived : public Base
{
public:
virtual void mf1() // 转交函数
{ Base::mf1(); } // 暗自成为inline,条款30
}; //考虑下面调用
Derived d;
int x;
....
d.mf1(); // ok,调用Derived::mf1
d.mf1(x); // error, Base::mf1() 被遮掩了

故而:

  1. derived classes内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。

  2. 为了让被遮掩的名称再见天日,可使用using声明式或转交函数。

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

  1. 成员函数的接口总是会被继承。条款32所说,public继承意味is-a(是一种)。

  2. 声明一个pure virtual 函数的目的是为了让derived classes 只继承函数接口。令人意外的是,C++竟然允许我们为pure virtual函数提供定义,但调用它的唯一途径是“调用时明确指出其class名称”。(baseObj->BaseName::virtualFuncName();)

  3. 声明简朴的(非纯)impure virtual函数的目的,是让derived classes继承该函数的接口和缺省实现。但是,允许impure virtual函数同时指定函数声明和函数缺省行为,却有可能造成危险。考虑如下代码:

// 现有A型、B型两种飞机,以相同方式飞行
class Airport
class Airplane
{
public:
virtual void fly(const Airport& destination);
....
};
void Airplane::fly(const Airport& destination)
{
// 缺省代码
} class ModelA:public Airplane { ... };
class ModelB:public Airplane { ... }; //现有一C型飞机,以不同于A、B型的飞行方式飞行
class ModelC:public Airplane { ... }; //竟然忘记重新定义C型飞机的fly函数 // 如下调用会导致灾难
Airport pdx(...);
Airplane* pa = new ModelC;
...
pa->fly(pdx); //

  上面代码的问题不在Airplane::fly有缺省行为,而在于ModelC在未明白说出“我要”的情况下就继承了该缺省行为(而此缺省行为却不是ModelC想要的)。我们必须做到“提供缺省实现给derived classes,但除非它们明白要求否则免谈”,解决方法在于切断“virtual函数接口”和其“缺省实现”之间的连接。可作如下修改:

// 现有A型、B型两种飞机,以相同方式飞行
class Airport
class Airplane
{
public:
virtual void fly(const Airport& destination) = ; // 纯虚函数,迫使子类定义自己的实现
....
protected:
void defaultFly(const Airport& destination);
};
void Airplane::defaultFly(const Ariport& destination)
{
// 缺省实现
} class ModelA:public Airplane
{
public:
virtual void fly(const Airport& destination) //
{ defaultFly(destination); }
...
};
class ModelB:public Airplane
{
public:
virtual void fly(const Airport& destination)
{ defaultFly(destination); }
...
}; class ModelB:public Airplane
{
public:
virtual void fly(const Airport& destination);
...
};
void ModelC::fly(const Airport& destination)
{
// 实现C型飞机特有的飞行方式
}

  3. 声明non-virtual函数的目的是为了令derived classes继承函数的接口及一份强制性实现。

故而:

  1. 接口继承和实现继承不同。在public继承之下,derived classes总是继承base class的接口。

  2. pure virtual函数只具体指定接口继承。

  3. 简朴的(非纯)impure virtual函数具体指定接口继承及缺省实现继承。

  4. non-virtual 函数具体指定接口继承以及强制性实现继承。

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

  假设我们有如下代码,其中有一个virtual函数:

class GameCharacter
{
public:
virtual int healthValue() const; // 返回游戏角色血量
...
};

  Method1. 藉由NVI(Non-Virtual Interface)手法实现Template Method 模式(一种设计模式):

class GameCharacter
{
public:
int healthValue() const
{
... // 做一些事前工作
int retVal = doHealthValue();
... // 做一些事后工作
return retVal;
}
...
private: // 私有,禁止外部访问(禁止类对象调用,只能在类内部被使用)
     // 私有虚函数,派生类可以重新定义它,healthValue函数为公有非虚函数,派生类会继承此函数.
    但是,父类和派生类对象都 能且只能 通过调用healthValue函数来间接调用私有虚函数,从而保证了基类对于私有虚函数的绝对控制(可以保证在私有虚函数之前/之后做一些工作,
     并且不会因为派生类重新定义私有虚函数而丧失这种保证,因为派生类对象同样只能通过基类的healthValue函数调用到自己的私有虚函数)
     // NVI手法允许derived classes重新定义virtual函数,从而赋予它们“如何实现机能”的控制能力,但base class保留诉说“函数何时被调用”的权利.
virtual int doHealthValue() const // derived classes可重新定义它
{
...
}
...
};

  这一基本设计,也就是“令客户通过public non-virtual成员函数间接调用private virtual函数”,称为non-virtual interface(NVI)手法。它是所谓Template Method设计模式(与C++ templates并无关联)的一个独特表现形式。我把这个non-virtual函数称为virtual函数的外覆器。

  NVI手法的一个优点隐式在上述代码注释“做一些事前工作”和“做一些事后工作”之中。这意味着外覆器确保得以在一个virtual函数被调用之前设定好适当场景,并在调用结束之后清除场景。

  注意,NVI手法涉及在derived classes内重新定义private virtual函数。“重新定义virtual函数”表示某些事“如何”被完成,“调用virtual函数”则表示它“何时”被完成。NVI手法允许derived classes重新定义virtual函数,从而赋予它们“如何实现机能”的控制能力,但base class保留诉说“函数何时被调用”的权利。

  Method2(函数指针). 藉由Function Pointers实现Strategy模式:角色构造函数接受一个指针,指向一个健康计算函数(函数指针)。

class GameCharacter;   // 前置声明
// 以下函数是计算健康指数的缺省算法
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter
{
public:
typedef int (*HealthCalcFunc)(const GameCharacter&); // 函数指针HealthCalcFunc
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
: healthFunc(hcf)
{} int healthValue() const
{ return healthFunc(*this); }
...
private:
HealthCalcFunc healthFunc;
};

  这个做法是常见的Strategy设计模式的简单应用,它具有以下特点:

  (1)优点:同一个游戏角色之不同实体可以有不同的健康计算函数。

class EvilBagGuy : public GameCharacter
{
public:
explicit EvilBagGuy(HealthCalcFunc hcf = defaultHealthCalc)
: GameCharacter(hcf)
{ ... }
...
};
int loseHealthQuickly(const GameCharacter&);
int loseHealthSlowly(const GameCharacter&);
// 不同角色搭配不同计算方式
EvilBagGuy ebg1(loseHealthQuickly);
EvilBagGuy ebg2(loseHealthSlowly);

  (2)优点:游戏角色健康计算函数可在运行期变更。例如GameCharacter可提供一个成员函数setHealthCalcator,用来替换当前的健康计算函数。

  (3)缺点:角色的健康计算需要用到class 内的non-public信息的时候就会有问题(non-member 函数无权访问 non-public 成分)。唯一能够解决“需要以non-member函数访问class 的non-public成分”的办法就是:弱化class 封装。例如class可声明那个non-member函数为friends,或是为其实现的某一部分提供public访问函数(getter函数)。

  Method3(函数对象). 藉由tr1::function 完成Strategy模式:tr1::function对象可以包装任何可调用物(也就是函数指针、函数对象、或成员函数指针),只要其签名式兼容于需求端。具体可参见C++11新特性之八——函数对象function。可对Method2代码修改成使用tr1::function:

class GameCharacter;   // 前置声明
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter
{
public:
// HealthCalcFunc可以是任何“可调用物”,可被调用并接受
// 任何兼容于GameCharacter之物,返回任何兼容于int的东西。详下:
typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
: healthFunc(hcf)
{} int healthValue() const
{ return healthFunc(*this); }
...
private:
HealthCalcFunc healthFunc;
};

  和前一个设计(其GameCharacter持有的是函数指针)比较,这个设计几乎相同。唯一不同的是如今GameCharacter持有一个tr1::function对象,相当于一个指向函数的泛化指针。这时,角色构造函数能接受的事物更具弹性:

short calcHealth(const GameCharacter&);      // 普通计算函数,返回non-int

struct HealthCalculator       // 使用clas类似
{
int operator() (const GameCharacter&) const // 重写()操作符
{ ... }
}; class GameLevel
{
public:
float health(const GameCharacter&) const; //成员函数,返回non-int
...
}; class EvilBagGuy : public GameCharacter
{
....
}; class EyeCandyCharacter : public GameCharacter
{
....
}; EvilBagGuy ebg1(calcHealth); // 普通函数
EyeCandyCharacter ecc1(HealthCalculator()); //函数对象
GameLevel currentLevel;
...
EvilBagGuy ebg2( // 成员函数
std::tr1::bind(&GameLevel::health,
currentLevel,
_1)
);

  这里需要对上述代码中的tr1::bind作下解析:首先表明,为计算ebg2的健康指数,应该使用GameLevel class 成员函数health。GameLevel::health 宣称它自己接受一个参数(那是个reference指向GameCharacter),但它实际上接受两个参数,因为它也获得一个隐式参数GameLevel,也就是this所指的那个。然而GameCharacter的健康计算函数只接受单一参数:GameCharacter(这个对象将被计算出健康指数)。如果我们使用GameLevel::health作为ebg2的健康计算函数,我们必须以某种方式转换它,使它不再接受两个参数(一个GameCharacter 和一个 GameLevel),转而接受单一参数(一个GameCharacter)。在这个例子中我们必然会想要使用currentLevel作为“ebg2的健康计算函数所需的那个GameLevel对象”,于是我们将currentLevel绑定为GameLevel对象,让它在“每次GameLevel::health被调用以计算ebg2的健康”时被使用。那正是tr1::bind的作为:它指出ebg2的健康计算函数应该总是以currentLevel作为GameLevel对象。tr1::bind具体见C++11新特性之二——std::bind  std::function 高级用法

  Method4(接口类):古典的Strategy模式:本例中为,每一个GameCharacter对象都内含一个指针,指向一个来自HealthCalcFunc继承体系的对象。可画出UML设计图:

class GameCharacter;   // 前置声明
class HealthCalcFunc
{
public:
...
virtual int calc(const GameCharacter& gc) const
{ ... }
...
};
HealthCalcFunc defaultHealthCalc; class GameCharacter
{
public:
explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc)
: pHealthFunc(phcf)
{} int healthValue() const
{ return pHealthFunc->calc(*this); }
...
private:
HealthCalcFunc* pHealthFunc;
}

  这个做法典型的采用了Strategy模式。另外,它还提供“将一个既有的健康算法纳入使用”的可能性——只要为HealthCalcFunc继承体系添加一个derived class即可。

小结:
  1. 使用non-virtual interface(NVI)手法,那是Template Method设计模式的一种特殊形式。它以public non-virtual成员函数包裹较低访问性的virtual函数。

  2. 将virtual函数替换为“函数指针成员变量”,这是Strategy设计模式的一种分解表现形式。

  3. 以tr1::function成员变量替换virtual函数,因而允许使用任何可调用物搭配一个兼容于需求的签名式。这也是Strategy设计模式的某种形式。

  4. 将继承体系内的virtual函数替换为另一个继承体系内的virtual函数。这是Strategy设计模式的传统实现手法。

故而:

  1. virtual函数的替代方案包括NVI手法即Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。

  2. 将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class 的non-public成员。

  3. tr1::function对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式兼容”的所有可调用物。

条款36: 绝不重新定义继承而来的non-virtual函数

  考虑下面代码:

class B
{
public:
void mf();
....
};
class D : public B
{
public:
void mf(); // 遮掩B::mf,条款33
.....
}; D x; // D 对象
B* pB = &x;
D* pD = &x;
// 针对D对象x,下面语句竟然调用的是不同的mf
pB->mf(); // 调用B::mf
pD->mf(); // 调用D::mf

  造成此一两面行为的原因是,non-virtual函数如B::mf 和 D::mf都是静态绑定(条款37)。这意思是,由于pB被声明为一个pointer-to-B,通过 pB调用的 non-virtual 函数永远是B所定义的版本,即使pB指向一个类型为”B派生之class”的对象。更明确地说,当mf(base class 内为 non-virtual 并在derived class 内被重载的函数)被调用,如何一个D对象都可能表现出B或D的行为;决定因素不在对象自身,而在于“指向该对象之指针”当初的声明类型。References的行径类似。(virtual函数是动态绑定,所以不受这个问题之苦,条款07也解释过为什么动态性base classes内的析构函数应该是virtual.)。

参见C++易混淆知识点整理第3点

故而:

  绝对不要重新定义继承而来的non-virtual函数。

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

  让我们一开始就将讨论简化。你只能继承两种函数:virtual和non-virtual函数。然而重新定义一个继承而来的non-virtual函数永远是错误的(条款36),所以我们可以安全地将本条款的讨论局限于“继承一个带有缺省参数值的virtual函数”。这种情况下,本条款成立的理由就非常直接而明确了:virtual函数系动态绑定,而缺省参数值却是静态绑定。

class Shape
{
public:
enum ShapeColor{ Red, Green, Blue }; // 提供类定义所必须的常量
virtual void draw(ShapeColor color = Red) const = ;
.....
};
class Rectangle : public Shape
{
public:
virtual void draw(ShapeColor color = Green) const;
...
};
class Circle : public Shape
{
public:
virtual void draw(ShapeColor color) const;
// 请注意:
// 以上这么写,则当客户以对象调用此函数,一定要指定参数值。
// 因为静态绑定下这个函数并不从其base 继承缺省参数值。
// 但若以指针(或reference)调用此函数,可以不指定参数值。
// 因为动态绑定下这个函数会从其base 继承缺省参数值。
...
};
// 考虑下面指针
Shape* ps; // 静态类型为Shape*, 没有动态类型, 尚未指向任何对象
Shape* pc = new Circle; // 静态类型为Shape*, 动态类型为Circle*
Shape* pr = new Rectangle; // 静态类型为Shape*, 动态类型为Rectangle* // 动态类型一如其名称所示,可在程序执行过程中改变(通常经由赋值完成)
ps = pc; // ps 的动态类型如今是Circle*
ps = pr; // ps 的动态类型如今是Rectangle*

  对象的所谓“静态类型”是指它在程序中被声明时所采用的类型(初始)。对象的所谓“动态类型”是指“目前所指对象的类型”(当前)。针对上面代码,考虑如下调用:

// 下面调用正常
pc->draw(Shape::Red); // 调用Cricle::draw(Shape::Red)
pr->draw(Shape::Red); // 调用Rectangle::draw(Shape::Red)
// 然而,下面调用则会出现问题
// 一如上面所说,virtual函数是动态绑定,而缺省参数值却是静态绑定
// 下面调用,你可能会在“调用一个定义于derived class内的virtual函数”的同时
// 却使用base class为它所指定的缺省参数值:
pr->draw(); // 调用Rectangle::draw(Shape::Red)

  此例子中,pr的动态类型是Rectangle*,所以调用的是Rectangle的virtual函数(动态绑定),一如你所预期。Rectangle::draw函数的缺省参数值应该是Green,但由于pr的静态类型是Shape*,所以此一调用的缺省参数值来自Shape class而非Rectangle class!(即使指针换成reference,问题依然存在)。

  C++为什么坚持以这种乖张的方式来运作呢?答案在于运行期效率。如果缺省参数值是动态绑定,编译器就必须有某种办法在运行期为virtual函数决定适当的参数缺省值。这比目前实行的“在编译器决定”的机制更慢而且更复杂。

  如果,客户想尝试遵守这条规则,并且同时提供缺省参数值给base 和 derived classes 的用户,如何:

class Shape
{
public:
enum ShapeColor{ Red, Green, Blue }; // 提供类定义所必须的常量
virtual void draw(ShapeColor color = Red) const = ;
.....
};
class Rectangle : public Shape
{
public:
virtual void draw(ShapeColor color = Red) const;
...
};

  不幸的是,上面这样的代码导致代码重复。更糟的是,代码重复又带着相依性:如果Shape内的缺省参数值改变了,所有“重复给定缺省参数值”的那些derived classes也必须改变,否则它们最终会导致“重复定义一个继承而来的缺省参数值”。条款35列了不少virtual函数的替代设计,其中之一是NVI(non-virtual interface)手法:令base classes 内的一个public non-virtual 函数调用private virtual函数,后者可被derived classes重新定义。可作如下修改:

class Shape
{
public:
enum ShapeColor{ Red, Green, Blue }; // 提供类定义所必须的常量
void draw(ShapeColor color = Red) const // non-virtual 函数
{
doDraw(color); // 调用一个virtual
}
.....
private:
virtual void doDraw(ShapeColor color) const = ; // 真正的工作在此处完成
};
class Rectangle : public Shape
{
public:
...
private:
virtual void doDraw(ShapeColor color) const; //注意,不须指定缺省参数值,但必须提供实现(可实现子类特定的颜色),因为base class 内为纯虚函数(pure virtual)
...
};

由于non-virtual 函数应该绝对不被derived classes覆写(条款36),这个设计很清楚地使得draw函数的color缺省参数值总是Red。
故而:

  绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定的,而virtual函数——你唯一应该覆写的东西——却是动态绑定。

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

  复合(composition)是类型之间的一种关系,当某种类型的对象内含它种类型的对象,便是这种关系。条款32曾说,“public继承”带有is-a(是一种)的意义。复合也有它自己的意义。实际上它有两个意义。复合意味has-a(有一个,包含关系)或 is-implemented-in-terms-of(根据某物(底层)实现出(适配器),类似STL的容器适配器如Stack,Queue等)。那是因为你正打算在你的软件中处理两个不同领域(domains)。程序中的对象其实相当于你所塑造的世界中的某些事物,例如人、汽车等等。这样的对象属于应用域部分。其他对象则纯粹是实现细节上的人工制品,如缓冲区、互斥器等等。这些对象相当于你的软件的实现域。当复合发生于应用域内的对象之间,表现出has-a的关系;当它发生于实现域内则是表现is-implemented-in-terms-of的关系。

  区分is-a和has-a并不困难,比较麻烦的是区分is-a(是一种)和 is-implemented-in-terms-of(根据某物实现出)这两种对象关系。  

  假如我们现在需要一组classes用来表现由不重复对象组成的sets。那么,考虑如下操作:
  1. 复用标准程序库提供的set template。不幸的是,标准程序库的set以平衡查找树实现,使它们在查找、安插、移除元素保证拥有对数时间效率,但空间销毁较大。而假设我们的程序,空间比效率更重要,那么标准程序库set template便不满足;

  2. 让你的set template继承std::list,毕竟实现sets的一种方法,便是在底层采用linked lists(链表)。而刚好,标准程序库有一个list template。然而, public继承中,一如条款32所说,对基类为真的每一件事,对派生类也应该为真。但list 可以内含重复元素,而我们的set,很不幸,不能内含重复元素。由于两个classes之间并非is-a关系,所有public继承不适合用来塑模它们。

  解决方案:Set对象可根据一个list对象实现出来。is-implemented-in-terms-of(根据某物实现出,是指使用了某物的特性,但又不完全相同,因为我们可能额外添加了一些特性,也可能在利用某物特性之前和之后做一些特殊操作,外覆在“某物特性”上面。)

template<class T>
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::list<T> rep; // 用来表述set数据
}; // Set成员函数可大量倚赖list 及标准程序库其他部分提供的机能来完成
template<typename T>
bool Set<T>::member(const T& item) const
{
return std::find(rep.begin(), rep.end(), item) != rep.end();
} template<typename T>
void Set<T>::insert(const T& item)
{
if(!member(item)) rep.push_back(item); // 判重
} template<typename T>
void Set<T>::remove(const T& item)
{
// typename 见条款42
typename std::list<T>::iterator it = std::find(rep.begin(), rep.end(), item);
if (it != rep.end()) rep.erase(it);
} template<typename T>
std::size_t Set<T>::size(const T& item) const
{
return rep.size();
}

故而:
  1. 复合的意义和public继承完全不同。

  2. 在应用域,复合意味has-a(有一个)。在实现域,复合意味is-implemented-in-terms-of(根据某物实现出)。

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

  private继承意味is-implemented-in-terms-of(根据某物实现出)。如果你让class D以private形式继承class B,你的用意是为了采用class B 内已经备妥的某些特性,不是因为B对象和D对象存在任何观念上的关系。private继承纯粹只是一种实现技术(这就是为什么继承自一个private base class 的每样东西在你的class 内都是private:因为它们都只是实现枝节而已)。借用条款34 提出的术语,private 继承意味只有实现部分被继承,接口部分应略去。如果D 以private 形式继承 B,意思是 D 对象根据 B 对象实现而得,再没有其他意涵了。private 继承在软件“设计”层面上没有意义,其意义只及于软件“实现”层面。
  条款38 指出复合的意义也是is-implemented-in-terms-of(根据某物实现出)。对于这两者,尽可能使用复合,必要时才使用private继承。何时才必要呢?主要是当protected 成员(派生类能访问到父类的protected 成员,所以必须是在类的继承体系之下;而复合不属于类的继承体系下)和/或 virtual 函数(virtual 函数会导致动态绑定,这也需要在类的继承体系之下完成)。
   考虑这样一种情况:假设我们有个Widget class, 现在我们想在运行期间周期性的记录审查它的每个函数被调用次数,此刻,我们需要某种定时器,代码如下:

class Timer
{
public:
explicit Timer(int tickFrequency);
virtual void onTick() const; // 定时器每滴答一次,此函数就被自动调用一次
....
}; // Method1:以private形式继承Timer
class Widget : private Timer
{
private:
virtual void onTick() const;
...
}; // Method2:public 继承 加 复合
class Widget
{
private:
class WidgetTimer : public Timer
{
public:
virtual void onTick() const;
...
};
WidgetTimer timer;
....
};

  这里,我们可能会选择Method2,原因:

  (1)你或许回想设计Widget 使它得以拥有derived classes,但同时你可能会想阻止derived classes重新定义onTick。如果Widget 继承自Timer,上面的想法就不可能实现,即使是private 继承也不可能(条款35 曾说过,derived classes 可以重新定义 virtual函数,即使它们不得调用(virtual 是私有)),但如果WidgetTimer 是 Widget 内部的一个private 成员并继承 Timer,Widget 的derived classes 将无法取用WidgetTimer(派生类 也无法访问 基类的 private 成员),因此无法继承它或重新定义它的virtual函数。

  (2)你或许会想将Widget 的编译依存性降至最低。如果Widget 继承Timer,当Widget 被编译时Timer 的定义必须可见,所以定义Widget 的那个文件恐怕必须#include Timer.h。但如果WidgetTimer 移出Widget之外而Widget内含指针指向一个WidgetTimer,Widget 可以只带着一个简单的WidgetTimer 声明式,不再需要#include 任何与Timer有关的东西,条款31。

  有一种特殊情况,可能会促使你选择“private 继承” 而不是 “继承加复合”:你所处理的class不带任何数据。这样的class (1)没有non-static成员变量,(2)没有virtual函数(因为这种函数的存在会为每个对象带来一个vptr,条款07),(3)也没有virtual base classes(因为这样的base classes 也会招致体积上的额外开销,条款40)。于是这种所谓的empty classes 对象不使用任何空间,因为没有任何隶属对象的数据需要存储。然而由于技术上的理由,C++裁定凡是独立(非附属)对象都必须有非零大小,所以,如果你这样做:

class Empty { };     // 没有数据,所以其对象应该不使用任何内存

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

  你会发现sizeof(HoldsAnInt) > sizeof(int);一个Empty成员变量竟然要求内存!在大多数编译器中sizeof(Empty)获得1,因为面对“大小为零之独立(非附属)对象”,通常C++官方勒令默默安插一个char到空对象内。然而齐位需求(条款50)可能造成编译器为类似HoldsAnInt 这样的 class 加上一个衬垫,所以有可能HoldsAnInt 对象不只获得一个char 大小,也许实际上被放大到足够有存放一个int。

  但或许你注意到了,我很小心地说“独立(非附属)”对象的大小一定不为零。也就是说,这个约束不适用于derived class对象内的base class成分,因为它们并非独立(非附属)。如果你继承Empty,而不是内含一个那种类型的对象:

class Empty { };     // 没有数据,所以其对象应该不使用任何内存

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

  几乎可以确定sizeof(HoldsAnInt) > sizeof(int)。这是所谓的EBO(empty base optimization,空白基类最优化),我试过所有的编译器都有这样的结果。值得注意的是,EBO 一般只在单一继承(而非多重继承)下才可行。

  现实中的“empty”classes并不真的是empty。虽然它们从未拥有non-static成员变量,却往往内含typedefs,enums,static成员变量,或non-virtual函数。STL 就有许多技术用途的empty classes,其中内含有用的成员(通常是typedefs),包括base classes unary_function 和 binary_function ,这些是“用户自定义之函数对象”通常会继承的classes。EBO的广泛实践,使这样的继承很少增加derived classes的大小。

故而:

  1. Private 继承意味 is-implemented-in-terms-of(根据某物实现出)。它通常比复合的级别低。但是当derived class需要访问 protected base class 的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。即便如此,一个混合了public继承和复合的设计,也能达成你所要的行为。

  2. 和复合不同,private 继承可以造成empty base 最优化。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。

参见 C++易混淆知识点整理第10点

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

  (1) 首先,需要认清一个事实,当多重继承(multiple inheritance;MI)进入设计景框,程序有可能从一个以上的base classes 继承相同的名称(如函数,typedef等等),那会导致较多的歧义机会。

class BorrowableItem
{
public:
void checkOut(); //
....
};
class ElectronicGadget
{
private:
bool checkOut() const;
.....
};
class MP3Player :
public BorrowableItem,
public ElectronicGadget
{ .... }; MP3Player mp;
mp.checkOut(); // 歧义,调用的是哪个checkOut ?

  注意此例中对checkOut的调用是歧义(模棱两可)的,即使两个函数之中只有一个可取用(BorrowableItem内的checkOut是public, ElectronicGadget内的却是private)。这与C++用来解析重载函数调用的规则相符:在看到是否有个函数可取用之前,C++ 首先确认这个函数对此调用而言是最佳匹配。找出最佳匹配函数后才检查其可取用性(是否最佳匹配—>是否可取用)。本例的两个checkOuts有相同的匹配程度(因此才造成歧义),没有所谓最佳匹配。因此ElectronicGadget::checkOut的可取用性也就从未被编译器审查。

  为了解决这个歧义,你必须明白指出你要调用哪一个base class 内的函数:

mp.BorrowableItem::checkOut();    // 明白调用此函数
// 尝试调用ElectronicGadget::checkOut 将会获得一个“尝试调用private成员函数”的错误

  (2) 多重继承可能导致“钻石型多重继承”(菱形继承):

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

  这样,File 中的成员变量将沿着InputFile和OutputFile两条路径被复制,最终在IOFile中造成两份成员变量。而C++缺省也是这么做的。如果你只需要在IOFile中保留一份File的成员变量,那么你必须令File 成为一个 virtual base class(虚基类,不同于抽象基类)。为了这样做,你必须令所有直接继承自它的classes 采用“virtual继承”(虚继承导致虚基类,保留基类的成员变量至多一份。):

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

  virtual 继承所需要付出的成本是巨大的:首先,使用virtual 继承的那些classes 所产生的对象往往比使用non-virtual 继承的兄弟们的体积大,访问virtual base classes 的成员变量时,也比访问non-virtual base classes的成员变量速度慢。其次,virtual base 的初始化责任是由继承体系中的最低层 class 负责,这暗示:(1)classes 若派生自virtual bases而需要初始化,必须认知其virtual bases——不论那些bases 距离多远,(2)当一个新的derived class加入继承体系中,它必须承担其virtual bases(不论直接或间接)的初始化责任。

  所以,对于virtual base class(亦相当于使用non-virtual继承):第一,非必要不使用virtual bases。平常请使用non-virtual继承。第二,如果你必须使用virtual base class,尽可能避免在其中放置数据。Java 和 .NET 的Interfaces 便是不允许含有任何数据。

  (3)下面代码说明多重继承也有其合理用途,塑模“人”的C++ Interface class(条款31):

// 多重继承合理用途例子:
// 这个类指出需要实现的接口
class IPerson
{
public:
virtual ~IPerson();
virtual std::string name() const = ;
virtual std::string birthDate() const = ;
}; class DataBaseID { ... }; // 用以实现IPerson接口的类,将被private继承
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 必须实现IPerson接口,需得以public 继承才能完成
// CPerson 需要重新定义valueDelimOpen 和valueDelimClose,
// 单纯复合无法应付,这里我们采用private 继承。
// private 继承自 PersonInfo,导致PersonInfo中的virtual函数在CPerson成为Private成员,和在CPerson中定义Private实现细目类似
class CPerson : public IPerson, private PersonInfo // 多重继承
{
public:
explicit CPerson(DatabaseID pid) : PersonInfo(pid) { }
virtual std::string name() const // 实现必要的IPerson成员函数
{ return PersonInfo::theName(); } // theName 内调用valueDelimOpen(Close)函数
virtual std::string birthDate() const
{ return PersonInfo::theBirthDate(); }
private:
const char* valueDelimOpen() const { return ""; } // 重新定义继承而来的virtual函数
const char* valueDelimClose() const { return ""; }
  private:
     // std::string nameStr; // 若有实现细目,置于具象类中并为private
};

  有了上面的interface class ,客户便可以使用factory function(工厂函数,条款31)将“派生自IPerson 的具象classes”实体化。

// factory function,根据一个独一无二的数据库ID 创建一个Person对象,条款18 告诉你为什么返回智能指针
// 也可以将createPerson声明在接口类IPerson中并使其成为static(接口类无法创建对象)。
std::tr1::shared_ptr<IPerson> createPerson(DatabaseID personIdentifier);
// 如下createPerson实现
std::tr1::shared_ptr<IPerson> createPerson(DatabaseID personIdentifier)
{
return
     // 由于CPerson public继承自 IPerson(为is-a关系)
std::tr1::shared_ptr<IPerson> (new CPerson(personIdentifier));
}
// 如下调用createPerson
DatabaseID askUserForDatabaseID();
DatabaseID id(askUserForDatabaseID());
...
std::tr1::shared_ptr<IPerson> pp(createPerson(id));

  注意:条款31中所描述的情况是,为了把class中的实现细目抽离出来,我们可以实现出handle class 或 interface class,以达到降低编译相依度的目的。本条款(3)中在此基础上还增加了对virtual函数的处理,为了同时将virtual函数抽离,我们private继承 PersonInfo。

故而:
  1. 多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承(虚基类)的需要。

  2. virtual继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果virtual base class 不带任何数据,将是最具实用价值的情况。

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

Effective C++ —— 继承与面向对象设计(六)的更多相关文章

  1. Effective C++ -- 继承和面向对象设计

    32.确保你的public继承了模is-a关系 public继承意味着is-a关系(里氏替换原则),一切适用于基类也适用于派生类. 矩形继承正方形问题: 可实施与矩形的操作无法实施与正方形 在编程领域 ...

  2. Effective C++ ——继承与面向对象设计

    条款32:确定你的public继承塑模出is-a关系 以public继承的类,其父类的所有的性质都应该使用与子类,任何需要父类的地方都应该能用子类来代替,任何子类类型的对象也同时是父类的: class ...

  3. EffectiveC++ 第6章 继承与面向对象设计

    我根据自己的理解,对原文的精华部分进行了提炼,并在一些难以理解的地方加上了自己的"可能比较准确"的「翻译」. Chapter 6 继承与面向对象设计 Inheritance and ...

  4. Effective C++(20) 继承与面向对象设计

    本文主要参考<Effective C++ 3rd>中的第六章部分章节的内容. 关注的问题集中在继承.派生.virtual函数等.如: virtual? non-virtual? pure ...

  5. Effective C++笔记(六):继承与面向对象设计

    参考:http://www.cnblogs.com/ronny/p/3756494.html 条款32:确定你的public继承塑模出is-a关系 “public继承”意味着is-a.适用于base ...

  6. Effective C++: 06继承与面向对象设计

    32:确定你的public继承塑模出is-a关系 以C++进行面向对象编程,最重要的一个规则是:public继承表示的是"is-a"(是一种)的关系. 如果令class D以pub ...

  7. Effective C++ 6.继承与面向对象设计

    //条款32:确定你的public继承塑模出is-a关系 // 1.public继承意味着is-a的关系,适用在基类上的方法都能用于派生类上. //条款33:避免遮掩继承而来的名称 // 1.在pub ...

  8. Effective C++笔记:继承与面向对象设计

    关于OOP 博客地址:http://www.cnblogs.com/ronny 转载请注明出处! 1,继承可以是单一继承或多重继承,每一个继承连接可以是public.protected或private ...

  9. 【Effective C++】继承与面向对象设计

    关于OOP 1,继承可以是单一继承或多重继承,每一个继承连接可以是public.protected或private,也可以是virtual或non-virtual. 2,成员函数的各个选项:virtu ...

随机推荐

  1. 基于jQuery图像碎片切换效果插件FragmentFly

    基于jQuery图像碎片切换效果插件FragmentFly.这是一款只需三步轻松完成碎片动画,参数可调,使用方便. 在线预览   源码下载 部分代码: <div class="all_ ...

  2. python 调用函数 / 类型转换 / 切片/ 迭代

    调用函数 / 类型转换 /  切片/ 迭代 1. 调用函数:abs(),max(),min() 2. 数据类型转换:int(),float(),str(),tool(),a=abs, 3. 定义函数, ...

  3. linux 中的进程wait()和waitpid函数,僵尸进程详解,以及利用这两个函数解决进程同步问题

    转载自:http://blog.sina.com.cn/s/blog_7776b9d3010144f9.html 在UNIX 系统中,一个进程结束了,但是他的父进程没有等待(调用wait / wait ...

  4. FusionCharts JavaScript API - Functions 常用方法整理笔记

    FusionCharts JavaScript API - Functions Home > FusionCharts XT and JavaScript > API Reference  ...

  5. wireshark error: There are no interfaces on which a capture can be done.

    一.Linux环境:1.root用户启动 01.启动一个shell 02.sudo wireshark (需要root权限) 2.普通用户启动 从Linux中第一次启动Wireshark的时候,可能会 ...

  6. mybatis 一对多关系

    <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "- ...

  7. Spring Boot 日志记录 SLF4J

    Spring Boot 日志记录 SLF4J 2016年01月12日 09:25:28 阅读数:54086 在开发中打印内容,使用 System.out.println() 和 Log4j 应当是人人 ...

  8. 关闭 禁用 Redis危险命令

    Redis的危险命令主要有: flushdb,清空数据库 flushall,清空所有记录,数据库 config,客户端连接后可配置服务器 keys,客户端连接后可查看所有存在的键 我们常常需要禁用以上 ...

  9. 关于Cocos2d-x中自己定义的类的名字和Cocos2d-x引擎库中的类的名字重复的解决方法

    方法一: 修改自己定义的类的名字,VS2013中可以用Ctrl+H来替换某个特定的单词,Ctrl+F是用来查询某个单词所在的位置或者有没有存在. 方法二: 1.给自己定义的类的.h和.cpp文件的整体 ...

  10. android 编译 app

    有些编写的app需要放到android的源码中进行编译.放置的路径packages/apps/ 编译方法,参考 http://blog.csdn.net/luoshengyang/article/de ...