面向对象程序设计的核心思想是封装(数据抽象)、继承和多态(动态绑定)。
通过使用数据抽象,我们可以将类的接口与实现分离;
使用继承,可以定义相似的类型并对其相似关系建模;
使用动态绑定,可以在一定程度上忽略相似类型的区别,而用统一方式使用它们的对象。
简单的说,继承的使用就是为了代码复用。
1.继承
①继承机制:是为了扩展原有类,增加新的功能。
②继承的定义格式:
子类名:继承方式 父类名③继承方式有三种:private(私有继承) protected(保护继承) public(公有继承);
公有继承:基类中公有成员和保护成员在派生类中的访问权限不发生改变,基类中的私有成员在派生中是不可访问的,虽然继承下来了;
保护继承:基类中的公有成员和保护成员在派生类中都将修改为保护成员,基类中的私有成员在派生类中是不可访问的;
私有继承:基类中的公有成员和保护成员在派生类中都将修改为私有成员,基类中的私有成员在派生类中是不可访问的。
2.继承中构造函数和析构函数的调用顺序:
当基类中显式定义构造函数,派生类中也显式定义了构造函数:
构造函数的调用顺序:先调派生类的构造函数,在派生类的初始化列表中调基类的构造函数;析构函数的调用顺序:先调派生类的析构函数(将派生类的析构函数体的内容执行完毕,在将要结束派生类的析构函数时调用基类的析构函数,将基类的析构函数调用完成后会返回到派生类析构函数体的左花括号之前,继而结束派生类的析构函数)。
3、总结:
①基类使用系统合成的构造函数(只有在需要时系统才会合成),这时的派生类也可以使用系统合成的构造函数(也是在需要时才会合成)。
当基类显式定义了缺省的构造函数,这时的派生类可以使用系统合成的(系统会在此时合成派生类的构造函数,在派生类的初始化列表会去调基类的构造函数)。②当基类中定义了非缺省的构造函数(需要参数),这时派生类中必须显式定义构造函数,且在派生类构造函数的初始化列表需要调用基类的构造函数(传参),这里应该很容易明白,因为系统并不知道该给基类的构造函数传什么参数,所以你必须自己进行传参。
③基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。
④基类的私有成员在派生类中是不可访问的,如果基类不想在类外直接访问,但又想在派生类中可以访问时,需要定义为protected。可以看出,protected这个访问权限是为了继承才出现的。
⑤派生类会继承基类中的所有成员,只是访问权限有时会发生改变,导致基类的某些成员在派生类中不可见。
⑥每个类控制他自己的成员初始化过程。
⑦防止继承的发生:c++11新标准提供了一种防止继承发生的方法,即在类名后加上一个关键字final;
⑧使用关键字struct时—>默认公有继承 使用关键字class时—–>默认私有继承。
⑨公有继承是一个接口继承,每个子类对象都可以看成是一个父类对象。
5.同名隐藏:
当派生类中特有的成员和从基类所继承的成员同名(包括成员变量和成员函数)时,在派生类中会隐藏基类中的成员,即使用派生类的对象直接只能访问派生类中的(和基类同名)成员,但是有时候也需要可以访问基类中的成员(同名),这时在派生类的成员函数可以使用形如 : 基类类名::成员名来访问基类中的同名成员。
①这里需注意:当父类和子类成员函数同名时,这时只需关注函数名,与参数列表和函数的返回值都无关;当参数列表不同时,可能会有同学认为它们可以构成重载,切记:它们不能构成重载,可以构成重载函数的首要条件是必须在同一作用域内,这里的父类和子类很明显是两个作用域。
②一般在继承体系中最好不要使用同名成员。
6.赋值兼容规则(public继承):
①子类对象可以赋值给父类对象(子类对象也可以看成一个父类对象);
②父类对象不能赋值给子类对象;
③子类对象的指针/引用不可以指向父类对象。(强制类型转换可以完成)。
7.友元与继承:
友元函数不能继承(因为友元函数不属于类,还记得友元函数不受类中访问权限的限制)。
8.静态成员变量可以继承,而且整个继承体系中只有一份静态成员变量,是所有类对象所共享的。
9.单继承多继承菱形继承*菱形虚拟继承
①单继承:一个子类只有一个直接父类时的这种继承关系为单继承。
②多继承:一个子类有两个或两个以上父类时的这种继承关系成为多继承。
多重继承的派生类继承了所有基类的属性。
③菱形继承(钻石继承)
菱形继承对象模型(对象成员在空间的布局):
定义一个D类的对象的d1,
从上图可以看出,在D类对象中存放了两份_b——>造成二义性(当使用D类对象去直接访问_b时,变量不明确)。
由此:可知菱形继承会造成二义性和数据冗余的问题。
为了解决这个问题,我们引入了虚继承。
④虚继承(为了解决菱形继承造成的二义性和数据冗余等问题)
虚继承的目的是令某个类做出声明,承诺愿意共享它的基类。1
注意:虚继承和一般的继承在调用构造函数时有一点差别,虚拟继承会先将1压栈,然后调用构造函数,而一般的继承直接调用构造函数,可以看出将1压栈只是为了区分虚拟继承和一般的继承。
通过示例分析虚拟继承的底层实现:
#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;
class A
{
public :
int _a;
};
class B:virtual public A
{
public :
int _b;
};
void Test1()
{
B b;
b._a = 1;
b._b = 2;
}
int main()
{
Test1();
return 0;
}
通过汇编剖析虚拟继承是怎样实现的?
通过上图可得在虚拟继承中,在构建派生类的对象时,汇编代码首先会push 1(一般继承中没有),还可以得知:派生类构造函数完成的工作:仅仅是将存放偏移量的表格的地址存放到对象的前4个字节,还可以得到派生类对象的对象模型。
⑤菱形虚拟继承:
菱形虚拟继承对象的模型:
我们先来测试一下菱形虚拟继承的程序,测试一下D类对象需要占用多少字节?
//下面只是代码段(没有加头文件)
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;
};
void Test1()
{
D d1;
cout << sizeof(D) << endl;
d1._c1 = 0;
d1._c2 = 1;
d1._d = 2;
d1._b = 3;
}
int main()
{
Test1();
return 0;
}
补充:
当不小心把菱形虚拟继承记忆为下图时,让我们一起来看会发生什么?
首先会看到不能解决菱形继承的二义性问题,
D类对象模型建立:
由上图可分析出:这时只有一张存放偏移量的表。