前言:
在之前的文章中,我们已经介绍了C++面向对象中的三大特性之一:封装。
那么这一篇文章,我们将介绍同为C++面向对象中的三大特性之一:继承。
那么什么是继承呢?
继承的概念:
一个类获取现有的类的所有属性和行为的机制
创建基于现有的类的新类,可以重用现有的类的属性和方法
可以在新创建的子类中添加属性和方法
被继承的类一般称为基类(父类),继承了基类的类一般称为派生类(子类)
代码如下:
// 基类
class Animal {
// eat() 函数
// sleep() 函数
};
//派生类
class Dog : public Animal {
// bark() 函数
};
继承的优点与缺点:
优点:
提高代码的复用性
提高代码的维护性
让类与类之间产生关系,是多态的前提
缺点:
增强了类之间的耦合,软件开发的一个原则是高内聚、低耦合,内聚是一个模块内各个元素彼此结合的紧密程度,耦合是一个软件里面不同模块之间的互相连接的数量。
继承的语法:
class 子类名 : 继承方式 父类名1 , 继承方式 父类名2 , ..... , 继承方式 父类名n
{
派生类的成员
};
继承的访问权限规则:
公有继承(public):当一个类派生自 公有基类时,基类的 公有成员也是派生类的 公有成员,基类的 保护成员也是派生类的 保护成员,基类的 私有成员不能直接被派生类访问,但是可以通过调用基类的 公有和 保护成员来访问。
保护继承(protected): 当一个类派生自 保护基类时,基类的 公有和 保护成员将成为派生类的 保护成员。
私有继承(private):当一个类派生自 私有基类时,基类的 公有和 保护成员将成为派生类的 私有成员。
PS:
一个派生类会继承所有的基类方法与成员,包括私有的基类方法与成员,只不过是被编译器所隐藏以致无法访问到所继承到的私有属性与方法。
一个派生类继承了所有的基类方法,但下列情况除外:
基类的构造函数、析构函数和拷贝构造函数。
基类的重载运算符。
基类的友元函数。
继承的构造和析构函数的顺序:
从上面的小提示,我们也能得知基类的构造函数、析构函数和拷贝构造函数不会被继承下来,那么我们就需要去讨论继承中的基类与派生类的构造函数、析构函数的出现顺序了。
通过正常的逻辑来说:应先有父亲,才会有儿子,所以我们的构造函数调用,应先调用我们基类的构造函数,再去调用派生类的构造函数。
那么析构函数应该先调用谁的呢?
我们来看一段代码:
#include <iostream>
using namespace std;
class Test
{
public:
Test()
{
cout<<"Test::Test()"<<endl;
}
private:
int mc;
};
class Base
{
public:
Base(int a)
{
ma = a;
cout<<"Base::base()"<<endl;
}
~Base()
{
cout<<"Base::~base()"<<endl;
}
private:
int ma;
Test t;
};
class Derive : public Base
{
public:
Derive(int b):Base(b)
{
mb = b;
cout<<"Derive::derive()"<<endl;
}
~Derive()
{
cout<<"Derive::~derive()"<<endl;
}
private:
int mb;
};
int main()
{
Derive d(2);
return 0;
}
那么我们来看它的运行结果:
我们可以清楚地得知,我们的析构函数顺序就是先调用派生类的析构函数,再去调用基类的调用函数
总结:
1.先调用基类构造函数,构造基类部分成员变量,再调用派生类构造函数构造派生类部分的成员变量。2.基类部分成员的初始化方式在派生类构造函数的初始化列表中指定。
3.若基类中还有成员对象,则先调用成员对象的构造函数,再调用基类构造函数,最后是派生类构造函数。析构顺序和构造顺序相反。
继承访问同名成员:
在我们写代码的过程中,经常会出现函数名重复的情况,那么在我们继承中,如何做出处理呢?
访问子类同名成员:直接访问即可
访问父类同名成员:需要添加作用域
如果访问的是静态的同名成员呢?
很简单,如上操作即可!!!
菱形继承:
单继承:
多继承:
菱形继承:
假设Person类有一个成员变量_age,Student类的对象中包含了这个 _age 成员,Teacher类的对象中也包含了 _age 成员,但是 Assistant 类依次继承了 Student 和 Teacher类,不考虑其他成员,就最终结果而言,Assistant类中确实包含了两份 _age 成员。
这就导致了菱形继承的 冗余性 和 二义性。
冗余性:存在重复的数据,比如_age 要存两份
二义性:如果要访问 _age 成员,是访问 Student 类的 _age,还是访问 Teacher类的_age
我们根据上面的情况不难看出,菱形继承中也存在数据的二义性,这里的二义性是由于他们间接都有相同的基类导致的。 这种菱形继承除了带来二义性之外,还会有有数据冗余浪费内存空间。
所以引入了虚基类与虚基表。
虚继承:
虚继承之所以产生虚基类指针和虚基类表,都是为了保证不管多少层的继承,虚基类的数据只有一份,从而避免二义性和不浪费类对象内存空间;(虚基类表不占用类对象的空间);
vbptr指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual
table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。