第四章 继承与派生
继承是C++语言的一种重要机制,该机制允许程序员在保持原有类特征的基础上进行更具体、更详细的类的定义,这样不仅可以节省开发程序的时间资源,还可为以后的程序设计增加新的资源。继承是通过派生方式实现的。(继承的实质:代码重用)
单继承:从一个基类继承。
多继承:从多个基类继承。
类有三种继承方式:公有继承(public)、私有继承(private)、保护继承(protected)
通过不同的继承方式限定成员的访问权限,以达到修改已有成员的目的。我们还可以通过同名覆盖、作用域分辨符和虚基类的方法达到此目的。
注意:派生类虽然继承了基类的所有成员,但构造函数和析构函数除外,因此,在派生类中,想要进行特别的初始化和清理工作时,就要加入新的构造函数和析构函数。
4.1继承与派生
派生出其他类的叫基类或父类,被派生的类叫派生类或子类。派生类同样可以作为基类派生出新的类。这样就形成了类的层次结构。
4.2派生类
4.2.1派生类生成过程
派生类是在基类的基础上,进行更具体、更详细的类的定义。派生新类一般经历三个步骤:吸收基类成员、改造基类成员、添加新的成员。
1.吸收基类成员
除构造函数和析构函数之外,新的类将基类的所有成员(很重要)全部接受。即使有些基类成员在派生类中不起任何作用,但其也被继承下来,并在生成对象时也要占有一定的内存空间。
2.改造基类成员
改造两方面:一、用派生类定义时的继承方式来控制访问权限
二、通过在派生类中定义一个与基类同名的数据或是函数,来对基类的数据或函数成员进行重新定义。
3.添加新的成员
派生类新成员必须与基类成员不重名。
4.重写构造函数和析构函数
4.2.2派生类定义
1.单继承定义格式
class<派生类名>:<继承方式><基类名>
{
<派生类新定义成员>
};
继承方式,也就是要指定派生类的访问控制方式。public,private,protected
2.多继承定义格式
class<派生类名>:<继承方式1> <基类名1>, <继承方式2> <基类名2>, ...
{
<派生类新定义成员>
}
4.2.3访问权限
访问限定有两方面的含义:
1.派生类新增成员函数对基类(继承来的)成员的访问。
2.从派生类对象之外对派生类对象中的基类成员的访问
公有继承、私有继承、保护继承
(上表中的第二列指的是派生类继承基类中的各个成员以后,这些成员分别在派生类中所具有的的属性,例如第4、5行所表示的是基类中的public和protected类型的成员在派生类中的成员属性是private)
对于公有继承、私有继承来说派生的类都不能直接访问基类中的私有成员,要想访问某些私有成员只能通过调用基类的成员函数,这样显得很不方便。
要想使这些私有成员既便于派生类访问,又禁止外界对它的操作,可以把这些私有成员定义为保护成员。当派生类使用保护继承方式派生时,基类的公有成员和保护成员在派生类都具有保护成员属性。
4.2.4派生类的构造函数和析构函数
我们想对派生类中新添加的成员进行初始化时,就必须按照实际需要添加新的构造函数。如果要对从基类继承下来的成员进行初始化,则还要由基类的构造函数来完成。当需要传递参数给基类构造函数时,必须为派生类建立一个构造函数,并由它来对基类的构造函数所需的参数进行设置。
析构函数是无参函数,派生类是否需要析构函数与基类无关,它按自己的实际情况决定是否添加新的析构函数。而且派生类的析构函数的执行不因为派生类没有定义相应的析构函数而不执行基类的析构函数。
一、构造函数
构造函数包含两个内容:
1、对派生类新增数据成员进行初始化。
2、对从基类继承下来的成员进行初始化。如果派生类中还有子对象,还应包含对子对象初始化的构造函数。
例如:
#include<iostream.h>
class Base
{
public:
Base(int x)
{cout<<"基类的构造函数 "<<x<<endl;}
};
class A:public Base
{
Base m1;
public:
A(int a,int b):Base(a),m1(b)
{cout<<"派生类的构造函数"<<endl;}
};
void main()
{
A obj(10,20);
}
构造函数的执行顺序是:先调用基类的构造函数,后调用子对象的构造函数,再调用派生类自己的构造函数
二、析构函数
它的执行顺序与构造函数正好相反,先执行派生类的析构函数,再执行基类的析构函数。
4.3多继承
4.3.3多继承的构造函数与析构函数
一、构造函数
多继承构造函数的执行顺序仍然是基类、对象、派生类,处于同一层次的多个基类之间的构造函数的执行顺序是按照派生类定义时从左到右的顺序来执行。
二、析构函数
析构函数的调用顺序正好与构造函数的相反
#include<iostream.h>
class Base1
{
public:
Base1(int x){cout<<"基类 1 构造函数"<<"x="<<x<<endl;}
~Base1(){cout<<"基类 1 析构函数"<<endl;}
};
class Base2
{
public:
Base2(){cout<<"基类 2 构造函数"<<endl;}
~Base2(){cout<<"基类 2 析构函数"<<endl;}
};
class A:public Base2,public Base1
{
Base1 m1;
Base2 m2;
public:
A(int a,int b):Base1(a),Base2(),m1(b),m2()
{cout<<"派生类构造函数"<<endl;}
~A(){cout<<"派生类析构函数"<<endl;}
};
void main()
{
A obj(10,20);
}
执行结果:
执行构造函数有三步。
第一步,调用基类构造函数,如有多个基类,执行顺序按派生类定义时从左到右的顺序,与派生类构造函数中构造函数所定义的各项顺序无关从结果的第一、二行来看。
第二步,基类对象创建时调用基类构造函数,这是程序结果中的第三、四行。
第三步,调用派生类自身的构造函数,结果第五行
析构函数也有三步。
顺序刚好相反:
第一步:先执行派生类的析构函数,结果第六行
第二步:撤销基类对象时调用基类的析构函数,结果第七、八行
第三步:执行基类的析构函数,结果第九、十行
4.4虚基类
例如:
#include<iostream.h>
class Base
{
protected:
int x;
public:
Base(){x=5;}
};
class Der1:public Base
{
public:
Der1();
{cout<<x<<endl;}
};
class Der2:public Base
{
public:
Der2()
{
x+=20;
cout<<x<<endl;
}
};
class Con:public Der1,Der2
{
public:
Con()
{
cout<<x<<endl;//error
cout<<"Der1 x="<<Der1::x<<endl;//加上作用域分辨符,就对
cout<<"Der2 x="<<Der2::x<<endl;
}
};
void main()
{
Con obj;
}
例中,派生类Con的构造函数有问题,该构造函数试图输出一个它有权访问的变量x,但是这里出现了二义性。因为Der1和Der2都是派生类Con的直接基类,它们都是从基类Base中派生来的,因此它们都有x数据成员,Con构造函数中要输出的x变量,究竟是从Der1还是从Der2上派生过来题中没有明确的说明。
解决二义性问题,有两种方法:
1、使用作用域分辨符来标示并分别访问它们;
2、将直接基类的共同基类设置为虚基类
虚基类的概念
如果一个派生类是由多个基类中派生出来的,恰巧这几个基类又是从同一个基类中派生出来的,这几个基类从上一级基类中继承来的成员就会拥有相同的名称。当派生类在创建对象时,这些同名成员在内存中就会有多个副本,我们不需要这么多副本,希望它们合成一个,这时我们可以用虚基类的方式来解决。
将派生出多个基类的那个基类设置为虚基类,它能使这几个基类的那些从不同路径继承上来的上级类成员在内存中只有一个副本。虚函数的说明是在定义派生类时,在需定义为虚基类的基类前加上virtual关键字。
例如:
....
class Der1:virtual public Base
{
public:
Der1();
{cout<<x<<endl;}
};
class Der2:virtual public Base
{
public:
Der2()
{
x+=20;
cout<<x<<endl;
}
};
.....
虚基类的初始化:
语法上与多继承的初始化是一样的,但在调用构造函数的顺序上是有差别的。
(1)先调用虚基类的构造函数,在调用非虚基类的构造函数。
(2)当同一层有多个虚基类,按照它们的说明顺序调用它们的构造函数;
(3)当虚基类是由非基类派生时,则先调用基类构造函数,再调用派生类的构造函数。
#include<iostream.h>
class Base1
{
public:
Base1()
{cout<<"Base1 的构造函数\n";}
};
class Base2
{
public:
Base2()
{cout<<"Base2 的构造函数\n";}
};
class D1:public Base1,public Base2
{
public:
D1()
{cout<<"D1 的构造函数\n";}
};
class D2:public Base1,public Base2
{
public:
D2()
{cout<<"D2 的构造函数\n";}
};
class C:public D1,virtual public D2
{
public:
C()
{cout<<"C 的构造函数\n";}
};
void main()
{
C obj;
}
具体分析一下程序执行构造函数的过程
当主函数创建对象C时,就要执行C类的构造函数。因为C类是从D1,D2派生来的,所以按照规则需要先调用D1、D2的构造函数,后执行C类的构造函数,因为D2是虚基类所以先调用,因此在这一层调用顺序是D2、D1、C
再分析一下D2的调用过程,D2是由Base1、Base2派生出来的,Base2是虚基类,因此先调用Base2的构造函数,因此这一层顺序是:Base2、Base1、D2
再分析下D1的调用过程,D1也是由Base1、Base2派生出来的,顺序仍然是Base2、Base1、D1,但是有区别的是因为Base2也是D2的虚函数,并已在D2中执行过,所以在D1中就不需要再执行了(不明白,求解)。所以,D1构造函数调用是按Base1,D1的顺序执行。