何为继承?
何为继承?在现实生活中继承这个词很常见。继承意志,继承父母留给我们的财产。继承在我们生活中很常见。在C++中继承是其一大特性。
面向对象程序设计(OOP)的核心思想是数据抽象、继承和动态绑定。
继承(inheritance)是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持 原有类特性的基础上进行扩展,增加功能。这样产生新的类,称派生类。继承呈现了面向对象程序设 计的层次结构,体现了由简单到复杂的认知过程。
继承应用
1 继承的基本书写格式
2 继承的三种方式
(1)public — 基类中的各自成员是什么访问权限,派生类继承下来就是什么访问权限。
#include<iostream>
using namespace std;
class Base
{
private:
int _pri;
protected:
int _pro;
public:
int _pub;
};
class Dervied :public Base
{
public:
Dervied()
{
_pro = 1;
}
public:
int _pubD;
};
int main()
{
Dervied d;
d._pub = 1;
d._pubD = 2;
return 0;
}
我们可以通过上述代码可以看出,在类外protected的是无法访问的,但是在我们可以在派生类中调出,从此也可以看出public的继承并没有改变继承下来变量的访问权限。(protected:(1)可以在该类中访问(2)友元函数可以访问)
(2)protected — 它将基类非private的访问权限都变成protected。
class Dervied :protected Base
{
public:
Dervied()
{
_pro = 2;
}
public:
int _d;
};
class Dervied :protected Base
{
public:
Dervied()
{
_pro = 2;
}
public:
int _d;
};
我们将继承方式变为protected,可以看出原先基类的公有成员将无法在类外访问,只能在派生类中访问。
(3)private — 它将基类非private的访问权限都变成private。
当我们把继承方式改为private时,在派生类内部继承下来的基类的非私有成员权限都变为private,这些成员只能在派生类中使用。那当我们再写一个派生类的子类时最早Base类所继承的所有成员在这个新的子类中都无法访问,这也是private的继承与protected继承的不同。
(4)总结
1
这三种继承方式,基类的公有和保护成员在派生类中都可以访问,而基类的私有成员继承时在派生类中都被隐藏起来,无论派生类内外都无法访问。
2 Class 默认私有继承,struct默认公有继承。
3当我们想把基类的成员只能在派生类中访问,例如我们只在派生类中做接口不想暴露基类函数的具体实现我们可以使用protected来继承,这样我们可以定义更多的子类根据需求不同写出不同接口的子类。
3 派生类的默认函数
当我们定义一个派生类的对象时,它是先调基类的构造函数还是派生类自己的呢?
那我们可以看看从汇编角度定义一个派生类底层都发生了什么:
#include<iostream>
using namespace std;
class Base
{
public:
Base()
{
cout << "Base" << endl;
}
};
class Dervied :public Base
{
public:
Dervied()
{
cout << "Dervied" << endl;
}
};
int main()
{
Dervied d;
return 0;
}
int main() { 00E469F0 push ebp 00E469F1 mov ebp,esp 00E469F3 sub esp,0DCh 00E469F9 push ebx 00E469FA push esi 00E469FB push edi 00E469FC lea edi,[ebp-0DCh] 00E46A02 mov ecx,37h 00E46A07 mov eax,0CCCCCCCCh 00E46A0C rep stos dword ptr es:[edi] 00E46A0E mov eax,dword ptr ds:[00E4F000h] 00E46A13 xor eax,ebp 00E46A15 mov dword ptr [ebp-4],eax Dervied d; 00E46A18 lea ecx,[d] 00E46A1B call Dervied::Dervied (0E413ACh) return 0; 00E46A20 mov dword ptr [ebp-0D8h],0 return 0; 00E46A2A lea ecx,[d] 00E46A2D call Dervied::~Dervied (0E414ABh) 00E46A32 mov eax,dword ptr [ebp-0D8h] }
class Dervied :public Base
{
public:
Dervied()
.
. //省略编译器调节栈帧
.
010237E6 call Base::Base (0102121Ch)
{
cout << "Dervied" << endl;
.
.
.
0102380D cmp esi,esp
0102380F call __RTC_CheckEsp (0102132Fh)
}
class Base { public: Base() {
cout << "Base" << endl;. . //省略编译器调节栈帧 .
01023787 call __RTC_CheckEsp (0102132Fh) }. . //省略编译器调节栈帧 .
~Dervied() { 00283E60 push ebp 00283E61 mov ebp,esp .
. 省略编译器对栈帧结构的调节
.
cout << "~Dervied" << endl; 00283EA9 mov esi,esp 00283EAB push 2813EDh 00283EB0 push 28DC8Ch 00283EB5 mov eax,dword ptr ds:[0029109Ch] 00283EBA push eax 00283EBB call std::operator<<<std::char_traits<char> > (02812B2h) 00283EC0 add esp,8 00283EC3 mov ecx,eax 00283EC5 call dword ptr ds:[291090h] 00283ECB cmp esi,esp 00283ECD call __RTC_CheckEsp (0281339h) } 00283ED2 mov dword ptr [ebp-4],0FFFFFFFFh 00283ED9 mov ecx,dword ptr [this] 00283EDC call Base::~Base (0281078h) .
. 恢复栈帧结构
. 00283EFF ret
从上面的汇编代码中我们可以看到在汇编中:
(1)Dervied构造函数 —> Base构造函数—>构造Base对象执行Base函数内容—>构造Der对象执行Dervied函数内容。(故为先构造出基类对象,再构造派生类对象)
(2)Dervied析构函数—>执行Dervied构造函数内容析构Dervied对象—>调用Base析构函数—>执行Base构造函数内容析构Base对象。
总结
(1)如上可看出,实际上派生类的构造函数会发生扩充,其扩充规则为把所有基类的构造函数扩充在派生类的构造函数中,其扩充的排列顺序为继承时的声明顺序,并且以类名::构造函数的方式调用。
(2)当基类中的构造函带有参数(非缺省参数)时,派生类必须要有构造函数并且在初始化列表内对其初始化。
(3)由此汇编还可以得出,当一个类执行构造函数时都是先执行初始化列表中的代码,把初始化列表中的代码执行完了,才执行构造函数主体。
4 派生类对象模型
#include<iostream>
using namespace std;
class Base
{
public:
Base()
{
_pri = 1;
}
private:
int _pri;
protected:
int _pro;
public:
int pcb;
};
class Dervied :public Base
{
public:
Dervied()
{
_pro = 2;
}
public:
int _d;
};
int main()
{
Dervied d;
d.pcb = 3;
d._d = 4;
cout << sizeof(d) << endl;
return 0;
}
如果你从这篇文章的头一直读到了这里,那么通过前面我们所讲派生类的构造函数调用次序的时候可以大致猜出来派生类对象是如何在内存中存储的。那这点你可以跳过去。
那我们通过调用内存窗口取对象d的地址可以看出来,派生类对象的存储模型是由地址低到高,先是基类对象模型,然后才是派生类对象模型。
继承进阶
1 虚拟继承
图 1 图2 图3
前面我们只讲了单继承,那么在多继承的应用中。当被继承的多个基类中有同名变量,在派生类中那么这个变量无法直接访问,报错为有二义性。我们可以1通过类名加作用域限定符解决。2在多继承中有个特殊的继承叫做菱形继承。让它在构造对象时产生虚表指针以此来解决二义性问题。
class I{
public:
I()
:a(8)
{}
int a;
int b;
};
class is : public virtual I
{
public:
is()
:_b(2)
{}
int _b;
};
class os : public virtual I
{
public:
os()
:_c(3)
{}
int _c;
};
class Ios :public is, public os
{
public:
Ios()
:_d(4)
{}
int _d;
};
如上图这个模型我们这些这样处理即可解决二义性问题。
这个模型在内存中的真实样子是这样的:
我们访问虚基表后,可以看到虚基表中记录了,虚基表指针和公共被继承基类的偏移量的值,就是通过这个偏移量来解决二义性问题的。这个内存中的图就是我图2的图,每一个使用虚拟继承的类都多了一个虚基表指针。
综上虚拟继承,一解决了二义性,二解决了数据冗余。
2 虚拟继承与多继承的对象模型
(1)对于产生二义性时,我们通过作用域限定符解决没有用虚拟继承时,多继承的存储结构如图2。
(2)当我们使用虚拟继承时,应使用图1这种方式。那么它的储存空间如图3所示。
(3)使用虚拟继承时,每一个使用虚拟继承的类会多增加4个字节,因为创建了虚基表指针。那么当一个空类虚拟继承另一个空类时,
这个空类大小不再是1而是4。
3 继承中的友元和静态变量
(1) 友元关系是不能继承的。
(2) 在整个继承体系中对于基类中static的变量,无论派生出多少个派生类,有且整个体系公用一个。
4 继承中的同名隐藏
(1) 当父类和子类出现同名变量时,赋值时隐藏父类变量。
(2) 当父类和子类出现同名函数时,无论参数列表是否相同,隐藏父类成员函数。
5 继承中的赋值问题
如图所表示,子类包含了父类。故是父类集合中的内容一定是子集合中的,相反子集合中内容不一定是父集合的。
那么可以得出:
(1) 子类可以赋值给父类,相反不行。
(2) 父类可以指向或引用子类,反之不行。
6 继承体系下的虚函数
所谓Base类指针可以引用派生类对象,其实在底层下由编译器把this中的地址向下调整为相应子类对象的地址。这也就是为什么我们可以用Base指针去指向一个派生类对象。
那么在多重继承下,就有个问题。即调用函数时,this指针需要调整为相应类对象的地址,然后调用。(为什么调整呢,因为在发生赋值兼容规则赋值的时候,实际上原先指向派生类对象的指针会移动至相应基类处。然后我们就可以把一个派生类对象当成一个基类对象使用了)