Chpater 6
作者:互联网
6 继承与面向对象设计
条款 32 确定你的 public 继承塑模出 is-a 关系
“Derived is a Base!”
当一个类可以描述成 is-a 这样的概念的时候,就应该用 public 继承。
例. 每个学生都是人,但人不一定是学生。因此学生类应该 public 继承自 “人” 类。
请记住 :
1. “public” 继承意味 is-a。适用于 base classes 身上的每一件事情一定也适用于 derived classes 身上,因为每一个 derived class 对象也都是一个 base 对象。
条款 33 避免遮掩继承而来的名称
“Derived class 作用域被嵌套在 base class 作用域内。”
例
class Base {
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf2();
void mf3();
...
}
class Derived : public Base {
public:
virtual void mf1();
void mf4();
...
}
void Derived::mf4() {
...
mf2();
...
}
当程序调用 mf4()
并且遇到其内部的mf2()
的时候,这是程序内部发生的事情 :
-
编译器首先查找
local
作用域,也就是mf4()
所覆盖的的作用域,很遗憾,并未找到任何名为mf2()
的东西。 -
因此转到
mf4()
外部也就是derived class
所覆盖的作用域去查找,但还是没有找到一个名为mf2()
的东西。 -
于是便继续移到外围去查找,这次来到了
base class
,并且在此找到了virtual void mf2();
。 -
至此,查找完毕。若未找到,便会继续向外查找。
“继承类内的重载函数会遮掩基类中的同名函数”
注意哦,是同名函数,不是同签名的函数哦,也就是说就算其参数不同,也是会被遮盖的。
例
class Base {
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
...
};
class Dericed : public Base {
public:
virtual void mf1();
void mf3();
void mf4();
...
};
调用
Derived d;
int x;
...
d.mf1(); //调用Derived::mf1()
d.mf1(); //错误,Derived::mf1()遮掩了Base::mf1()和Base::mf1(int),但它自己没有参数所以会错误
d.mf2(); //调用缺省版本的Base::mf2()
d.mf3(); //调用Derived::mf3()
d.mf3(x); //错误,Derived::mf3()遮掩Base::mf3(double)和Base::mf3()
解决方法 :
实际上,如果你在使用 public 继承而又不继承那些重载函数,就是违反了 is-a 关系,所以一下解决方法还是少用。
- 使用
using
声明
例
...
class Derived : public Base {
public:
using Base::mf1;
using Base::mf3;
...
};
调用
Derived d;
int x;
...
d.mf1(); //匹配派生类的Derived::mf1()
d.mf1(x); //匹配Base::mf1(int)
d.mf2(); //匹配缺省版本的Base::mf2()
d.mf3(); //匹配Derived::mf3()
d.mf3(x); //匹配Base::mf3(x)
- 使用 inlining 转交函数(forwarding function)
例
...
class Derived : private Base {
public:
virtual void mf1()
{ Base::mf1(); }
...
};
Derived d;
int x;
d.mf1(); //调用Derived:mf1(),进而调用Base::mf1()
d.mf1(x); //错误,被遮掩
注: 因为这种做法是违反 public 继承原则的,所以我们选择使用 private 继承。
请记住 :
1. derived classes 内的名称会遮掩 base classes 内的名称。在 public 继承下从来没有人希望如此。
*2. 为了让遮掩的名称再见天日。可以使用 using 声明式或转交函数 (forwarding function)。*
34 区分接口继承和实现继承
public 继承的概念由两部分组成:
1. 函数接口( function interfaces )继承
2. 函数实现( function implementations )继承
先看书上的例子
class Shape {
public:
virtual void draw() const = 0;
virtual void error(const std::string &msg);
int objectID() const;
...
};
class Rectangle : public Shape{...};
class Ellipse : public Shape{...};
显而易见,Shape
是一个抽象基类,因此它不能过创建实体。但是按照条款 32 的规则,其成员函数接口总是会被继承的。
该类共声明三个函数。
-
virtual void draw() = 0
pure virtual 函数 : 必须被 “继承了它们” 的具象类重新声明,而且通常在抽象基类中是没有定义的。
声明一个 pure virtual 函数的目的是为了让 derived classes 只继承函数接口。
另:可以为 pure virtual函数提供的定义,调用它的唯一途径就是明确指出类的名称。但尽量不去用它。提供缺省版本有更好的做法。
-
virtual void error()
impure virtual 函数 : derived class 继承其函数接口,但同时 impure virtual 函数会提供一份实现代码。
声明一个 impure virtual 函数的目的,是让 derived classes 继承该函数的接口和缺省实现。
-
int ObjectID() const
non-virtual 函数 : 不变性凌驾于特异性,意味着 derived class 不希望做出不同的行为,而是继承该函数的行为。
声明 non-virtual 函数的目的是为了令 derived classes 继承函数的接口及一份强制的实现。
请记住 :
1. 接口继承和实现继承不同。在 public 继承之下,derived classes 总是继承 base class 的接口。
2. pure virtual 函数只具体指定接口继承。
3. impure virtual 函数具体指定接口继承及缺省实现继承。
4. non-virtual 函数具体指定接口继承以及强制性实现继承。
35 考虑 virtual 函数以外的其他选择
前提 :假设我们在写一个游戏人物类,其中有一个生命值降低的函数,它返回一个整数来表示不同的健康程度。不同的人物有不同的方式计算生命健康度
class GameCharacter {
public:
virtual int healthValue() const;
...
};
正如标题所言,我们一般的做法就是将其声明为 virtual 函数,再有不同的派生类去继承它。除此之外,还有其他的做法:
- 藉由 Non-Virtual Interface 手法实现 Template Method 模式
概括 : 保留 healthValue()
但让它变成 non-virtual 函数,并调用一个 private virtual 函数。
例
class GameCharacter {
public:
int healthValue() const {
... //事前工作
int retVal = deHealthValue(); //真正的工作
... //事后工作
return retVal;
}
private:
virtual int deHealthValue() const { //derived class 可以重新定义它
...
}
};
通常我们把这个 non-virtual 函数称为外覆器(wrapper)。
NVI 手法的优点就是可以保证在调用函数的前后做一些处理,不需要用户来自己处理,普遍适用于像锁定互斥器、制造运转日志记录项等等。
另外,没必要让 virtual 函数一定是 private。可以把它定义为 protected ,但如果函数必须定义为 public 则不能实施 NVI 手法了。
- 藉由 Function Pointers 实现的 Strategy 模式
概述 : 如果我们要求人物的健康指数的计算与人物无关,而令每个人物的构造函数接受一个指针,该指针指向一个健康计算函数,这样我们便可以给不同的人物赋予不同的健康计算函数指针,以此来计算人物的健康程度。
class GameCharacter; //前置声明
int defaultHealthCalc(const GameCharacter& gc);
class GamaCharacter {
public:
typedef int (*HealthCalcFunc)(const GameCharacter &);
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
: healthFunc(hef)
{}
int healthValue() const {
return healthFunc(*this);
}
...
private:
HealthCalcFunc healthFunc;
};
- 藉由 tr1::function 完成 Strategy 模式
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter {
public:
typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
:healthFunc(hcf)
{}
int healthValue() const
{ return healthFunc(*this); }
...
private:
HealthCalcFUnc healthFunc;
};
此时构造函数接受任何兼容的可调用物,包括但不限于函数对象、成员函数。
- 古典的 Strategy 模式
class GameCharacter;
class HealthCalcFunc {
public:
...
virtual int calc(const GameCharacter& gc) const
{...}
...
};
HealthCalcFunc defaultHealthCalc;
class GameCharacter {
public:
explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc)
: pHeathCalc(puf)
{}
int healthValue() const
{ return pHealthCalc->calc(*this)}
...
private:
HealthCalcFunc* pHealthCalc;
};
注意如果想添加一个健康计算方法,只需要为 HealthCalcFunc
类添加一个派生类即可。
请记住 :
1. virtual 函数的替代方案包括 NVI 手法及 Strategy 设计模式的多种形式。NVI 手法自身是一个特殊形式的 Template Method 设计模式。
2. 将机能从成员函数移到 class 外部函数,带来的一个缺点是,非成员函数无法访问 class 的 non-public 成员。
3. tr1::function 对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式兼容”的所有可调用物。
36 绝不重新定义继承而来的 non-virtual 函数
这个没什么好讲的,如果重新定义继承而来的 non-virtual 函数,就会遮掩基类的同名函数。那这不叫继承了,而且违背了两个原则 :
-
使用 B 对象的每一件事,也适用与 D 对象,因为每个 D 对象都是一个 B对象。
若此时 D 重新定义了该函数,则每次需要该函数时,由于每个 D 对象都是一个 B对象,所以该函数的行为与基类一致,以至于每次都得调用基类的函数,而不是自己的(函数实现继承)函数,那么“每个 D 对象都是一个 B对象”明显是错的。
-
B 的 derived classes 一定会继承 mf 的接口和实现,因为 mf 是 B 的一个 non-virtual 函数。
因为 non-virtual 函数的继承“不变性凌驾于特异性之上”,如果你重新定义继承而来的函数,那这个函数明显应该是 virtual 函数才对。
请记住 :
1. 绝不重新定义继承而来的 non-virtual 函数。
37 绝不重新定义继承而来的缺省参数值
“本条款讨论“继承一个带有缺省参数值的 virtual 函数” ”
我们先明确本条款的理由,这也是本条款的核心 :
virtual 函数是动态绑定,而缺省参数值却是静态绑定。
区别 :
- 对象的静态绑定,就是它在程序中被声明时所采用的类型。
- 对象的动态类型则指的是“目前所指对象的类型。”
由于缺省(默认)是静态绑定的,因此使用基类指针调用函数时,并不会根据多态性去选择缺省参数值,而是直接使用基类声明时指定的缺省参数值。因此重新定义是无效的,同时也会起到误导的后果。
请记住 :
1. 绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而 virtual 函数----你唯一应该覆写的东西才是动态绑定。
38 通过复合塑模出 has-a 或 “根据某物实现出”
什么是复合?
复合是类型之间的一种关系,当某种类型的对象内含其他的类型的对象,便叫复合。
之前介绍的 public 继承带有 is-a 的含义,而复合意味着 has-a 或者 is-implemented-in-terms-of(根据某物实现)。
区分 :
- has-a : 复合发生在应用域内的对象之间,应用域就是说你在程序中将现实生活中的事物抽象成一个个的类,例如一辆汽车,一个人。
- is-implemented-in-terms-of : 复合发生在实现域内的对象之间,实现域就是类似域缓冲区、互斥器等实现细节的人工制品,可以理解成以某种数据结构为基础而实现的一种新的数据结构。
请记住 :
1. 复合的意义和 public 继承完全不同。
2. 在应用域,复合意味着 has-a 。在实现域,复合意味着 is-implemented-in-terms-of。
39 明智而审慎地使用 private 继承
private 继承的特点 :
- 编译器不会将一个 derived class 对象转换成一个 base class 对象。(丧失了多态性)
- 由 private base class 继承而来的所有成员,在 derived class 中都会变成 private 属性。
因此如果我们让 class D 以 private 形式继承 class B,你的用意是为了采用 class B 内已经具备的某些特性,而不是因为 B 和 D 之间存在某种逻辑关系。
根据上述讨论,我们很容易看出 private 继承和之前提到的 is-implemented-in-terms-of 是一样的概念,而我们在前面是利用复合来实现的,那目前为止我们就有了了两种实现方式,一种是 private 继承, 一种是复合。
所以说我们该怎样选择?
- 使用 private 继承
例
假设我们现在有一个类 Widget,其中需要用到定时器,目前现有一个定时器类 :
class Timer {
public:
explicit TImer(int tickFrequency);
virtual void onTick() const;
...
};
由于 class Timer 中有 virtual 函数,所有 Widget 必须继承自 Timer。显然两者不符合 is-a 关系,因此我们不能使用 public 继承。
因此我们使用 private 继承
class Widget : private TImer {
private:
virtual void onTIck() const;
...
};
这样用户就不会解除到 Timer 相关的函数。
- 使用复合
例
class Widget {
private:
class WidgetTimer : public Timer {
public:
virtual void onTick() const;
...
};
WidgetTimer timer;
};
我们将内嵌类 WidgetTimer 放在 private 中,因为这样可以让类 Widget 的派生类无法访问。
有一种情况成为我们使用 private 继承的理由 :
private 继承主要用于 “当派生类想要访问基类的 protected 成分,或为了重新定义一个或多个 virtual 函数”。其实这个使用复合也可以做到,但如果我们所继承的基类不携带任何数据也就是没有 non-static 成员变量,没有 virtual 函数,也没有 virtual base classes。但其实在C++中也是会占有一定的开销,因为C++会默认地安插一个char到空对象中。真正的不占用任何开销的行为就是使用 private 继承也就是所谓的 EBO(empty base optimization)。
例
class Empty {};
//复合做法
class HoldAnInt {
private:
int x;
Empty e;
};//sizeof(HoldAnInt) > sizeof(int),空白类还是占用了开销
//private 继承
class HoldAnInt : private Empty {
private:
int x;
};//sizeof(HoldAnInt) == sizeof(int),不占用开销
但实际上,大多数类都不是空白类,所以无论什么时候,只要可以,你还是选择复合。
请记住 :
1. private 继承意味 is-implemented-in-terms-of 。通常比复合的级别低。但是当 derived class 需要访问 protected base class的成员,或需要重新定义继承而来的 virtual 函数时,这么设计是合理的。
2. 和复合不同,private 继承可渔鸥造成 empty base 最优化。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很需要。
40 明智而审慎地使用多重继承
使用多重继承时,有两种方案,如图,IOFile 和 File 之间有多条相通路径。两种方案分别为
- IOFile 从其中每一个直接基类中继承一份数据
- IOFile 只继承一份数据
一般我们使用第二种方案,需要用到 virtual 继承
class File {...};
class InputFile : virtual public File{...};
class OutputFIle : virtual public File{...};
class IOFile : public InputFile,
public OutputFIle
{...};
缺点 :
- 使用 virtual 继承的那些 classes 所产生的对象体积往往很大
- 访问 virtual base classes 的成员变量时速度也比较慢
什么时候使用:
- 非必要时不使用
- 如果要使用,尽可能避免在其中virtual base class 中放置数据
再看一个例
class IPerson {
public:
virtual ~IPerson();
virtual std::string name() const = 0;
virtual std::string birthDate() const = 0;
};
IPerson 的客户必须以 IPerson 的 pointers 和 references 来编写程序,因为这是抽象基类,所以我们使用一个工厂函数。
std::shared_ptr<IPerson> makePerson(DatabaseID personIdedntifier);
DatebaseID askUserForDatabaseID();
DatabaseID id(askUserForDatabaseID());
std::shared_ptr<IPerson> pp(makePerson(id));
我们假设该类的派生类叫 CPerson , 我们要做的是继承抽象基类的所有函数,再假设我们有一个既有的类 PersonInfo 可以帮助我们重写抽象积累的函数。
class PersonInfo {
public:
explicit PersonInfo(Database pid);
virtual ~PersonInfo();
virtual const char* theName() const;
virtual const char* theBirthDate() const;
...
private:
virtual const char* valueDelimOpen() const;
virtual const char* valueDelimClose() const;
...
};
其中 private 里的那两个方法是用来格式化输出相应的字符串的,但是不同的类当然有不同的格式化,所以我们可以去继承它,通过重写该方法从而达到目的。
缺省实现
virtual const char* valueDelimOpen() const;
{ return "["; }
virtual const char* valueDelimClose() const;
{ return "]"; }
const char* PersonInfo::theName() const {
static char value[Max_Length];
std::strcpy(value, valueDelimOpen());
//将value中的字符串附加到 name 成员中
std::strcat(value, valueDelimClose());
return value;
}
分析 :
显然,CPerson 的实现需要用到 PersonInfo 这个类,因此它们之间的关系应该是 is-implemented-in-terms-of , 一共有 private 继承和复合两种方法,因为我们要继承重写 PersonInfo 中的某些方法,所以应该使用 private 继承。而又因为 CPerson 和 IPerson 是一种 is-a 的关系,所以使用 public 继承。
class CPerson : public IPerson, private PersonInfo {
public:
explicit CPerson(DatabaseID pid) : PersonInfo(pid) {}
virtual std::string name() const
{ return PersonInfo::theName; }
virtual std::string theBirthDate()
{ return PersonInfo::theBirthDate; }
private:
virtual const char* valueDelimOpen() const {return "";}
virtual const char* valueDelimClose() const {return "";}
};
请记住 :
1. 多重继承比单一继承复杂。它可能导致新的歧义性,以及对 virtual 的维护。
2. virtual 继承会增加大小、速度、初始化复杂度等等成本。如果 virtual base classes 不带任何数据,将是最具实用价值的请款、
3. 多重继承的确有正当用途。其中一个情节涉及“public继承某个 Interface“ class 和 “private”和 “private 继承某个”协助实现的 class“的两相组合。
标签:函数,Chpater,继承,virtual,class,private,public 来源: https://www.cnblogs.com/Lingh/p/16618440.html