一:引言:
C++存在三大性质,分别是封装、继承和多态,这也是面向对程序语言具有的三大特性,前面我们已经介绍了其第一大特性封装,现在我们具体来谈谈什么是继承。
首先,举个经典的例子予以说明:
在自然界中,生物分为植物和动物,那么,我们以动物为例,比如,动物中的飞行类,飞行类动物又分为身躯为彩色的和单一色的,彩色的有蝴蝶,小鸟,七星瓢虫等等,单一色彩的又有飞蛾、蜻蜓等,按这样细分下来,每个个体都具有上一层的共性,又具有自身的特性。那么,可能文字有些难以理解,我用一张图来说明。
简单的说,下一层动物具有上一层的全部性质,但又有自身的特有的性质。
二:继承是什么?
继承(inheritance)是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。这样产生新的类,即保持原有类(基类或称为父类)的性质,又有自身独特的部分,称派生类或子类,继承呈现了面向对象程序设计的层次结构,体现了“由简单到复杂”的认知过程。
三:继承的格式:
class 派生类(子类) : 继承权限 基类(父类)
{};
如下图所示:
四:继承权限和访问权限的区别:
(1).访问权限
访问权限是类中对各成员变量作用域的限制。
(1).继承权限:代表基类成员在派生类中的可见性。
A.继承权限为public:代表基类中的成员在派生类中按基类本身的权限执行,基类中访问权限为public和protected的成员变量在派生类中能够执行访问,赋值,修改等操作,针对访问权限为private的成员变量,其在派生类中无法进行操作;在类外,只能对基类中访问权限为public的成员变量进行操作。protected和private均不能访问。
B.继承权限为protected和private:基类中各成员变量的访问权限在派生类中将降级为该继承权限。如下表格详细说明:
五.继承的方式:
继承的方式分为三类:一是单继承,二是多继承,三是既有单继承又有多继承这种继承方式如菱形继承。
六.单继承:
(1).什么是单继承?
单继承只有一个基类base,派生类在保持基类原有性质的同时自身又创建了成员变量或成员函数,这种继承方式称为单继承。
(2)单继承示意图:
(3). 单继承程序实现:
#include<iostream>
using namespace std;
class base//基类(父类)
{
public:
base()//构造函数
{
cout<<"base所占的字节数为:"<<sizeof(base)<<endl;
}
base(const base& s)//拷贝构造函数
{}
~base()//析构函数
{}
public:
int pub;
protected:
int pro;
private:
int pri;
};
class devirse :public base//派生类(子类)
{
public:
devirse()
:base()
{
cout<<endl;
cout<<"devirse所占字节数为:"<<sizeof(devirse)<<endl;
pub=111111;//(1)//试图在派生类中改变基类访问权限为public的变量
cout<<"派生类中pub="<<pub<<endl;//(1)成功赋值
cout<<"派生类中能成功改变基类中访问权限为public的成员变量"<<endl;
pro=222222;//(2)试图在派生类中改变基类中访问权限为protected的变量
cout<<"派生类中pro="<<pro<<endl;//(2)成功赋值
cout<<"派生类中能成功改变基类中访问权限为protected的成员变量"<<endl;
//pri=333333;(3)试图在派生类中改变基类中访问权限为private的变量
//cout<<"派生类中pri="<<pri<<endl;(3)失败
cout<<"派生类中不能改变基类中访问权限为private的成员变量"<<endl;
}
public:
int pubd;
protected :
int prod;
private:
int prid;
};
void FunTest()
{
base b;
b.pub =1;//在类外改变基类里访问权限为public的变量
cout<<"类外改变基类中访问权限为public的成员变量,pub值为:"<<b.pub<<endl;//成功赋值
devirse d;
d.pub =10;
cout<<"类外能改变派生类中继承基类访问权限为public的成员变量"<<d.pub<<endl;
//d.pro =20;//(2)试图在类外改变派生类中从基类继承下来的pro
//cout<<d.pro <<endl;//(2)无法通过编译,访问失败
cout<<"类外不能改变派生类中继承基类访问权限为protected的成员变量"<<endl;
//d.pri =30;//(2)试图在类外改变派生类中从基类继承下来的pro
//cout<<d.pri <<endl;//(2)无法通过编译,访问失败
cout<<"类外不能改变派生类中继承基类访问权限为private的成员变量"<<endl;
}
int main()
{
FunTest();
return 0;
}
(4).单继承运行结果:
在上述程序中,具体步骤已阐述的很清楚,这里就不再赘述,其中,为了能看到程序的运行结果,无法实现的语句已注释掉,并将结果显示在屏幕上,执行结果如下:
七.多继承:
(1)什么是多继承?
多继承有一个以上的父类。也就是说:一个派生类有两个或两个以上的直接父类时,这种继承方式称为多继承。
(2)多继承示意图:
(3)多继承程序实现:
#include<iostream>
using namespace std;
class c1//基类之一 c1
{
public:
c1()
{
cout<<"c1所占的字节数为:"<<sizeof(c1)<<endl;
}
c1(const c1& s)
{}
~c1()
{}
public:
int pubc1;
};
class c2//基类c2
{
public:
c2()
{
cout<<"c2所占的字节数为:"<<sizeof(c2)<<endl;
}
c2(const c2& s)
{}
~c2()
{}
public:
int pubc2;
};
class d :public c1,public c2//由c1和c2派生出的派生类d
{
public:
d()
:c1()
{
cout<<endl;
cout<<"d所占字节数为:"<<sizeof(d)<<endl;
}
public:
int pubd;
};
void FunTest()
{
d date;//派生类创建对象
date.pubc1=10;//试图对第一基类中访问权限为public的成员变量进行赋值
cout<<"pubc1="<<date.pubc1<<endl;//成功赋值
date.pubc2=20;//试图对第二基类中访问权限为public的成员变量进行赋值
cout<<"pubc2="<<date.pubc2<<endl;//赋值成功
date.pubd=30;//试图对派生类自身访问权限为public的成员变量进行赋值
cout<<"pubd="<<date.pubd<<endl;//成功赋值
}
int main()
{
FunTest();
return 0;
}
(4)程序运行结果:
具体还可以在基类中分别创建访问权限为protected和private的成员变量,分别在派生类和类外对其进行测试,测试方式与单继承类似,这里不再赘述。程序运行结果如下:
八.菱形继承(钻石继承):
(1)什么是菱形继承?
菱形继承:假如已有一个基类为base,那么c1单继承基类base,c2也单继承base ,d则采用多继承的方式,既继承c1又继承c2,这种特殊的继承方式称为菱形继承。因对象模型形状特殊,类似菱形,故而称其为菱形继承,它拥有一个浪漫又高贵的称呼,那就是钻石继承。
(2).菱形继承示意图:
(3)程序实现如下:
#include<iostream>
using namespace std;
class base//基类
{
public:
base()
{
cout<<"base所占字节大小为:"<<sizeof(base)<<endl;
}
base (const base& data)
{}
~base()
{}
public :
int pub;
};
class c1:public base//c1单继承base
{
public:
c1()
{
cout<<"c1所占的字节数为:"<<sizeof(c1)<<endl;
}
c1(const c1& s)
{}
~c1()
{}
public:
int pubc1;
};
class c2:public base//c2单继承base
{
public:
c2()
{
cout<<"c2所占的字节数为:"<<sizeof(c2)<<endl;
}
c2(const c2& s)
{}
~c2()
{}
public:
int pubc2;
};
class d :public c1,public c2//d多继承c1和c2
{
public:
d()
:c1()
{
cout<<endl;
cout<<"d所占字节数为:"<<sizeof(d)<<endl;
}
public:
int pubd;
};
void FunTest()
{
d date;
date.pubc1=10;
cout<<"pubc1="<<date.pubc1<<endl;
date.pubc2=20;
cout<<"pubc2="<<date.pubc2<<endl;
date.pubd=30;
cout<<"pubd="<<date.pubd<<endl;
//date.pub =40;//试图在类外访问pub
//cout<<"pub="<<date.pub<<endl;//访问失败
date.c1::pub =40;//试图在类外访问c1 中的pub
cout<<"date.c1::pub="<<date.c1::pub<<endl;//访问成功
date.c2::pub =50;//试图在类外访问c2 中的pub
cout<<"date.c2::pub="<<date.c2::pub<<endl;//访问成功
}
int main()
{
FunTest();
return 0;
}
(4)程序运行结果如下:
(5)菱形继承二义性:
细心的人可能已经发现,程序中为什么将date.pub =40;cout<<"pub="<<date.pub<<endl;
注释掉,而使用的是
date.c1::pub =40;
这种访问方式,我们将前者称为方式一,后者称为方式二那么,下面用一张图来具体阐述:
cout<<"date.c1::pub="<<date.c1::pub<<endl;
date.c2::pub =50;
cout<<"date.c2::pub="<<date.c2::pub<<endl;
(1)提出问题:
当使用菱形继承时,通过sizeof(d)来求派生类在内存中的大小,屏幕输出结果为20个字节,那么,当使用派生类创建对象时,date.pub=40;
却只能显示4个变量,那么,多出的4个字节是什么?难道是编译器出错了吗?
(2)给出解析:
实际上不是编译器的原因,c1和c2都是单继承base,所以在c1和c2中各存在一份基类的pub,那么,采用以上那种赋值方法,到底是将值赋给c1 还是c2呢?这时候,编译器思维混乱,不清楚程序员的想法,所以报出“对象访问不明确”这样的错误,那么,解决这个问题很简单,只需要加上对应的类域就可以了,这样要给哪个pub赋值,编译器就一清二楚了,也就是上面的方式二。
九.构造函数和析构函数:
(1)构造函数:
在派生类中,当遇到构造函数时,先调派生类自身的构造函数—->在派生类构造函数初始化列表部分调用基类的构造函数—->再执行派生类构造函数的函数体部分(用来构造派生类自身成员)
(2)析构函数:
与构造函数恰恰相反, 在派生类中遇到析构函数时,先调用派生类的析构函数—->销毁派生类自身管理的资源—->调用基类的析构函数—->销毁基类资源
(3)示意图如下:
十.虚继承:
(1)什么是虚函数?
若在普通的继承权限前加上关键字virtual( adj 虚拟,虚构的)会发生什么情况呢?形如class devirse :virtual public base
(2)虚函数程序如下:
#include<iostream>
using namespace std;
class base
{
public:
base()
{
cout<<"base所占的字节数为:"<<sizeof(base)<<endl;
}
public:
int pub;
};
class devirse :virtual public base
{
public:
devirse()
{
cout<<"虚继承中派生类所占字节的大小:"<<sizeof(devirse)<<endl;
}
public:
int pubd;
};
void FunTest()
{
devirse data;
}
int main()
{
FunTest();
return 0;
}
(3)程序运行结果:
(4)结果分析:
在基类中存在一个访问权限为public的成员变量pub;通过基类base来生成派生类,派生类中自身构造一个访问权限为public的成员变量pubd,若是普通的继承方式,通过sizeof(devirse)求派生类在内存中所占字节的大小,按理说应该是(4+4)=8个字节,可是,加上关键字virtual后,派生类的大小却为12个字节,比普通的继承方式多出4个字节,下面就对这多出的4个字节做出解释:
如图所示,虚函数中多出的4个字节将指向偏移量表格的地址,用来记录派生类与基类之间的偏移量。
十一.注意:
(1).同名隐藏。
1.函数重载:
同一个作用域,相同函数名—->参数列表不同(参数个数、类型、次序)和返回值是否相同无关。
2.同名隐藏:
在继承体系中,基类和派生类有相同名称的成员变量(成员函数),若使用派生类调用同名称的类成员时,优先调用派生类自身的成员。
(2).赋值
派生类对象可以赋给基类对象,但基类对象不可以赋给派生类对象
(3)指向
基类的指针或引用可以指向派生类对象
(4)友元&static
友元关系无法继承,静态成员可以继承
(5)合成构造函数
在继承体系中,若基类已经存在缺省的构造函数,而派生类的构造函数没有显示给出,编译器会为派生类合成默认的构造函数。其作用是在派生类初始化列表中调用基类的构造函数
另外,采用虚继承方式时,在派生类中,编译器也一定会为其合成默认的构造函数。
十二.总结:
本篇文章篇幅较长,是鄙人逐字逐句码上去的。文中稍有采用比较口语化的解释方式,这样是为了使问题更容易阐述,同时,几乎在每一个点后面都附有相应的图来予以说明,以图文并茂的方式剖析,找出问题的关键所在,从而使得分析的更加清晰。当然,本人学术有限,不足之处望读者多多包涵。