一.继承:
1.继承的概念:
继承(inheritance)是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。这样产生新的类,即保持原有类(基类或称为父类)的性质,又有自身独特的部分,称派生类或子类,继承呈现了面向对象程序设计的层次结构,体现了“由简单到复杂”的认知过程。
2. 通俗易懂的说法:
继承就是:“龙生龙,凤生凤,老鼠的儿子会打洞”。
3.详情请见:
http://blog.csdn.net/dai_wen/article/details/78511266
二.虚函数:
1.什么是绑定?
绑定是将函数体与函数调用相联系称为绑定
1.绑定的分类:
绑定分为早绑定(静态绑定)和晚绑定(动态绑定)。
(1).早绑定:
早绑定:绑定这个动作发生在程序运行之前完成,这是我们所熟悉的方法, 因为在C编译器下只有早绑定,早绑定又称为静态绑定。
(2).晚绑定:
晚绑定:绑定在程序运行期间,基于指向对象的类型,晚绑定又称为动态 绑定。
2.如何让一个程序实现晚绑定?
使用virtual关键字来实现,该关键字只能修饰类的成员函数。
3.虚函数的形式:
class Number
{
protected:
virtual void number()=0;//纯虚函数
//没有任何实现的函数,至少基类中是这样,需要使用记号=0来表示纯虚函数
}
4.菱形继承的虚函数:
1.模型:
2.菱形继承的虚函数的程序实现:
下面,我们用程序来模拟实现上图内容,c1,c2分别作为base的派生类,均为单继承,同时, 采用虚拟公有继承的方式;devirse为多继承,其基类分别为c1,c2;
程序如下:
#include<iostream>
using namespace std;
class base
{
public:
base()
{
cout<<"base所占字节大小为:"<<sizeof(base)<<endl;
}
public:
int pub;
};
class c1:virtual public base
{
public:
c1()
{
cout<<"c1所占字节数大小为:"<<sizeof(c1)<<endl;
}
public:
int pubc1;
};
class c2:virtual public base
{
public:
c2()
{
cout<<"c2所占字节数大小为:"<<sizeof(c2)<<endl;
}
public:
int pubc2;
};
class devirse : public c1,public c2
{
public:
devirse()
{
cout<<"devirse所占字节数大小为:"<<sizeof(devirse)<<endl;
}
public:
int pubd;
};
void FunTest()
{
devirse d;
d.pub=10;
cout<<d.pub <<endl;
}
int main()
{
FunTest();
return 0;
}
3.程序分析:
1.base
base中含有一个访问权限为public的成员变量pub,大小为4个字节,这是显而易见,毫无疑问的。
2.c1
针对c1,它虚拟继承了base,该派生类的大小应该是:
4个字节的base大小+自身4个字节大小的pubc1+用来指向偏移量列表的4个字节大小的空间,故而,c1所占字节大小应为(4+4+4)=12。
3.c2
c2的继承方式完全等同于c1, 故而,所占内存大小应该也为12个字节;
4.devirse
说到这里,devirse所占内存空间的大小就出现的争议:
1.第一种说法:
按照虚拟公有继承的方式,devirse所占内存空间的大小理论上应为:c1的大小(12B)+c2 的大小(12B)+派生类自身所占内存空间的大小(4B)+指向偏移量列表的4个字节的大小(4B),也即:12+12+4+4=32个字节;
2.第二种说法:
按照普通公有继承的方式,devirse所占内存空间的大小理论上应为:c1的大小(12B)+c2 的大小(12B)+派生类自身所占内存空间的大小(4B),也即:12+12+4=28个字节;
对于我们的思维方式来说,结果莫过于这两种,要么28,要么32,要想得到结果,那么,我们只需要通过运行程序便知:
4.程序运行结果:
5.结果分析:
base,c1,c2所占内存空间的大小与我们想象中的相符,令人出乎意料的是:devirse所占内存空间的大小不是32,也不是28,而是24个字节!!这是为什么呢?
原来, 在devirse中,只有一份base,并不是c1,c2中均继承下来的两份。首先,在devirse类中,指向偏移量表格该项代表所属类到基类的偏移量,除基类外,c1,c2分别有8个字节,devirse自身有4个字节,加上共有的基类base4个字节,故而:8+8+4+4=24个字节。
6.虚继承–解决菱形继承的二义性和数据冗余的问题
- 虚继承解决了在菱形继承体系里面子类对象包含多份父类对象的数据冗余&浪费空间的问题。
- 虚继承体系看起来好复杂,在实际应用我们通常不会定义如此复杂的继承体系。一般不到万不得已都不要定义菱形结构的虚继承体系结构,因为使用虚继承解决数据冗余问题也带来了性能上的损耗。
三.多态:
1.什么是多态?
概念:顾名思义,多态就是指同一事物在不同场景下的多种形态。
2.举个栗子:
1.你吃了吗?
在国外,外国人见面的时候打招呼,通常会说: “how are you? ”;
在中国,朋友见面时,正常情况不会说:“你好吗? ”这样的话,
一般会说:“你吃了吗? ”
那么,具体是吃什么?吃饭? 吃零食?还是下午茶?
这时,大家就会更具不同的场所来回答,如果我们在食堂相遇,显而易见,他是问我吃“饭”了没,同样,在不同的场景下, 有着不同的具体对象 等着填充。
2.我要上厕所:
人有三急,假如有一天,你在公园散步,突然间想上厕所,那么,首先,你得找到公厕,这时候,左边是男厕,右边为女厕,那么你要到哪个厕所去,这就取决于你的性别了。
3.程序实现:
以上厕所为例:
#include<iostream>
using namespace std;
#include<windows.h>
class WashRoom//修建厕所
{
public:
void GoToManWashRoom ()
{
cout<< "Man-->Please Left" <<endl;
}
void GoToWomanWashRoom ()
{
cout<< "Woman-->Please Right" <<endl ;
}
};
class Person//人
{
public:
virtual void GoToWashRoom(WashRoom & _washRoom ) = 0;
};
class Man:public Person//男士往左
{
public:
virtual void GoToWashRoom(WashRoom & washRoom )
{
washRoom.GoToManWashRoom();
}
};
class Woman:public Person//女士朝右
{
public:
virtual void GoToWashRoom(WashRoom & washRoom )
{
washRoom.GoToWomanWashRoom ();
}
};
void FunTest()
{
WashRoom washRoom;//找到厕所
//来了十个人,性别随机
for ( int iIdx = 1 ; iIdx <= 10 ; ++iIdx)
{
Person* pPerson;//一个人
int iPerson = rand ()%iIdx;//性别随机
if (iPerson&0x01)
pPerson = new Man ;//若为男士
else
pPerson = new Woman ;若为女士
pPerson->GoToWashRoom (washRoom);//选择适当方向
delete pPerson;//销毁对象
pPerson = NULL ;
Sleep(1000 );
}
}
int main()
{
FunTest();
return 0;
}
4.实现结果:
5.实现条件:
《1》基类中必须有虚函数, 在派生类中必须要对基类中的虚函数“重写”;
《2》必须通过基类的指针或者引用来调虚函数,不可通过派生类创建对象,利用点(.)操作符访问
6.实现原理:
使用派生类重写基类的虚函数实现多态,要求函数名、参数列表、返回值完全相同(协变除外)。
虚函数重写:在基类和派生类中都有虚函数,且虚函数的原型(函数返回类型,函数名称,参数列表) 都相同。
参数列表:参数的个数,参数的类型及参数的排列顺序。
(1)协变除外
在基类中的返回类型为base ,在派生类中为devirse ,函数的返回类型不同,但仍可以实现重写;
(2)析构除外
在基类中,析构函数的函数名为~base();而在派生类中,析构函数的函数名为~deverise();基类与派生类的函数名不相同,但也可以构成重写。
7.哪些函数不能被定义为虚函数?
(1).友元函数
(2).构造函数
(3).静态成员函数(static)
(4).内联函数(inline)
8.两个最好
(1)最好将基类中的析构函数给成virtual;
(2)最好不要使基类的运算符重载给成virtual
具体原因敬请关注本人更新下一篇博客。
小结:
1、使用派生类重写基类的虚函数实现多态,要求函数名、参数列表、返回值完全相同(协变和析构除外)。
2、基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性。
3、只有类的非静态成员函数才能定义为虚函数,静态成员函数不能定义为虚函数。
4、如果在类外定义虚函数,只能在声明函数时加virtual关键字,定义时不用加。
5、构造函数不能定义为虚函数,虽然可以将operator=定义为虚函数,但最好不要这么做,使用时容易混淆。
6、不要在构造函数和析构函数中调用虚函数,在构造函数和析构函数中,对象是不完整的,可能会出现未定义的行为。
7、最好将基类的析构函数声明为虚函数。(析构函数比较特殊,因为派生类的析构函数跟基类的析构函数名称不一样,但是构成覆盖,这里编译器做了特殊处理)
8、虚表是所有类对象实例共用的
本文中存在一些疑点尚未解决,仅仅抛出了问题,如3.2.7只是给出条例,未做出解释,对于遗留的这些问题,将在下一篇博文中持续更新。入有不便,望多加理解