继承

这一节的有些内容可能需要读者预先阅读多态那一章节的基础知识才能更好地理解。

看过我总结的其他《C++编程思想》章节的读者可能会知道大部分的东西仍然是这本书上的。但是这一节可能会有不同,因为那本书上的内容有些粗略,因此我参考了其他的资料,例如《高质量C++编程指南Effective C++第二版和在网上下载的一些课件,总之,会让你继承方面的知识提高几个层次。所以内容会有点多,本来还打算将多态和继承放在一个章节,那样估计就有读者吓跑了。但是还是让我很头疼,要把这些东西总结到一块可不是一件容易的事,好吧,开始主题吧!

在此我不在叙述继承的语法规则,请看那本书吧!还有更多的内容等着读者去阅读呢!

首先需要明白继承的重要性:继承是面向对象程序设计的基石。可能知道或者不知道这一点对一个人C++学的好不好并没有直接的关系。但是我觉得对你是否知道和理解面向对象有很大关系。因为我曾经就以为面向对象就是类,其实也对也不对,这里我就不做过多解释了。

另外继承很重要的一点是:继承的最终目的是为了实现多态。这一点读者将在多态那一节看到,这一节或多或少也会涉及多态,不理解没关系,但必须以后要理解。

1、继承的概念

简单叙述一下继承来做个铺垫吧。如果A是基类,B是A的派生类,那么B将继承A的数据和函数。例如:

class A

{

public:

    void Func1(void);

    void Func2(void);

};

 

class B : public A

{

public:

    void Func3(void);

    void Func4(void);

};

void main()

{

    B b;

    b.Func1(); // B 从A 继承了函数Func1

    b.Func2(); // B 从A 继承了函数Func2

    b.Func3();

    b.Func4();

}

这个简单的示例程序说明了一个事实:C++的“继承”特性可以提高程序的可复用性。正因为“继承”太有用、太容易用,才要防止乱用“继承”。

2、区分继承和模板

或许读者会问这有什么难的?确实不难,但是有时候他们真的很像:都是为了使代码重用。

考虑下面两个例子:

 

1)你想设计一个类来表示对象的堆栈。这将需要多个不同的类。例如,会有一个类来表示int型的堆栈,第二个类来表示string的堆栈,等等。你也许对设计一个最小的类接口很感兴趣,所以会将对堆栈的操作限制在:创建堆栈,销毁堆栈,将对象压入堆栈,将对象弹出堆栈,以及检查堆栈是否为空。设计中,你不会借助标准库中的类。

2)你想设计一个类来表示猫。这也将需要多个不同的类,因为每个品种的猫都会有点不同。和所有对象一样,猫可以被创建和销毁,但,正如所有猫迷所知道的,猫所做的其它事不外乎吃和睡。然而,每一种猫吃和睡都有各自惹人喜爱的方式。

 

这两个问题的说明听起来很相似,但却导致完全不同的两种设计。为什么?

答案涉及到"类的行为" 和 "类所操作的对象的类型"之间的关系。对于堆栈和猫来说,要处理的都是各种不同的类型(堆栈包含类型为T 的对象,猫则为品种T),但你必须问自己这样一个问题:类型T 影响类的行为吗?如果T不影响行为,你可以使用模板。如果T影响行为,你就需要虚函数,从而要使用继承。

对于Stack类来说,即使对T一无所知,你还是能够写出每个成员函数。Stack 的行为在任何地方都不依赖于T。这就是模板类的特点:行为不依赖于类型。

但是,猫呢?为什么猫不适合模板?

重读上面的说明,注意这一条:"每一种猫吃和睡都有各自惹人喜爱的方式"。这意味着必须为每种不同的猫实现不同的行为。不可能写一个函数来处理所有的猫。解决的方法就是制定一个抽象基类,然后其他种类的猫都必须继承自这个抽象基类。

剩下的问题是,为什么继承不适合Stack 类?想知道为什么,不妨试着去声明一个Stack层次结构的抽象基类——所有其它的堆栈类都从这个唯一的类继承:

class Stack 

{ // a stack of anything

public:

    virtual void push(const ??? object) = 0;

    virtual ??? pop() = 0;

...

};

现在问题很明显了。该为纯虚函数push和pop声明什么类型呢?冷酷而严峻的事实是,做不到。这就是为什么说继承不适合创建堆栈。

讨论完了堆栈和猫,下面将得到的结论总结一下:

1)当对象的类型不影响类中函数的行为时,就要使用模板来生成这样一组类。

2)当对象的类型影响类中函数的行为时,就要使用继承来得到这样一组类。

2、使用继承还是组合?

在某种意义上,我们常把基类和派生类之间的关系看做是一个“is-a()”关系(严格来说是把基类和共有继承自它的派生类之间的关系看做是一个“is-a”关系,原因以后再说),因为我们可以说“圆形是一个形体”。对继承的一种测试方法就是看我们是否可以说这些类有“is-a”关系,而且还有意义。有时需要想一个派生类型添加新的接口元素,这样就扩展了接口并创建了新类型。这个新类型仍然可以代替这个基类,但这个代替不是完美的,因为这些新函数不能从基类访问。这可以描述为“is-like-a()”关系;新类型有老类型的接口,但还包含其它函数,所以不能说他们完全相同。

而组合可以看做是一个“has-a()”关系,因为我们可以说“眼睛、鼻子、耳朵等是头的一部分”。所以类Head 应该由类Eye、Nose、Mouth、Ear 组合而成,不是派生而成。

了解继承的程序员,可能会觉得继承太好用了,区分这些做什么,都用继承吧!那么劝你还是不要再用C++了。因为我们会为此付出代价。为避免乱用继承,我们应该给继承立一些规则:

1)如果类和类毫不相关,不可以为了使B的功能更多些而让B

继承的功能和属性。

2)若在逻辑上的“一种”(is-a),则允许继承的功能和属性。例如男人(Man)是人(Human)的一种,男孩(Boy)是男人的一种。那么类Man可以从类Human派生,类Boy可以从类Man派生。

3)若在逻辑上AB的“一部分”(has-a),则不允许BA派生,而是要用A和其它东西组合出B

注意事项:

看起来很简单,但是实际应用时可能会有意外,继承的概念在程序世界与现实世界并不完全相同。

例如从生物学角度讲,鸵鸟(Ostrich)是鸟(Bird)的一种,按理说类Ostrich 应该可以从类Bird 派生。但是鸵鸟不能飞,那么Ostrich::Fly 是什么东西?

所以更加严格的继承规则应当是:若在逻辑上BA的“一种”,并且的所有功能和属性对而言都有意义,则允许继承的功能和属性。

3、构造函数和析构函数调用的次序

在说明调用次序之前,先来看一下构造函数的初始化表

构造函数有个特殊的初始化方式叫“初始化表达式表”(简称初始化表)。初始化表位于函数参数表之后,却在函数体 {} 之前。这说明该表里的初始化工作发生在函数体内的任何代码被执行之前

构造函数初始化表的使用规则:

1)如果类存在继承关系,派生类必须在其初始化表里调用基类的构造函数,例如:

class A

{…

    A(int x); // A 的构造函数

};

class B : public A

{…

    B(int x, int y);// B 的构造函数

};

B::B(int x, int y)

: A(x) // 在初始化表里调用A 的构造函数

{

    …

}

2)类的 const 常量只能在初始化表里被初始化,因为它不能在函数体内用赋值的方式来初始化。

3)类的数据成员的初始化可以采用初始化表或函数体内赋值两种方式,这两种方式的效率不完全相同。

非内部数据类型的成员对象应当采用第一种方式初始化,以获取更高的效率。

3.1 构造和析构的次序

构造从类层次的最根处开始,在每一层中,首先调用基类的构造函数,然后调用成员对象的构造函数。析构则严格按照与构造相反的次序执行,该次序是唯一的,否则编译器将无法自动执行析构过程。

一个有趣的现象是,成员对象初始化的次序完全不受它们在初始化表中次序的影响,只由成员对象在类中声明的次序决定。这是因为类的声明是唯一的,而类的构造函数可以有多个,因此会有多个不同次序的初始化表。如果成员对象按照初始化表的次序进行构造,这将导致析构函数无法得到唯一的逆序。

4、名字隐藏

派生一个类时可以分四个步骤:

1)吸收基类成员

不论是数据成员还是函数成员(除了构造函数、析构函数、赋值函数)全盘接收。

2)改造基类成员也叫重定义(redefining)

声明一个和某基类成员同名的新成员,派生类中的新成员就屏蔽了基类同名成员称为重写(overriding)

3)发展新成员

派生类新成员必须与基类成员不同名,它的加入保证派生类在功能上有所发展。 

4)重写构造和析构函数

上面的步骤就是继承与派生编程的规范化步骤。

2步中,新成员如是成员函数,参数表也必须一样,否则是重载,任何时候重新定义了基类中的一个重载函数,在新类之中所有其他的版本则被自动隐藏了。不要和覆盖混淆了,将会在多态时讲解。

3步中,独有的新成员才是继承与派生的核心特征。

4步是重写构造函数与析构函数,派生类不继承这两种函数。

关于隐藏的详细内容读者需自行阅读《C++编程思想 第一卷》中“继承与组合”部分。

4.1 决不要重新定义继承而来的非虚函数

在《Effective C++第二版》中有这么一个条款:

决不要重新定义继承而来的非虚函数(关于虚函数多态那一节会介绍)

我想虽然不至于“决不”,但是“最好不要重新定义”是很有必要的,而且绝大多数情况下没有必要去重新定义。下面我们就来分析原因。

假设类 公有继承于类B,并且类中定义了一个公有成员函数mfmf的参数和返回类型不重要,所以假设都为void。换句话说,我这么写:

class B {

public:

    void mf();

    ...

};

class D: public B { ... };

 

甚至对Bmf 一无所知,也可以定义一个类型的对象x:

D x;

那么,如果发现这么做:

 

B *pB = &x;  // 得到的指针

pB->mf();  // 通过指针调用mf

 

和下面这么做的执行行为不一样:

D *pD = &x;  // 得到的指针

pD->mf();  // 通过指针调用 mf

 

你一定就会感到很惊奇。

因为两种情况下调用的都是对象 的成员函数mf,因为两种情况下都是相同的函数和相同的对象,所以行为会相同,对吗?对,会相同。但,也许不会相同。特别是,如果mf 是非虚函数而又定义了自己的mf 版本,行为就不会相同:

class D: public B 

{

public:

    void mf(); // 隐藏了B::mf

    ...

};

pB->mf(); // 调用B::mf

pD->mf(); // 调用D::mf

 

下面两段话难以理解,我也是好长时间后才理解的,所以不要灰心。

 

行为的两面性产生的原因在于,象 B::mf D::mf 这样的非虚函数是静态绑定的。这意味着,因为pB 被声明为指向的指针类型,通过pB 调用非虚函数时将总是调用那些定义在类中的函数——即使pB 指向的是从派生的类的对象,如上例所示。

相反,虚函数是动态绑定的,因而不会产生这类问题。如果mf 是虚函数,通过pB pD 调用mf 时都将导致调用D::mf,因为pB pD 实际上指向的都是类型的对象。

 

所以,结论是,如果写类 时重新定义了从类继承而来的非虚函数mf的对象就可能表现出精神分裂症般的异常行为。也就是说,的对象在mf 被调用时,行为有可能象B,也有可能象D,决定因素和对象本身没有一点关系,而是取决于指向它的指针所声明的类型。引用也会和指针一样表现出这样的异常行为。

这一小节的详细内容读者可以自行阅读《Effective C++2版》中第37个条款。

4.2 决不要重新定义继承而来的缺省参数值

这一小节的内容和上一小节的内容比较相像,读者同样可以查看那本书,本节就不在详细叙述了。

5、继承方式(访问控制)

访问控制也是三种:

公有(public)方式,亦称公有继承

保护(protected)方式,亦称保护继承

私有(private)方式, 亦称私有继承。 

其中共有继承是绝对主流。但有时我们的确有必要私有的或者保护的继承自基类,需要读者仔细去体会,我在这里简单地列举一些例子。

首先我们可能奇怪,private继承的目的是什么,因为在一个新类中使用组合创建一个private对象的选择似乎更加合适。为了完整性,private继承被包含在该语言中。但是,如果不是为了其他理由,则应当减少混淆。所以通常使用组合而不是private继承。然而,这里偶然有这种情况,即可能产生像基类接口一样的接口部分,而不允许该对象的处理像一个基类对象。Private继承提供了这个功能。

class Pet
{
public:
	char eat()const
	{
		return 'a';
	}
	int speak()const
	{
		return 2;
	}
	float sleep()const
	{
		return 3.0;
	}
	float sleep(int)const
	{
		return 4.0;
	}
};
class Goldfish : Pet	//注意默认为私有继承
{
public:
	using Pet::eat;
	using Pet::sleep;
};
int main()
{
	Goldfish bob;
	bob.eat();
	bob.sleep();
	bob.sleep(1);
	//! bob.speak();//error: private member function
}

上面的例子实现了对私有继承的成员的公有化,利用私有继承隐藏了基类的部分功能。

注意给出一个重载函数的名字将使基类中所有它的重载版本公有化。

很显然,私有继承的含义不是“is-a,私有继承意味着“用...来实现”。也就是,如果使类D私有继承自类B,这样做是因为你想利用类B中已经存在的某些代码,而又想隐藏这些代码。

在使用private继承取代组合之前,应当仔细考虑,当与运行时类型标识相连时,私有继承特别复杂。

5.1 protected

理解了private继承,则protected继承就简单了,它的意思就是“就这个类的用户而言,它是private的,但是它可以被从这个类继承来的任何类使用”。就这么简单,它是不常用的,它的存在只是为了语言的完备性。

6、多重继承

仅仅多重继承似乎并不是多难,但是难就难在把virtual、多态、虚表和RTTI加进来,个人认为这块知识似乎是C++中最难的部分。掌握了这块知识,那么你的C++水平已经很不错了,就差阅读一些库代码了,比如STLboost等。在C++中多重继承是否有必要这一问题现在仍然存在着大量的争议。

在我的博客中,曾经转载过一篇“输入输出流类库”文章,这篇文章中介绍了输入输出流类继承体系。或许我会在总结《C++编程思想 第二卷》中的“输入输出流”中再次介绍,不过我先把其中的继承体系给大家展示一下吧:


对,是不是很复杂,个人直到现在都没有弄清楚这么一个庞大的继承体系的所有细枝末节,因为它太复杂了,几乎容纳了C++所有的知识。好吧,了解一下也是好的,现在就先来介绍一些基础内容吧!

6.1 接口继承

多重继承中毫无争议的一种运用属于接口继承(interface inheritance)。虽然说在C++中,所有的继承都是实现继承(implementation inheritance),这里所说的接口继承仅仅是模拟,但是我还是这么叫吧,因为它和一般的继承确实区别很大。可能看完《C++编程思想》这本书都没有弄明白为什么叫做接口继承和接口类(interface class)

其实从名字就可以看出来,接口类只提供一种接口,你不能实现它,前面讲到的抽象基类就是非常标准的接口类,它没有数据成员,仅仅包含了一些函数的声明。目的就是提供统一的接口,它的作用直到讲多态时才会体现出来(因为在这里你完全可以不需要它),这里就不说明了。在派生类中必须实现所有的接口,下面是一个例子:

#include <string>
#include <iostream>
#include <sstream>
using namespace std;
class Printable
{
public:
	virtual ~Printable()
	{}
	virtual void print(ostream&)const = 0;
};
class Intable
{
public:
	virtual ~Intable()
	{}
	virtual int toInt()const = 0;
};
class Stringable
{
public:
	virtual ~Stringable()
	{}
	virtual string toString()const = 0;
};
class Able :public Printable, public Intable, public Stringable
{
private:
	int mydata;
public:
	Able(int x) :mydata(x)
	{}
	void print(ostream& os)const
	{
		os << mydata;
	}
	int toInt()const
	{
		return mydata;
	}
	string toString()const
	{
		ostringstream os;
		os << mydata;
		return os.str();
	}
};
void testPrintable(const Printable& p)
{
	p.print(cout);
	cout << endl;
}
void testIntable(const Intable& p)
{
	cout << p.toInt() + 1 << endl;
}
void testStringable(const Stringable& s)
{
	cout << s.toString() + "th" << endl;
}

void main()
{
	Able a(7);
	testStringable(a);
	testIntable(a);
	testPrintable(a);
}

Able“实现”了接口PrintableIntableStringable,因为它提供了那些对他们进行声明的函数的实现。

6.2 实现继承

关于实现继承我就不多讲了,它的例子随处可见,如果体会不到那就到库中看看吧,希望你不会因为那些复杂的继承关系而搞的头昏眼花吧!

重要的一点,基类的析构函数一定要是虚函数,这一点非常重要。这个将在多态一节中讲到。再一次提到多态,再次重复:继承的最终目的就是为了实现多态,不如此的话,继承真的就成了多余的了(你可以通过其他方式来实现它)

6.3 重复子对象

这里会讲到一个有趣的继承体系——“钻石继承”,所以多点耐心,你就会得到它。

当从某个基类继承时,可以在其派生类中得到那个基类的所有数据成员的副本。下面的程序说明了多个基类子对象在内存中的可能布局:

#include <iostream>
using namespace std;
class A
{
	int x;
};
class B
{
	int y;
};
class C :public A, public B
{
	int z;
};

void main()
{
	cout << "sizeof(A) == " << sizeof A << endl;
	cout << "sizeof(B) == " << sizeof B << endl;
	cout << "sizeof(C) == " << sizeof C << endl;
	C c;
	cout << "&c == " << &c << endl;
	A* ap = &c;
	B* bp = &c;
	cout << "ap == " << static_cast<void *>(ap) << endl;
	cout << "bp == " << static_cast<void *>(bp) << endl;
	C* cp = static_cast<C*>(bp);
	cout << "cp == " << static_cast<void *>(cp) << endl;
	cout << "bp == cp?" << boolalpha << (bp == cp) << endl;
	cp = 0;
	bp = cp;
	cout << bp << endl;
}
/*output
sizeof(A) == 4
sizeof(B) == 4
sizeof(C) == 12
&c == 002ffe98
ap == 002ffe98
bp == 002ffe9c
cp == 002ffe98
bp == cp?true
0
*/

正如读者所见,对象cB子对象部分从整个对象开始位置偏移了4个字节。其布局如下所示:

A’s data

B’s data

C’s data

要深究为什么这样输出,读者请阅读《C++编程思想 第二卷》。

下面这个例子比较重要,而且容易混淆:

#include <iostream>
using namespace std;

class Top
{
	int x;
public:
	Top(int n) :x(n)
	{}
};
class Left :public Top	//注意这里是共有继承
{
	int y;
public:
	Left(int m, int n) :Top(m), y(n)
	{}
};
class Right :public Top	//注意这里是共有继承
{
	int z;
public:
	Right(int m, int n) :Top(m), z(n)
	{}
};
class Bottom :public Left, public Right
{
	int w;
public:
	Bottom(int i, int j, int k, int m) :Left(i, k), Right(j, k), w(m)
	{}
};

void main()
{
	Bottom b(1, 2, 3, 4);
	cout << sizeof b << endl;	//20
}

关于《C++编程思想 第二卷》关于这个例子的类图可能容易混淆,还是按照下面这样理解吧:


而如果将上面的代码改成下面这样,使Top成为LeftRight的虚基类,那么情况就不同了:

#include <iostream>
using namespace std;

class Top
{
	int x;
public:
	Top(int n) :x(n)
	{}
	virtual ~Top()
	{}
};
class Left :virtual public Top	//注意这里是虚共有继承
{
	int y;
public:
	Left(int m, int n) :Top(m), y(n)
	{}
};
class Right :virtual public Top	//注意这里是虚共有继承
{
	int z;
public:
	Right(int m, int n) :Top(m), z(n)
	{}
};
class Bottom :public Left, public Right
{
	int w;
public:
	Bottom(int i, int j, int k, int m) :Top(i), Left(0, j), Right(0, k), w(m)
	{}
};

void main()
{
	Bottom b(1, 2, 3, 4);
	cout << sizeof b << endl;	//28
	cout << static_cast<void *>(&b) << endl;	//0046fae0
	Top *p = static_cast<Top*>(&b);
	cout << static_cast<void *>(p) << endl;	//0046faf4
	cout << dynamic_cast<void *>(p) << endl;	//0046fae0
}

为什么是28字节呢?原因有两个:

1)Top对象只有一个。

这是由于LeftRight虚继承了Top类,LeftRight子对象看起来各有一个指向共享的Top子对象的指针(或某种概念上等价的对象),并且对LeftRight成员函数中那个子对象的所有引用都要通过这些指针来完成。

2)虚函数机制

当加入虚函数时,编译器会为每个含有虚函数的类增加一个指针类型大小的空间用来存放虚表指针,详细内容会在多态一节讲解。

所以对象b的内存映像可能是下面这个样子:


这里的类图如下:


哈,看起来是不是像个菱形,所以这种继承关系就叫做“菱形继承”(或钻石继承)。 

在前面的代码中,最奇怪的事情是Bottom构造函数中对Top的初始化程序。正常的情况下,不必担心直接基类以外的子对象的初始化,因为所有的类只照料他们的直接基类的初始化。然而,由于从BottomTop有多个继承路径,因此依赖于中间类LeftRight将必须的初始化数据传递给基类导致了二义性——谁负责基类的初始化呢?基于这个原因,最高层次派生类必须初始化一个虚基类

因为虚基类引起共享子对象,共享发生之前他们就应该存在才有意义。所以子对象的初始化顺序遵循如下的规则递归的进行:

1)所有虚基类子对象,按照它们在类定义中出现的位置,从上到下、从左到右初始化。

2)然后非虚基类按照通常顺序初始化。

3)所有的成员对象按声明的顺序初始化。

4)完整的对象的构造函数执行。

下面的程序举例说明了这个过程:

#include <string>
#include <iostream>
using namespace std;

class M
{
public:
	M(const string& s)
	{
		cout << "M " << s << endl;
	}
};
class A
{
	M m;
public:
	A(const string& s) :m("in A")
	{
		cout << "A " << s << endl;
	}
	//virtual ~A();
};
class B
{
	M m;
public:
	B(const string& s) :m("in B")
	{
		cout << "B " << s << endl;
	}
	//virtual ~B();
};
class C
{
	M m;
public:
	C(const string& s) :m("in C")
	{
		cout << "C " << s << endl;
	}
};
class D
{
	M m;
public:
	D(const string& s) :m("in D")
	{
		cout << "D " << s << endl;
	}
};
class E :public A, virtual public B, virtual public C
{
	M m;
public:
	E(const string&s) :A("From E"), B("From E"), C("Form E"), m("in E")
	{
		cout << "E " << s << endl;
	}
};
class F :virtual public B, virtual public C, public D
{
	M m;
public:
	F(const string& s) :B("From F"), C("From F"), D("From F"), m("in F")
	{
		cout << "F " << s << endl;
	}
};
class G :public E, public F
{
	M m;
public:
	G(const string& s) : B("From G"), C("From G"), E("From G"), F("From G"), m("in G")
	{
		cout << "G " << s << endl;
	}
};

void main()
{
	G g("form main");
}

输出结果如下:

M in B

B from G

M in C

C from G

M in A

A from E

M in E

E from G

M in D

D from F

M in F

F from G

M in G

G from main

有没有眼花呢?研究研究吧。

6.4 名字查找问题

如果一个类有多个直接基类,既可以共享这些基类中那些同名的成员函数,如果要调用这些成员函数中的一个,那么编译器将不知道调用他们之中的哪一个。下面的程序举例将会报告这样一个错误:

class Top
{
public:
	virtual ~Top()
	{}
};
class Left :virtual public Top
{
public:
	void f()
	{}
};
class Right :virtual public Top
{
public:
	void f()
	{}
};
class Bottom :public Left, public Right
{};

void main()
{
	Bottom b;
	b.f();	//Error here
}

Bottom已经继承了两个同名的函数,并且没有办法在他们之间进行选择,通常消除二义性调用的方法,是以基类名来限定函数的调用。

在一个层次结构中的不同分支上存在的同名函数常常发生冲突,下面的继承层次结构不存在这样的问题:

class Top
{
public:
	virtual ~Top()
	{}
	virtual void f()
	{}
};
class Left :virtual public Top
{
public:
	void f()
	{}
};
class Right :virtual public Top
{};
class Bottom :public Left, public Right
{};

void main()
{
	Bottom b;
	b.f();	//Calls Left::f()
}

程序在这里没有显式调用Right::f()。因为Left::f()是位于层次结构的最高派生类。一般情况下,如果类A直接或间接派生自类B,换句话说,在继承层次结构中类A比类B处于“更高的派生层次”,那么名字A::f()就比B::f()占优势,因此,在同名的两个函数之间进行选择时,编译器将选择占优势的那个函数。如果没有占优势的名字,就会产生二义性。

6.5 避免使用多重继承

当提到关于是否使用多重继承的问题时,至少要回答如下两个问题:

1)是否需要通过新类来显示两个类的公共接口?(换句话说,如果一个类能够包含在另一个类中,那么仅有它的某些接口暴露在一个新类中。)

2)需要向上类型转换为两个基类类型吗?

如果可以对上面任何一个问题回答“不是”,那么就可以避免使用MI,并且应该这样做。

请看这样的情况,一个类只是作为一个函数参数需要向上进行类型转换。在这种情况下,这个类就可以被嵌入,并且在新类中有一个自动类型转换函数提供产生一个指向嵌入的对象的引用。任何时候,如果要将新类的一个对象作为参数传递给某个期盼嵌入对象的函数,这就需要使用类型转换函数。然而,类型转换不能用于普通的多态成员函数的选择;那需要使用继承机制来完成。推荐使用组合而不使用继承。

7、向上类型转换

前面已经有几个例子用到了向上类型转换,在这里做一下简单的介绍,读者应该详细的看一下《C++编程思想 第一卷》中有关向上类型转换的内容。

将派生类对象的引用或指针(没有其他)转变成基类对象引用或指针的活动被称为向上类型转换(upcasting)。向上类型转换总是安全的,因为是从更专门的类型到更一般的类型。

确定应当用组合还是用继承,最清楚的方法之一是询问需要从新类向上类型转换。向上类型转换发生在函数调用期间。向上类型转换还能出现在对指针或引用简单赋值期间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值