继承和派生
单继承
为了代码复用
继承类的声明格式:
class [派生类名] : [继承权限] [父类名]
class Base//基类
{
public:
int _pubB;
Base(int pub, int pro, int pri)
:_pubB(pub)
,_proB(pro)
,_priB(pri)
{}
protected:
int _proB:
private:
int _priB;
}
以上是一个基类,也叫做父类
class Derived : public Base
{
private:
int _priD;
protected :
int _proD;
}
以上类就是一个派生类
继承权限
- 任何的继承方式下派生类都会继承基类的所有变量,只是不能直接访问基类的私有成员变量,可以通过基类的可访问的方法来进行改变。
- 以public的继承方式继承的派生类继承到的基类变量在在自己的类中的权限保持不变。
- 派生类中变量权限为public,在派生类中的权限也为public
- 派生类中变量权限为protect,在派生类中的权限也为protect
- 派生类中变量权限为private,在派生类中的权限也为private,但是不能直接进行访问
- 以protected的继承方式继承的派生类继承到的变量在自己的类中的权限变为protected(除了private)
- 派生类中变量权限为public,在派生类中的权限也为protected
- 派生类中变量权限为protected,在派生类中的权限也为protected
- 派生类中变量权限为private,在派生类中的权限也为private,并且不能直接访问
- 以protected的继承方式继承的派生类继承到的变量在自己的类中的权限变为protected(除了private)
- 派生类中变量权限为public,在派生类中的权限也为private
- 派生类中变量权限为protected,在派生类中的权限也为private
- 派生类中变量权限为private,在派生类中的权限也为private,并且不能直接访问
- 不写继承权限时,默认使用private方式继承
- 友元关系不能继承,因为友元函数不是类的成员
- 静态成员变量可以被继承,但是在继承体系中只有一份
基类和派生类的关系(is-a)
- 派生类的对象就是一个基类的对象
对象模型:类中成员变量在内存中的布局形式(非静态成员变量)
Base://基类成员
_pubB;
_proB;
_priB;
Derived://派生类成员
_pubB;
_proB;
_priB;
_pubD;
_proD;
_priD;
使用角度:所有用到基类对象的位置都可以使用子类对象了类代替
赋值兼容规则—public继承方式
- 子类对象可以直接赋值给父类对象
- 子类对象给派生类对象赋值的时候,只赋值派生类中的基类成员。
- 基类对象不可以直接赋值给派生类类对象
- 派生类成员比基类成员多,在赋值的时候,在赋值派生类有的变量而基类没有的时候,就会发生错误。
- 基类的指针可以指向派生类的对象,但是只能访问派生类中的基类的成员。
- 基类的引用可以指向派生类的对象,也只能访问派生类中的基类的成员。
同名隐藏
在基类和派生类中具有相同名称的成员(函数、变量)都会发生同名隐藏(名字相同即可,不需要参数也相同)。
如果用派生类对象去访问这个进程体系中的同名成员,只能访问派生类自己的,基类的同名成员无法访问,除非加上基类作用域。
class Base
{
public:
void Print()
{
cout<<"hehe"<<endl;
}
}
class Derived : public Base
{
public:
int Print()
{
cout<<"haha"<<endl;
return 0;
}
}
int main()
{
Base b;
b.Print();//打印hehe
Derived d;
d.Print();//打印haha并返回0
d.Base::Print()//打印hehe
}
派生类的默认成员函数
构造函数:
- 在构造派生类的对象的时候,首先初始化其成员
- 但是派生类的成员中有一个基类的成员,而前面我们说过,如果类中的成员变量含有另一个类的对象,就必须将此成员对象放在初始化列表中进行初始化
- 初始化一个对象的时候就会调用其类的构造函数
- 所以在构造派生类的对象的时候执行语句运行到其初始化列表的时候,就会先构造基类的对象,然后才继续进行自己的构造函数。
如果基类中的构造函数需要手动输入参数,那么就必须在派生类的构造函数中显式的给出参数列表用以初始化
析构函数:
- 在析构派生类的对象的时候会直接去调用派生类的析构函数,首先会清理自己的成员,但是其成员变量中还含有一个基类,所以当派生类调用完自己的析构函数后会调用基类的析构函数,用以销毁其中含有的基类对象。
多继承
一个类可以继承一个类,也可以继承于多个类
格式:
- 继承列表用逗号隔开
每个父类前面都要写上继承权限
class A : public B, protected C { …… } //A类继承于B类和C类,并且B类的继承方式时public,C类的继承方式是protected
多继承派生类的对象模型:
按继承列表从左到右的顺序排列:
class A: B对象成员; C对象成员; A自己的成员变量;
菱形继承(钻石继承)(单继承和多继承的综合)
class B
{
public:
int _b;
}
class C1 : public B
{
public:
int _c1;
}
class C2 : public B
{
public:
int _c2
}
class D : public C1, public C2
{
public:
int _d;
}
菱形继承中含有二义性:
- C1对象中含有从B中继承来的_b变量,C2对象中也有从B中继承来的_b变量。
- 所以在D类对象中如果访问_b变量就会产生二义性,因为D对象中既有C1对象的_b变量,也有C2对象中的_b变量。
如果直接访问就会报错,为了更加明确,在访问的时候必须加一个作用限定符。
D d;//构造一个D类的对象 d._d = 10;//没问题 d._b = 20;//报错,无法确定是C1类继承的_b变量还是C2类继承的_b变量 d.C1::_b = 30;//没问题,因为限定了在C1中的_b变量。
虚拟继承
在继承权限前面加上
virtual
关键字。
class B
{
public:
_b;
}
class D : public B
{
public:
_d;
}
D d;
虚拟继承的对象模型(以d为例):
- 基类成员在下,派生类成员在上
- 前四个字节是一个指针,指向的是一个存放偏移量的空间(表格也叫虚基表)
- 存放偏移量的空间的前四个字节存放对象相对于自己的的偏移量,接着四个字节存放相对于基类对象的偏移量
- 用派生对象去访问基类成员的话是通过偏移量表格查询到基类的成员相对于自己的偏移量,然后在去访问
虚拟继承和普通继承的区别
- 对象多了四个字节,这四个自己存放偏移量表格的地址
- 虚拟继承派生类的基类成员在下,自己的成员在上,但是普通继承基类成员在在上,自己的成员在下
- 派生类对象访问基类成员要通过偏移量表格
- 编译器会给派生类合成一个构造函数并且多传一个参数 1。
- 合成构造函数的目的,在构造期间将偏移量表格的地址放在前四个字节。
- 多传一个参数1的作用:检测是否为虚拟继承
菱形虚拟继承解决菱形继承的二义性问题
将菱形继承的所有单继承变为虚拟继承
class B
{
public:
int _b;
}
class C1 : virtual public B
{
public:
int _c1;
}
class C2 : virtual public B
{
public:
int _c2
}
class D : public C1, public C2
{
public:
int _d;
}
虚拟继承的派生类内对象存模型:
从上到下分别为:
- “左边”的类(除了基类的成员)
- “右边”的类(除了基类的成员)
- 派生类自己的成员
- 基类成员
- 其中“左边”的类的基类成员和“右边”的类的基类成员都是最底下的[基类成员](合二为一)
图示:
虚拟继承和直接继承的区别
时间:
- 虚拟继承的访问时间是大于直接继承的,因为虚拟继承访问基类成员时时通过偏移表来访问的,者无疑增加了访问的时间
空间:
- 虚拟继承比直接继承多占用了一部分空间,就是偏移表占用的空间,还有偏移表的地址所占的空间