继承和派生一般性概念
继承:在一个已经存在的类的基础上建立一个新的类,新类从已有的类那里获取某些已有的特征,这种现象称为类的继承。
派生:从另一个角度说,从已有的类产生一个新的类,称为类的派生。
派生类的一般声明形式为:
class派生类名:[继承方式] 基类名
{
派生类新增的成员
};
例1 继承和派生演示
1)建立一个控制台空工程,并加入实现文件ExampleOne.cpp。
#include<iostream>
usingnamespace std;
classCBase
{
public:
int m_iBase;
int BaseFunc()
{
return m_iBase;
}
};
classCDerived:public CBase
{
public:
int m_iDerived;
int DerivedFunc()
{
return m_iDerived;
}
};
intmain()
{
CDerived *pd=new CDerived;
int itest;
itest=pd->DerivedFunc();
return 0;
}
如上,CBase类派生了一个新的类CDerived,或者说类CDerived从类CBase继承而来,这样,类CDerived不但具有新增的成员m_iDerived和DerivedFunc(),还有从类CBase继承而来的成员m_iBase和BaseFunc(),从而类CDerived实际具有4个成员。
类的继承方式
派生类并非把基类的所有特性都继承下来。它还受两方面约束:
不论何种方式,下面这些基类的特征是不能从基类继承下来的:
构造函数
析构函数
用户重载的new 运算符
用户重载的=运算符
友员关系
对基类的除上面以外的其他成员的继承受继承方式限制,有三种继承的方式:私有继承、保护继承和公有继承。不同的继承方式将导致从基类继承来的成员在派生类中具有不同的访问限制属性。
三种继承方式一般形式为:
class 派生类名:private 基类名 //私有继承
{
派生类新增成员
};
class 派生类名:protected 基类名 //保护继承
{
派生类新增成员
};
class 派生类名:public 基类名 //公有继承
{
派生类新增成员
};
不同的继承方式下,各种成员的访问属性总结如下表所示。
(不同的继承方式下各种成员的访问属性列表)
继承方式 | 基类中的访问属性 | 派生类中的访问属性 |
公有继承 | private | 为基类私有 |
protected | 保护类型 | |
public | 公有类型 | |
保护继承 | private | 为基类私有 |
protected | 保护类型 | |
public | 保护类型 | |
私有继承 | private | 为基类私有 |
protected | 私有类型 | |
public | 私有类型 |
派生类对基类成员的覆盖
如果基类和派生类中存在同名的成员数据或者成员函数,那么派生类的成员数据和成员函数将覆盖掉基类的成员数据和成员函数。而且与基类和派生类的访问属性无关;与基类和派生类的函数间的参数和返回类型无关。
例2 演示派生类对基类成员的覆盖
1)建立一个控制台空工程,并加入实现文件ExampleTwo.cpp。
#include<iostream>
usingnamespace std;
classCBase
{
protected:
int m_i;
public:
int Func(int i)
{
return m_i;
}
};
classCDerived:public CBase
{
public:
int m_i; //覆盖掉基类的m_i;
int Func() //覆盖掉基类的Func
{
m_i=9; //访问自身的成员
CBase::m_i=22; //通过作用域解析符访问基类的成员
return m_i;
}
};
voidmain()
{
CDerived obj;
obj.Func();
//obj.Func(3); //错误!基类的Func被覆盖
obj.CBase::Func(3); //正确,通过作用域解析符访问基类的成员
}
派生类对象的初始化与清除
如果需要在初始化派生类成员时也初始化基类的成员,则可在派生类的构造函数的初始化成员列表中初始化基类,如果基类的构造函数均带有参数,则基类必须出现在派生类的初始化成员列表中。
派生类构造函数及初始化列表的一般形式为:
派生类名(构造函数参数列表): 基类名(参数列表),派生类成员1(参数列表1),,,派生类成员n(参数列表n)
例 3 演示派生类对象的初始化和清除
1)建立一个控制台空工程,加入文件ExmapleThree.cpp,文件内容如下:
#include<iostream>
usingnamespace std;
classCBase
{
protected:
int m_iBase;
public:
//CBase(){}
CBase(int i){m_iBase=i;}
};
classCDerived:public CBase
{
public:
int m_iDerived;
CDerived(int i);
};
CDerived::CDerived(inti):CBase(0),m_iDerived(i)
{
//注意派生类构造函数的写法,以及掌握怎样在派生类中访问基类的成员变量
}
intmain()
{
CDerived *pd=new CDerived(1);
delete pd;
return 0;
}
执行派生类构造函数的顺序为:
1) 调用基类的构造函数,初始化基类的数据成员;
2) 初始化派生类的数据成员;
3) 执行派生类的构造函数本身。
派生类的析构函数相对简单,与无继承关系的普通类的析构函数形式相同。执行析构函数的顺序为:
1) 调用派生类的析构函数;
2) 调用基类的析构函数。
基类对象和派生类对象间的转换与赋值
基类对象和派生类对象间的转换
正如整型数据可以自动转换成double型一样,基类对象与派生类对象间也存在赋值兼容的关系。假如有如下基类和派生类:
classCBase
{
//...
};
classCDerived:public CBase
{
//...
};
基类对象和派生类对象间的转换具体表现在以下几个方面:
派生类对象可以向基类对象赋值;
CBase base;
CDerived derived;
base=derived; //将派生类对象的基类部分拷贝给基类对象
派生类对象可以替代基类对象向基类对象的引用进行赋值或者初始化;
CBase b1;
CDerived d1;
CBase &b1Alias=b1;//普通的引用
b1Alias=d1; //将d1的基类部分拷贝给b1Alias(即b1)
CBase &b2=d1; //d1基类部分的引用
如果函数的参数是基类对象或者基类对象的引用,则相应的实参可以是派生类对象。
void Func1(CBase base)
{
//...
}
void Func2(CBase &base)
{
//...
}
CDerived derived;
Func1(derived); //将derived的基类部分拷贝给行参base
Func2(derived) //将derived的基类部分当作行参使用
派生类对象的地址可以赋给基类类型的指针变量,或者说,基类型的指针可以指向派生类对象。
CBase *pBase;
CDerived derived;
pBase=&derived;
基类对象和派生类对象间的赋值
如果用户定义了基类的拷贝构造函数,而没有定义派生类的拷贝构造函数,那么在用一个派生类对象初始化新的派生类对象时,两对象间的派生类部分执行缺省的行为,而两对象间的基类部分执行用户定义的基类拷贝构造函数。
如果用户重载了基类的对象赋值运算符=,而没有定义派生类的对象赋值运算符,那么在用一个派生类对象给新的派生类对象赋值时,两对象间的派生类部分执行缺省的赋值行为,而两对象间的基类部分执行用户定义的重载赋值函数。
如果用户定义了派生类的拷贝构造函数或者重载了派生类的对象赋值运算符=,则在用已有派生类对象初始化新的派生类对象时,或者在派生类对象间赋值时,将会执行用户定义的派生类的拷贝构造函数或者重载赋值函数,而不会再自动调用基类的拷贝构造函数和基类的重载对象赋值运算符,这时,通常需要用户在派生类的拷贝构造函数或者派生类的赋值函数中显式调用基类的相应函数。
例4 演示基类对象和派生类对象间的赋值
1)建立控制台空工程ExampleFour,并加入实现文件ExmapleFour.cpp。
#include<iostream>
usingnamespace std;
classCBase
{
protected:
char *m_pszData;
public:
CBase(const char *pszData)
{
m_pszData=new char[strlen(pszData)+1];
strcpy(m_pszData,pszData);
}
CBase(const CBase &base)
{
m_pszData=newchar[strlen(base.m_pszData)+1];
strcpy(m_pszData,base.m_pszData);
}
CBase &operator =(const CBase &base)
{
if(this==&base)
return *this;
m_pszData=new char[strlen(base.m_pszData)+1];
strcpy(m_pszData,base.m_pszData);
return *this;
}
~CBase(){delete []m_pszData;}
};
classCDerived:public CBase
{
public:
CDerived(const char*pszData):CBase(pszData){}
};
voidmain()
{
CDerived d1("Hello!");
//派生类使用缺省的拷贝构造函数、基类调用用户定义的拷贝构造函数
CDerived d2=d1;
//派生类使用缺省的赋值操作,基类调用用户重载的赋值运算符
d1=d2;
}
保护构造函数与私有构造函数
保护构造函数
如果一个类的构造函数和析构函数是保护类型的,则不能在程序中实例化该类的对象(相当于从外部来调用该类的保护型成员,也就是非公有成员),这样的类称为抽象类。
由于保护的构造函数可以被派生类访问,所以我们通常将抽象类用于各种派生类的公共基类,使得该抽象类派生的类拥有共同的特性。
例5 保护构造函数演示
1)建立控制台空工程ExampleFive,并加入实现文件ExampleFive.cpp。
#include<iostream>
usingnamespace std;
classCAbstract
{
protected:
CAbstract(){} //基类保护类型的构造函数
//...
};
classCDerived:public CAbstract
{
//...
};
voidmain()
{
//CAbstract objAbs;//错误,无法实例化,因为无法从类的外部来访问该类的非公有成员。那怎么访问?
CDerived objDer; //答案:可以通过派生类的构造函数访问基类的保护型构造函数
}
私有构造函数
如果类的构造函数是私有的,这样的类也是抽象类,但是因为派生类也无法调用它,从而无法用派生的方式初始化,但是可以用静态函数的方式实例化该类的对象。
例6 私有构造函数演示
1)建立控制台空工程ExampleSix,并加入实现文件ExampleSix.cpp。
#include<iostream>
usingnamespace std;
classCAbstract
{
private:
CAbstract(){} //保护类型的构造函数
//...
public:
static CAbstract *CreateInstance()
{
return new CAbstract();
}
static void ReleaseInstance(CAbstract *pThis)
{
delete pThis;
}
};
voidmain()
{
CAbstract *pObj=CAbstract::CreateInstance();
//...
CAbstract::ReleaseInstance(pObj);
}
回忆以前的单例模式(只能实例化一个对象的类的实现),跟以上例子有什么相同和不同之处。
多重继承和虚基类
多重继承(Multiple Inheritance)
如果一个派生类同时有两个或者多个基类,派生类从两个和多个基类中继承所需的属性,这种继承方式称为多重继承(Multiple Inheritance)。
声明多重继承的一般形式为:
class派生类名:继承方式1 基类名1,继承方式2 基类名2,,,继承方式n 基类名n
{
//... 派生类新增的成员。
};
例7 多重继承和成员二义性消除的简单方法演示
1)建立控制台空工程ExampleSeven,并加入实现文件ExampleSeven.cpp。
#include<iostream>
usingnamespace std;
classCBaseA
{
public:
int m_i;
void Func()
{
m_i++;
}
};
classCBaseB
{
public:
int m_i;
void Func()
{
m_i++;
}
};
classCDerived:public CBaseA,public CBaseB
{
public:
int m_j;
};
intmain()
{
return 0;
}
以上类的声明,相应的数据成员在内存中的布局形式如图所示。
(数据成员在内存中的布局形)
从图中很容易看出同样的成员m_i,在基类中有一份,在派生类中也有一份。可以想象一下,如果使用派生类对象来访问该成员的时候,就会出现二义性问题:到底访问的是基类中的m_i,还是访问派生类中的m_i?所以调用时必须注意防止二义性。具体用法见主函数体中相关语句和注释。
intmain()
{
CDerived d;
d.m_j=0; //正常
//i=d.m_i; //引起二义性
//通过成员民限定消除二义性,访问从CBaseA中继承来的m_i d.CBaseA::m_i=1;
//通过成员民限定消除二义性,访问从CBaseB中继承来的m_i
d.CBaseB::m_i=2;
//d.Func(); //引起二义性
//通过成员民限定消除二义性,访问从CBaseA中继承来的成员函数Func()
d.CBaseA::Func();
//通过成员民限定消除二义性,访问从CBaseB中继承来的成员函数Func()
d.CBaseB::Func();
CBaseA *pa;
CBaseB *pb;
pa=&d; //系统会自动将pa指向对象d的CBaseA部分
pb=&d; //系统会自动将pb指向对象d的CBaseB部分
return 0;
}
二义性的另外一种消除方法-----虚基类
如果一个派生类有多个直接基类,而这些直接基类又有一个共同的基类,则在生成派生类对象时,系统会为派生类对象生成共同基类数据成员的多份拷贝。
如果希望派生类对象中只包含一份共同基类的数据成员,则可以在声明派生类时,通过virtual加继承方式,使派生对象只保留共同基类的一份数据成员。
例8 虚基类使用方法演示
1)建立控制台空工程ExampleEight,并加入实现文件ExampleEight.cpp。
#include<iostream>
usingnamespace std;
classCBase
{
public:
int m_i;
};
classCBaseA:public CBase
{
public:
int m_ia;
};
classCBaseB:public CBase
{
public:
int m_ib;
};
classCDerived: public CBaseA, public CBaseB
{
public:
int m_d;
};
intmain()
{
return 0;
}
按照以上类声明形式,则对应的数据成员内存布局形式如下图所示。
(数据成员内存布局形式图)
类体系结构如下图所示。
(类体系结构图)
从图中也可以看出,同样的数据成员在基类和派生类中都有,同样的道理,访问该成员的时候也会出现二义性。
下面介绍使用虚基类的方式来消除二义性。将以上类CBaseA和CBaseB的声明形式改为:
classCBaseA:virtual public CBase
{
public:
int m_ia;
};
classCBaseB:virtual public CBase
{
public:
int m_ib;
};
如此一来,这样派生类对象可以直接使用公共基类的成员,从而消除二义性。
intmain()
{
CDerived d;
d.m_i=9;
return 0;
}
可以在一个已经存在类的基础之上来产生一个新的类,对于已经存在类具有的方法和属性,新类就不用再重新添加。这样可以提高代码可重用性。新产生的类具有两种成员:新添加的成员和原来类具有的成员。这个已经存在的类叫做基类,产生的新类叫做派生类。类的继承方式有三种:公有、私有、保护继承。其中由于共有继承不改变基类成员在派生类中的访问属性,所以用得较多。当派生类中具有基类里边的同名的成员时,派生类的该同名成员会覆盖基类同名的成员。派生类的成员有两种,那么对应的派生类的构造函数对这些成员变量进行初始化的时候就要完成两部分工作。对本身新添加数据成员的初始化和从基类接收过来成员的初始化(调用基类构造函数)。派生类对象和基类对象由赋值兼容的关系,凡是该关系是单向的。当一个类的构造函数是私有的或者受保护型的,那么这样的类不能实例化本身,那么这样的类叫做抽象类。当一个派生类具有两个以上的基类时,就叫做多重继承,这种时候有可能产生成员的二义性,使用虚基类可以消除。