Conclusion for Inheritance and Object Oriented Design



条款32:

1.公有继承是一种is-a关系

2.任何函数如果期望获得一个类型为基类(pointer-to基类或reference-to基类)的实参,都也愿意接受一个继承类对象。这点只对public继承成立。

3.如果解决public继承中“不是所有的鸟都会飞”问题?

4.某些可施行于矩阵身上的事情却不可以施行于正方形身上(故不可以public继承)


条款33:

1.内层作用域的名称会掩盖外层作用域的名称(编译器先在local作用域查找,找不到再去其他作用域)

2.继承类的作用域被嵌套在基类作用域内,所以继承类内的名称会覆盖基类名称。

3.继承类内的函数名与基类内函数名相同,类型不同的重载函数,继承类内的函数会遮掩这个函数。

4.如果继承基类并且加上重载函数,又希望重新定义或覆写其中一部分,继承类遮掩了基类内的同名重载函数,可以使用using声明式达成目标。(using声明式放在public区域:bass class内的public名称在publicly dereved class内应该是public)

5.如果是私有继承,using声明式派不上用场,using声明式会令继承而来的某给定名称之所有同名函数在derived class中都可见。

6.如果不想继承base class所有函数,private derived可以运用转交函数来遮掩base class带参数的重载函数,不需要使用using声明式。

class Base{
public:
	virtual void mf1() = 0;
	virtual void mf1(int);
	//...
};

class Derived :private Base{
public:
	virtual void mf1()
	{
		Base::mf1();
	}
	//...
};

条款34

1.拥有纯虚函数的基类是一个抽象类,只能被继承,不能创建基类实体,只能创建derived class的实体。

2.public继承由两部分组成,函数接口继承和函数实现继承。其中函数的接口就是声明。

3.纯虚函数就是只有声明,没有实现,是接口继承。任何“继承了”他们的具象class必须重新声明,而且他们在抽象class中通常没有定义。声明一个纯虚函数是为了让derived class只继承函数接口。

4.可以为纯虚函数提供定义,但调用它的唯一途径是“调用时明确指出其class名称”。

Shape* ps = new Shape; //错误,Shape是抽象类
Shape* ps1 = new Rectangle;  //OK
ps1->draw();           //调用Rectangle::draw
Shape* ps2 = new Ellipse;    //OK
ps2->draw();           //调用Ellipse::draw
ps1->Shape::draw();    //调用Shape::draw
ps2->Shape::draw();    //调用Shape::draw
5.声明简朴的impure virtual函数的目的,是让继承类继承该函数的接口和缺省实现。即非纯虚函数能同时指定函数声明和函数缺省行为。

6.允许impure virtual函数同时指定函数声明和函数缺省行为有可能造成危险。(某些不需要该缺省行为的继承类也拥有了)

7.解决6中问题的方法是切断“virtual函数接口”和其“缺省实现”之间的连接。

class Airplane{
public:
	virtual void fly(const Airport& destination) = 0;
	//...
protected://因为是Airplane及其继承类的实现细目。乘客不需要知道怎么飞
	void defaultFly(const Airport& destination);
};

void Airplace::defaultFly(const Airport& destination)
{
	//缺省行为,将飞机飞至指定目的地
}
这次纯虚函数只提供接口,缺省行为由独立函数defaultFly提供。若想使用缺省实现,可以在其fly函数中对defaultFly做一个inline调用。

这样继承类就不可能意外继承不正确的fly实现代码了。

8.上一条中过度雷同的杉树名称可能引起class命名空间污染问题。下面利用“pure virtual函数必须在derived class中重新声明,但他们也可以拥有自己的实现”这一事实。

class Airplane{
public:
	virtual void fly(const Airport& destination) = 0;
	//...
};

void Airplace::fly(const Airport& destination)
{
	//缺省行为,将飞机飞至指定目的地
}

class ModelA::public Airplane{
public:
	virtual void fly(const Airport& destination)
	{
		Airplane::fly(destination);
	}
};

class ModelB::public Airplane{
public:
	virtual void fly(const Airport& destination);
};

void ModelB::fly(const Airport& destination)
{
	//将B型飞机飞至指定目的地
}

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


条款35:

见条款35(考虑virtual函数以外的其他选择)之Template Method模式和Strategy模式


条款36:

1.non-virtual函数是静态绑定的,virtual函数是动态绑定的。

#include <iostream>
using namespace std;

class B{
public:
	void mf(){ cout << "Base" << endl; }
	//...
};

class D :public B{
public:
	void mf(){ cout << "Derived" << endl; }
};

int main()
{
	D x;
	B* pB = &x;
	D* pD = &x;
	pB->mf();
	pD->mf();
	return 0;
}
输出是:

Base
Derived
原因是pB被声明为一个pointer-to-B,通过pB调用的non-virtual函数永远是B所定义的版本,即使pB指向一个类型为B派生的类的对象。

如果mf是个virtual函数,不论是通过pB或pD调用mf,都会导致调用D::mf,因为pB和pD真正指的都是一个类型为D的对象。

#include <iostream>
using namespace std;

class B{
public:
	virtual void mf(){ cout << "Base" << endl; }
	//...
};

class D :public B{
public:
	void mf(){ cout << "Derived" << endl; }
};

int main()
{
	D x;
	B* pB = &x;
	D* pD = &x;
	pB->mf();
	pD->mf();
	return 0;
}
输出是:

Derived
Derived
2.任何一个对象可能表现出B或D的行为,决定因素在于“指向该对象的指针”当初的声明类型。

条款37:

1.virtual函数是函数动态绑定,而缺省参数值却是参数静态绑定,非virtual函数是函数静态绑定。(静态绑定又名前期绑定,动态绑定又名后期绑定)

2.静态类型是它在程序中被声明时所采用的类型,所谓动态类型是指“目前所指对象的类型”。

#include <iostream>
using namespace std;


class Shape{
public:
	enum ShapeColor{ Red, Green };
	virtual void draw(ShapeColor color = Green) const = 0
	{
		cout << "noColor" << endl;
	}
};

class Rectangle :public Shape{
public:
	virtual void draw(ShapeColor color = Red) const
	{
		cout << (color==Green?"Green":"Red") << endl;
	}
};

class Circle :public Shape{
public:
	virtual void draw(ShapeColor color) const
	{
		cout << (color == Green ? "Green" : "Red") << endl;
	}
};

int main()
{
	Shape* pc=new Circle;
	pc->draw();                    //使用Shape的缺省参数Green
	Shape* pr = new Rectangle;
	pr->draw();                    //使用Shape的缺省参数Green
	pr->draw(Shape::Red);          //不使用缺省参数
	Rectangle* r = new Rectangle;
	r->draw();                     //使用Rectangle的缺省参数Red
	return 0;
}


输出是:

Green
Green
Red
Red
注意:缺省情况下调用的函数是继承类的,但是默认参数是基类提供的。

3.缺省参数值是静态绑定的,这样编译器就可以在编译期决定,这样可以提高运行期效率。

4.使用条款35的NVI手法可以避免这种情况:令base class 内的一个public non-virtual函数调用private virtual函数,后者可以被derived classes重新定义。这里我们让non-virtual函数指定缺省参数,而private virtual函数负责真正的工作。

class Shape{
public:
	enum ShapeColor{Red,Green,Blue};
	void draw(ShapeColor color = Red) const
	{
		doDraw(color);
	}
private:
	virtual void doDraw(ShapeColor color) const;
};

class Rectangle :public Shape{
public:
	//...
private:
	virtual void doDraw(ShapeColor color) const;
};


条款38:

1.复合关系是指某种对象内含有它中类型的对象,复合意味着has-a或is-implemented-in-terms-of。

2.人、汽车、一张视频画面等属于应用域,缓冲区、互斥器、查找树等属于实现域。当复合发生在应用域内的对象之间表现出has-a关系;当它发生在实现域内,则是表现is-implemented-in-terms-of的关系。

注意:STL中vector与list的关系就是后者。

3.如何根据list实现set。


条款39:

1.protected成员:对外界来说,行为与私有成员相似,对派生类来说,行为与公有成员相似。

2.如果classes之间的继承关系是private,编译器不会自动将一个derived class对象转换为一个base class对象。

3.由private base class继承而来的所有成员,在dervied class中都会变成private属性。纵使他们在基类中原本是protected或public属性。

4.private继承意味着implemented-in-terms-of,private继承只是一种实现技术,private base class的每样东西对继承类来说都只是实现枝节而已。

5.基类的virtual函数经过private继承后,在dervied class中都会变成private属性,如果重定义在dervied class的public下,则会提供public接口,仍然可以运行,但是不要这样做,这样会让客户端以为他们可以调用它。

#include <iostream>
using namespace std;


class Timer{
public:
	explicit Timer(int tickFrequency){}
	virtual void onTick() const{ cout << "Timer" << endl; }
	//...
};

class Widget :private Timer{
public:
	Widget() :Timer(1)
	{}/*
	void testOnTick()
	{
		onTick();
	}*/
	virtual void onTick() const{ cout << "Widget" << endl; }
};

int main()
{
	Widget test;
	test.onTick();
	return 0;
}
输出:

Widget
6.可以用复合来代替private继承,采用嵌套class,阻止继承类重新定义onTick()。
class Widget{
private:
	class WidgetTimer :public Timer{
	public:
		virtual void onTick() const;
		//...
	};
	WidgetTimer timer;
	//...
};
7.上面那种方法可以模拟Java和C#的“阻止derived class重新定义virtual函数”。

8.如果是继承,则必须包含头文件,如果是复合,则只需前置声明。所以使用复合可以降低编译依存性。

9.private主要用于“当一个意欲成为继承类者想访问一个意欲成为基类的protected成分,或为了重新定义一个或多个virtual函数。”

10.当所处理的class不带任何数据时(没有non-static成员变量,没有虚函数,也没有virtual base class),选择private继承而不是“继承加复合”,可使空间最优化。

#include <iostream>
using namespace std;


class Empty{};

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

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

int main()
{
	cout << sizeof(Empty) << endl;
	cout << sizeof(int) << endl;
	cout << sizeof(HoldsAnInt1) << endl;     //大于sizeof(int)
	cout << sizeof(HoldsAnInt1) << endl;     //大多数编译器等于sizeof(int)
	return 0;
}

输出结果:

1
4
8
8(注:我的VS2013输出8,大多编译器输出4)

这就是所谓的空白基类最优化在(emptybase optimization-EBO  empty base classopimization-EBCO)。在空基类被继承后由于没有任何数据成员,所以子类优化掉基类所占的1byte
11.上面空表内占的内存为1字节,因为面对“大小为零的独立对象”,通常C++官方勒令默默安插一个char到空对象内。

12.STL有许多技术用途的empty classes,其中内含有用的成员(通常是typedefs),包括基类unary_function和binary_function,这些是“用户自定义的函数对象”,通常会被继承的classes。


条款40:

1.如何进行多重继承?

#include <iostream>
using namespace std;


class BorrowableItem{
public:
	void checkOut(){ cout << "BorrowableItem" << endl; }
	//...
};

class ElectronicGadget{
private: //虽然是private,但两个checkOut有相同的匹配程度
	bool checkOut()
	{
		cout << "ElectronicGadget" << endl;
		return 1;
	}
	//...
};

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

int main()
{
	MP3Player mp;
	mp.checkOut();   //error:歧义,调用哪个checkOut
	return 0;
}
注:基类的private函数经过public继承后,对继承类来说,仍然是private。

2.如果程序从一个以上的base classes继承相同的名称(如函数、typedef等等),会导致歧义,如何解决?

可以在调用时明确调用那个checkOut函数。

mp.BorrowableItem::checkOut();   //OK
mp.ElectronicGadget::checkOut(); //error:cannot access private member
3.钻石型多重继承:有一个继承体系而其中某个base class和某个derived class之间有一条以上的相通线路。

class File{};
class InputFile :public File{};
class OutputFile :public File{};
class IOFile :public InputFile, public OutputFile
{};
C++的缺省做法是base class内的成员变量经由每一条路径被复制,IOFile从其每一个base class继承一份,所以其对象内应该有两份fileName成员变量。

4.如果不想有上述重复情况,必须令那个带有此数据(File)成为一个virtual base class。这样,必须令直接继承它(File)的classes(InputFile和OutputFile)采用“virtual继承”
class File{};
class InputFile :virtual public File{};
class OutputFile :virtual public File{};
class IOFile :public InputFile, public OutputFile
{};
5.C++标准程序库内含一个多重继承体系,结构和4相似,只不过class是class template,名称分别是basic_ios,basic_istream,basic_ostream和basic_iostream。
6.使用virtual继承的那些classes所产生的对象往往比使用non-virtual继承的兄弟们体积大,访问virtual base classes的成员变量时,也比访问non-virtual base classes的成员变量速度慢。(使用virtual base classes,经可能避免在其中放置数据)
7. 下面补充一个关于virtual函数和多重继承的隐晦错误
#include <iostream>
using namespace std;
 
class Base1{
public:
    virtual void foo1() {};
};
 
class Base2{
public:
    virtual void foo2() {};
};
 
class MI : public Base1, public Base2{
public:
    virtual void foo1 () {cout << "MI::foo1" << endl;}
    virtual void foo2 () {cout << "MI::foo2" << endl;}
};
 
int main(){
    MI oMI;
 
    Base1* pB1 =  &oMI;
    pB1->foo1();
   
    Base2* pB2 = (Base2*)(pB1); // 指针强行转换,没有偏移
    pB2->foo2();
     
    pB2 = dynamic_cast<Base2*>(pB1); // 指针动态转换,dynamic_cast帮你偏移
    pB2->foo2();
 
    return 0;
}

你会认为屏幕上会输出什么?是下面的结果吗?

MI::foo1

MI::foo2

MI::foo2

这样认为没有什么不对的,因为C++的多态性保证用父类指针可以正确的找到子类实现,并调用。所以会有上面的输出。

但是,现实却不是这样,下面是真实的输出:

(以上实现在VC 2005和Linux Gcc 4.1.2效果一致)

 

为什么

为什么会出现上面的情况呢,上面代码中的注释部分也许解释了,这里再来详细的来分析一下。

首先,C++使用一种称之为vtable(google “vtable” for more details)的东西实现virtual函数多态调用。vtable每个类中都有一个,该类的所有对象公用,由编译器帮你生成,只要有virtual函数的类,均会有vtable。在继承过程中,由于类Base1和类Base2都有vtable,所以类MI继承了两个vtable。简单的分析一下对象oMI内存结构,如下:

0 vtable_address_for_Base1 –> [MI::foo1, NULL]

4 vtable_address_for_Base2 –> [MI::foo2, NULL]

其实很简单,就两个vtable的指针,0和4代表相对地址,指针地址大小为4。

pB1的值为0(pB1 == 0),所以调用“pB1->foo1()”时,可以正确的找到MI::fool这个函数执行。

但是当使用强行转换,将pB1转给pB2,那么实质上pB2的值也是0(pB2 == 0),当调用“pB2->foo2()”时,无法在第一个vtalbe中找到对应的函数,但是却不报错,而是选择执行函数MI::foo1,不知道为什么会有这种行为,但是这种行为却十分恶心,导致结果无法预期的(最后调用的函数会与函数申明的循序有关),不太会引起注意,使得bug十分隐晦。

可以设想,当一个有复杂的业务逻辑的程序,而类似这种函数调用和指针强行转换分布在不同的函数或模块中,可想而知,bug定位十分困难。

当使用动态转换时,也就是“pB2 = dynamic_cast<Base2*>(pB1)”,dynamic_cast函数会根据尖括号中的类型进行指针偏移,所以pB2的值为4(pB2 == 4),这样调用“pB2->foo2()”就会按照期望的方式执行。

结论

上面的现象在单继承中是不会出现的,因为只有一个vtable(子类的virtual函数会自动追加到第一个父类的vtable的结尾)。所以不会出现上面的现象,而多重继承却出现了上面的想象,所以需要注意以下两点:

1)多重继承需要慎用

2)类型转换尽量采用c++内置的类型转换函数,而不要强行转换


你可能感兴趣的:(Conclusion for Inheritance and Object Oriented Design)