C++中有三大特点—封装,继承,多态,而本文就三个特点之一的继承进行总结与概述;
- 继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。简而言之就是可以讲已有的类进行一定功能的延续。被继承的类称之为基类(父类),继承了功能的类叫做派生类(子类)
- 继承的格式
首先定义一个普通的Base类作为我们的基类,即被继承的类;
class Base
{
public:
void FunTest()
{
cout << "Base FunTest()" << endl;
}
public:
int _pub;
protected:
int _pro;
private:
int _pri;
下面定义派生类,它继承了Base类的一定功能;
class Derive :public Base
{
private:
int _d;
};
可以看出其与普通类的区别在于定义类名时加了一些东西;
其中派生类与基类我们已经知道,下面介绍继承类型,或者称为继承关系
- 继承类型
继承类型可以理解为子类继承父类的方式。方式不同,继承下来的成员使用时的权限就不同;
继承共有3种方式 :public(公有继承)、protected(保护继承)、private(私有继承)。
当我们使用公有继承时,分别在类内外使用三种不同权限的父类成员:
与上表所述相同,可以对三种继承一一进行尝试。继承类型是可以省略的,而当使用class而省略继承类型时,会默认为private类型进行继承;而如果是struct时,继承类型默认为public类型进行继承。
public继承是一个接口继承,而protected/private继承是一个实现继承,前者满足is-a原则,每个子类对象也都是一个父类对象;后者满足has-a原则,基类的部分成员并非完全成为子类接口的一部分。因此我们在平时使用时会更多的使用公有继承而非另两种继承方式,并且由于在公有继承时可以将一个子类对象看成父类对象,则我们可以使用一个子类对象给一个父类对象进行赋值但反之不行,父类的指针与引用同样可以指向一个子类对象,但子类的指针与引用不可以指向一个父类。这是继承的赋值兼容规则;
class Base
{
private:
int _pri;
};
class Derive :public Base
{
private:
int _d;
};
void Funtest()
{
Base b;
Derive d;
b = d;
d = b;
}
首先进行子类和父类的值得互相赋予
可以看到父类时无法赋值给子类的,并且无法强制类型转换;
指针和引用的相互赋予
void Funtest()
{
Base b;
Derive d;
Base *pb = &d;
Derive *pd = &b;
}
子类指针不可以指向父类,这里可以进行强制类型转化,但由于两者的大小可能不同,不建议使用。
- 派生类的默认成员函数
当了解上述的内容之后,我们基本已经可以使用一个继承的类了,接下来便开始研究派生类的默认成员函数。首先当我们去定义一个普通类对象时,编译器会调用类的构造函数,而派生类定义对象时,不仅会调用自己的构造函数,还会调用基类的构造函数,并且是先调用基类的构造函数再调用自己的:
class Base
{
public:
Base()
{
cout << "Base()" << endl;
}
~Base()
{
cout << "~Base()" << endl;
}
};
class Derive :public Base
{
public:
Derive()
{
cout << "Derive()" << endl;
}
~Derive()
{
cout << "~Derive()" << endl;
}
};
void Funtest()
{
Derive d;
}
int mian()
{
Funtest();
return 0;
}
同样的,在析构时,调用自己的析构函数之后会再次调用基类的析构函数.
那么当我们的派生类又有一个子对象时,三者的构造函数又会有基类->子对象->自己的顺序去一次调用构造函数。
当然在继承使用时应该注意这么几点:
- 基类没有缺省构造函数,派生类必须要在初始化列表中显式给出基类名和参数列表
- 基类没有定义构造函数,则派生类也可以不用定义,全部使用缺省构造函数
- 基类定义了带有形参表构造函数,派生类就一定定义构造函数
其思想可以参考普通类有子对象时的构造函数。
- 继承体系中的作用域
在继承中,子类与父类是两个不同作用域。当我们在派生类中定义一个与基类中的成员函数名相同的函数时,子类成员将屏蔽父类对成员的直接访问(在子类成员函数中可以使用作用域限定符访问)——隐藏(重定义)
class Base
{
public:
void FunTest()
{
cout << "Base:FunTest()" << endl;
}
};
class Derive :public Base
{
public:
void FunTest()
{
cout << "Derive:FunTest()" << endl;
}
};
void Funtest()
{
Derive d;
d.FunTest();
d.Base::FunTest();
}
int main()
{
Funtest();
return 0;
}
隐藏需要满足以下几个条件:
- 不同的作用域
- 函数名相同
不构成重写
继承中的友元与静态成员函数
友元类无法继承,即基类的友元函数不能访问派生类的私有和保护的成员
class Base
{
friend void Funtest();
private:
int _Base_pri;
};
class Derive :public Base
{
protected:
int _pro;
private:
int _pri;
};
void Funtest()
{
Base b;
b._Base_pri = 10;
Derive d;
d._pro = 10;;
d._pri = 10;
}
int main()
{
Funtest();
return 0;
}
基类定义了static成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有 一个static成员实例
class Base
{
public:
Base()
{
++_count;
}
public:
static int _count;
};
int Base::_count = 0;
class Derive1 :public Base
{
};
class Derive2 :public Base
{
};
void Funtest()
{
Derive1 d1;
Derive1 d2;
Derive1 d3;
Derive2 d4;
cout << Base::_count << endl;
Derive1::_count = 0;
cout << Base::_count << endl;
}
int main()
{
Funtest();
return 0;
}
上面这段代码这里我们将基类的构造函数将count自加操作,每当构造一个对象是会使_count加1,再在派生类域中将值赋成0;
由此可见在派生类和基类中的_count是同一变量;
- 单继承、多继承与菱形继承
其中对象成员的分布为:
单继承:
多继承:
菱形继承:
从基类的成员分布可以看到,其中基类1和2继承的最基类成员都出现在派生类的成员中,那么当我们赋值给最基类成员时,便会由于不知道访问谁而出错
class A
{
public:
int _a;
};
class B1 :public A
{
public:
int _b1;
};
class B2 :public A
{
public:
int _b2;
};
class C :public B1, public B2
{
public:
int _d;
};
void Funtest()
{
C c;
c._a = 10;
}
int main()
{
Funtest();
return 0;
}
为了解决这里二义性和数据冗余的问题,我们引进了虚继承;
- 虚继承
这里介绍一个关键字:virtual,用于虚拟继承和虚函数前;
当我们在菱形继承的中间一环添加虚继承则可以解决这个问题菱形继承二义性和数据冗余的问题
class A
{
public:
int _a;
};
class B1 :virtual public A
{
public:
int _b1;
};
class B2 :virtual public A
{
public:
int _b2;
};
class C :public B1, public B2
{
public:
int _d;
};
void Funtest()
{
C c;
c._a = 10;
}
int main()
{
Funtest();
return 0;
}
这里我们便对最基类的成员成功赋值,我们跟进程序的反汇编可以发现,虚继承和普通的继承区别在于:
会合成默认的构造函数,真正去合成这个函数,并且调用,
查看这时对象的内存可以看到编译器放了一个指针在一个对象的前4个字节。
访问这四个字节时我们看到了两个值0和8
这两个数字前者为成员中指针距离自己这个对象的首地址的字节数,后者为基类成员距离自己的自己数,因此成员中的指针是指向了一个偏移量。也由此可以看出,虚拟继承的成员分布方式不同于普通继承。
而当虚拟继承时,中级基类和派生类的成员分布分别为:
这样我们在访问最基类的成员时,会通过偏移地址访问到,便解决了二义性的问题,但同样的,产生了效率降低的问题。