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的缺省参数GreenShape* pr = new Rectangle;pr->draw(); //使用Shape的缺省参数Greenpr->draw(Shape::Red); //不使用缺省参数Rectangle* r = new Rectangle;r->draw(); //使用Rectangle的缺省参数Redreturn 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:歧义,调用哪个checkOutreturn 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,经可能避免在其中放置数据)
#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相关推荐
- Object oriented Design
OO Concepts – Object model (abstraction, encapsulation, modularity, hierarchy); data abstraction; in ...
- Java SE 008 理解面向对象程序设计 (Inside Object Oriented Programming)
Java SE 008 理解面向对象程序设计 (Inside Object Oriented Programming) 前言:此笔记为圣思园张龙老师讲述的java视频课程笔记,自己看视频学习时记录的, ...
- OO开发思想:面向对象的开发方法(Object oriented,OO)
面向对象的开发方法(Object oriented,OO)认为是好文章吧,拿来分享一下(转载) 面向对象的开发方法(Object oriented,OO) 从事软件开发的工程 师们常常有这样 的体会: ...
- 面向对象编程(Object Oriented Programming)概念总结及延伸(一)
1.介绍 笔者的梦想是成为一个架构师,但是要成为一个合格的架构师是相当不易的,它既需要丰富的项目经验也需要不断地吸取新的知识,而且在这过程中我们也要不断巩固基础知识.我也注意到了,现在主流的文章大都集 ...
- Java基础篇--面向对象(Object Oriented)
Java基础篇--面向对象(Object Oriented) 1. 面向对象概念 1.1 什么事面向对象 1.2 理解面向对象 1.3 面向对象的特点 1.4 面向对象开发设计特征 2. 类与对象 2 ...
- python Object Oriented Programming
python 知识点整理(五) 本文只是对python部分知识点进行学习和整理 本篇主要是针对python的Object Oriented Programming的总结 本文目录 python 知识点 ...
- Java OOP(Object Oriented Programming)个人理解及总结
面向对象编程(Object Oriented Programming,OOP,面向对象程序设计) 其三大特征:封装,继承,多态: 封装:解决数据的安全问题. 继承:解决代码的重用问题. 多态:解决程序 ...
- Python编程基础:第三十九节 面向对象编程Object Oriented Programming
第三十九节 面向对象编程Object Oriented Programming 前言 实践 前言 到目前为止我们都是函数式编程,也即将每一个功能块写为一个函数.其实还有一种更常用的编程方式被称为面向对 ...
- Coursera课程Python for everyone:Quiz: Object Oriented Programming
Object Oriented Programming 11 试题 1. Which came first, the instance or the class? instance class fun ...
最新文章
- Java项目:潜艇大战项目(java+swing)
- .NET中的枚举(Enum)
- Notepad++ JSON关键字自动提示
- 【机器学习】机器学习用到的常用术语
- Visual C++ MFC/ATL开发-提高篇
- android sqlite使用之模糊查询数据库数据的三种方式
- Redis学习---(10)Redis 集合(Set)
- SAP License:HANA在线日志被误删后如何恢复?
- android怎么查看方法被谁调用,Android中查看布局文件中的控件(view,id)在哪里被调用(使用)...
- Appium+python自动化(十一)- 元素定位秘籍助你打通任督二脉 - 下卷(超详解)...
- wininet InternetOpen\InternetOpenUrl\InternetReadFile 等
- 日本企业给我们的启示
- 冰点还原标准版-中文版(全面支持Windows 7)7.0.020.3172(最新版)下载与注册
- Re:天选2之找不到WLAN网络
- Electron播放 RTMP流 实现
- 添加视频字幕后期制作Premiere Pro 2022中文
- Adam Harley的卷积神经网络3D视觉化模型
- 推荐系统——矩阵分解
- 关于正向级数收敛而它的平方也收敛的证明
- Ajax与分页的实现