前言
面向对象程序设计有四个主要的特点:抽象,封装,继承和多态。继承性是面向对象设计最重要的特征,可以是如果不掌握继承性,就没有掌握类的对象的精华,就没有掌握面向对象程序设计的真谛。本篇文章主要讲的就是c++继承的相关知识点。
一、继承与派生的概念
面向对象技术强调软件的可重用性,c++的可重用性就是通过继承来实现的。一个新类从已有的类哪里获得其已有的特性,这种现象称为类的继承。从另外一个角度来说,从一个已经有的类产生一个新的子类,称为类的派生。关于基类与派生类的关系,可以表述为:派生类是基类的具体化,而基类则是派生类的抽象。
二、派生类的声明方式
声明派生类的一般格式为:
class 派生类名:继承方式 基类名
{
派生类新增加的成员
};
继承方式包括:public,protected,private,继承方式是可选的,如果不写继承方式,默认的是private。
三、派生类的构成
1.从基类接受成员
派生类把基类全部的成员接受过来(不包括构造函数和析构函数),也就是说是没有选择的。可能出现这样的情况,有些基类的成员在派生类中是用不到的,但是也必须继承过来,这就会造成数据的冗余,尤其是在多次派生之后,会在派生类中存在大量无用的数据,不仅浪费了大量的空间,而且在对象的建立,赋值,复制过程中花费更多的时间,从而降低了效率。这在目前的c++标准中是没有办法解决的,因此要求我们根据派生类的需要慎重选择基类,使冗余量最小。
2.调整从基类接受的成员
接受基类的成员使不能选择的,但是程序员可以对这些成员作某些调整。例如可以改变基类成员在派生类中的访问属性,这是通过指定继承方式实现的。此外还可以在派生类中声明与基类同名的成员,则在派生类中的成员会覆盖基类的的同名成员,但是应当注意,如果是成员函数,不仅函数名要相同,函数的参数表也要相同,如果不相同就成为了函数的重载而不是函数的覆盖了。
3.在声明派生类时增加的成员
这部分内容是很重要的,它体现了派生类对基类的扩展。此外在定义派生类的同时还应当定义派生类的构造和析构函数。因为构造与析构函数是不能从基类继承的。
四、派生类成员的访问属性
对基类和派生类自己增加的成员是按照不同规则来处理的。具体来说,在讨论访问属性时,需要考虑以下几种情况:
(1).基类的成员函数访问基类的成员函数
基类的成员函数可以访问基类的成员。
(2).派生类的成员函数访问派生类自己增加的成员
派生类的成员函数可以访问派生类的成员。
(3).基类的成员函数访问派生类的成员
基类的成员函数只能访问基类的成员,不能访问派生类的成员。
(4).派生类的成员函数访问基类的成员
稍微复杂一些。
(5).派生类外访问派生类的成员
派生类外可以访问派生类的公用成员,不能访问派生类的私有成员与保护成员。
(6).派生类外访问基类类的成员
稍微复杂一些。
上述的(4)和(6)涉及到基类的成员才派生类中的访问属性问题,**不仅要考虑基类成员的访问属性,还要考虑继承方式。**这些在下面会通过三张表表示。
1.公用继承
2.私有继承
3.保护成员和保护继承
1.保护成员
保护成员不能被类外访问,这点和私有成员类似,但是有一点是与私有成员不同,保护成员可以被派生类的成员函数引用。而私有成员是不可能被派生类的成员函数引用的。
2.保护继承
在派生中,成员有四种不同的访问属性
1.公用的,派生类内类外都可以访问
2.受保护的,派生类内可以访问,类外不可以访问,其下一层的派生类可以访问
3.私有的,派生类内可有访问,类外不可以访问
4.不可访问的,类内类外都不能访问。
4.多级派生的访问属性
和上面的方法一样,由于在实际的运用中基本不会用到,这里不再讲述。
注意:私有继承和保护继承只是一种理论的方法,在实际中很少用到。
五、派生类的构造析构函数
前面已经讲到过,基类的构造函数是不能继承的,在声明派生类时,派生类并没有把基类的构造函数继承过来,因此对继承过来的基类成员的初始化规工作也要由派生类的构造函数承担。所以在设计派生类的构造函数时,不仅要考虑派生类所增加的成员的数据成员的初始化,还要考虑基类的数据成员的初始化。解决这问题的思路是:在执行派生类的构造函数时,调用基类的构造函数。
1.简单派生类的构造函数
构造函数的一般形式:
派生类构造函数名(总参数表):基类构造函数名(参数表)
{
派生类新增数据成员初始化语句;
}
注意:
1.在类外定义派生类的构造函数,而在类中声明派生类的构造函数时,声明部分不包括上面的基类构造函数名(参数表)部分,只有在定义时才将他列出。
2.还可以将上述派生类新增加数据成员的初始化语句也用初始化列表初始化。
3.在建立对象时,构造函数的调用顺序是:派生类构造函数先调用基类构造函数,然后再执行派生类构造函数本身。
4.派生类释放对象时,先执行派生类析构函数,再执行基类析构函数。
2.有子对象的派生类的构造函数
构造函数的一般形式:
派生类构造函数名(总参数表):基类构造函数名(参数表),子对象名(参数表)
{
派生类新增数据成员初始化语句;
}
注意:
1.执行派生类构造函数的顺序是:先调用基类构造函数,再调用子对象构造函数,最后调用派生类构造函数本身。
2.析构顺序与构造顺序相反。
3.多层派生时类的构造函数
构造函数的一般形式:
派生类构造函数名(总参数表):直接基类构造函数名(参数表)
{
派生类新增数据成员初始化语句;
}
注意:不要列出每一层派生类的构造函数,只需列出其上一层派生类(即直接基类)的构造函数即可。
4.派生类构造函数的特殊形式
在派生类构造函数时,可以有以下两种特殊的形式。
1.当不需要对派生类新增加的成员函数进行初始化时,派生类的构造函数体为空,即构造函数是空函数。此时派生类构造函数的参数个数等于基类构造函数和子对象的参数之和。此时派生类构造函数的作用只是为了将参数传递给基类和子对象,执行基类构造函数和子对象构造函数。在实际工作中常见这种用法。
2.如果在基类中没有定义构造函数,或者定义了没有参数的构造函数,那么在定义派生类的构造函数时可以不写基类构造函数,因为此时派生类构造函数没有向基类构造函数传递参数的任务,在调用派生类的构造函数时,系统会自动调用基类的默认构造函数。相反如果在基类中定义了带参数的构造函数,那么在派生类中就必须显示的调用。
3.如果即定义了有参也定义了无参构造函数,则在派生类中可以包含基类构造函数,也可以不包含基类构造函数。
5.派生类的析构寒素
只需记住一句话:派生类析构函数的调用顺序和构造函数的调用顺序相反。
六、多重继承
1.声明多重继承的方法
用一个例子来说明:
class D:public A,public B,public C
{
}
2.多重继承派生类的构造函数
多重继承构造函数的形式:
派生类构造函数名(总参数表):基类1构造函数(参数表),基类2构造函数(参数表),基类3构造函数(参数表)
{
派生类新增加数据成员初始化;
}
3.多重继承引起的二义性问题
二义性问题:继承的成员同名而产生二义性问题。
这里假设C是A和B的直接派生类。
1.两个基类有同名的成员。
可以用基类名+作用域限定符::来解决无法识别哪一个基类成员的问题。
2.两个基类和派生类都有同名的成员。
规则:基类的同名成员在派生类中被屏蔽。
3.如果A和B都是从同一个基类N派生(菱形继承问题)
可以用直接派生类名+作用域限定符::来解决无法识别哪一个基类成员的问题。
4.虚基类
1.虚基类的作用
c++提供虚基类的方法,使得在继承间接共同基类是只保留一份成员。
声明虚基类的一般形式:
class 派生类名:virtual 继承方式 基类名
注意:
(1).虚基类并不是在声明基类时声明的,而是在声明派生类时,指定继承方式时声明的。
(2).为了保证基类在派生类中只继承一次,应当在该基类的所以直接派生类中声明为虚基类,否则仍然会出现对基类的多重继承。
2.虚基类的初始化
注意:
在定义D类规则函数是,与以往的方法有所不同,以前在派生类的构造函数中只负责对其直接基类初始化,再由其直接基类负责对间接基类初始化,现在由于基类在派生类中只有一份数据,所以这一份数据的初始化必须由派生类直接初始化,所以规定在派生类中不仅要对其直接基类初始化,还要对虚基类初始化。
七、基类与派生类的转换
我们知道不同的数据类型在一定条件下可以相互转换,如整形可以赋值给浮点型,但是不能把一个整形赋值给指针变量。这种不同类型之间的自动转换和赋值,称为赋值兼容。基类与派生类对象之间也有赋值兼容关系。具体体现在以下几个方面:
1.派生类对象可以向基类对象赋值。
注意:
子类型是单向的,不可逆的。只能用子类对象对其基类对象赋值,而不能反过来。同理,同一基类的不同派生类对象中间也不能赋值。
2.派生类对象可以代替基类对象向基类对象的引用赋值或初始化。
3.如果函数的参数是基类对象或者基类对象的引用,相应的实参可以用之类对象。
4.派生类对象的地址可以赋值给基类对象的指针变量,也就是说,指向基类对象的指针变量也可以指向派生类对象。
实际上,上述第四种情况,可以实现多态。参考如下文章
c++之多态
注意:
通过指向基类对象的指针,只能访问派生类中的基类成员,而不能访问派生类增加的成员。
八、继承与组合
对象成员的类型可以是派生类的基类,也可以是另一个已经定义的类。在一个类中以另一个类的对象作为数据成员的,称为类的组合。
组合和继承都有效地利用了已有类的资源,但是两者的概念和用法不同。
通过继承,建立了派生类与基类的关系,它是一种“是”的关系。如黑人是人,白人是人。
通过组合,建立了成员类与组合类的关系,它是一种“有”的关系。如不能说教授是一个生日,只能说教师有一个生日的属性。
继承是纵向的,组合是横向的。