继承

       得到钱的最快的方式是什么?答曰:继承!这个网络段子程序员都听过,今天就来学习一下c++中的继承。
什么是继承?
       C++中继承的概念:继承机制是面向对象程序设计使代码可以复用的最重要的手段,他允许程序员在保持原有类的基础之上进行扩展,增强其功能。这样产生新的类称为派生类或子类,被继承的类称为基类或父类。
继承的定义格式:
       与类成员的访问类型相似,继承可分为public、protected和private三种类型,我们先给出继承的定义格式:

Derive-class-name-------派生类(子类)名
Access-label--------继承类型(public、protected、private)
Base-class-name--------基类(父类)列表(可以有多个,即多继承)
继承中成员访问类型的变化:
         各继承类型中成员的访问类型变化(在继承中,基类的私有成员继承到派生类中后始终是不可见的):
public:
           基类的各类型成员的访问类型不变,从基类中继承的私有成员不可见。(public->public、protected->protected)

protected:
           在受保护继承中,基类的公有成员(public)和受保护成员(protected)继承到派生类中都变成受保护成员,基类中的私有成员通过继承在派生类中不可见。(public、protected->protected)

private:
           在私有继承中, 基类的公有成员(public)和受保护成员(protected)继承到派生类中后都变成私有成员(private),基类中的私有成员在派生类中不可见。(public、protected->private)

我们先来证明基类中的私有成员到派生类中后都不可见:
        在如下代码中将基类中的成员声明为私有成员,派生类通过公有继承继承基类的成员,如果基类私有成员在派生类中可见,那么我们就可以通过派生类中的成员函数对其赋值,否者不能赋值。
class Base
{
private:
	int _b;
};
class Derive :public Base
{
public:
	void Test()
	{
		_b = 0;
		_d = 1;
	}
	int _d;
};

我们先屏蔽Test函数,用sizeof对派生类求大小,以证明基类中的私有成员在派生类中存在----如果不存在则大小为4,存在则为8。我们来看结果:
可以看出,基类私有成员在被派生类继承了,我们来看能否通过编译:

显然,代码没有同过编译,经过报错分析,我们可以确定派生类中不能访问从基类继承的私有成员,由此说明,在公有继承中基类私有成员在派生类中不可见,其他继承中也一样。
        我们再来看看在public继承中,public和protected成员的类型变化,先声明如下继承:
class Base
{
public:
	int _b1;
protected:
	int _b2;
};
class Derive :public Base
{
public:
	int _d;
};
在此继承中,如果public成员和protected成员访问类型都变为public,那么可以在类外对其进行访问;如果类型不变,则_b1可以在类外访问,_b2在类外则不能进行访问;如果都变为private,那么_b1和_b2都不能在类外访问。我们用如下代码进行测试:
int main()
{
	Derive d1;
	d1._b1 = 0;
	d1._b2 = 1;

	return 0;
}

我们看看能否通过编译:

没有通过编译,我们再看看仅访问_b1时能否通过编译:

对_b1的访问成功,显然,public继承中,成员的访问类型没有发生变化。
         我们可以用相同的办法对protected继承和pribate继承作类似的检测,在此不做累述。
派生类的对象模型:
          我们知道类的数据成员在内存中根据类内声明的顺序存放,那么在继承体系中派生类的对象模型是怎样的呢?可以确定的是基类个派生类的成员不会交叉排列,我们可以有两种猜测,一种是派生类独有成员在基类成员前,一种是派生类独有成员在基类成员后,先给出如下代码:
class Base
{
public:
	int _b1;
	int _b2;
};
class Derive :public Base
{
public:
	int _d;
};
上面两种猜测可以表示为:
我们通过如下代码对数据成员进行赋值:
int main()
{
	Derive d1;
	d1._d = 0;
	d1._b1 = 1;
	d1._b2 = 2;

	return 0;
}
我们来查看内存布局:

可以看见_b1和_b2位于_d之前。由此我们可以确定派生类的对象模型符合第二种猜测。
       通过上面的探讨,我们可以确定在派生类的对象模型中,从基类中继承的成员在派生类独有的成员之前,即上面的第二种模型。
继承关系中的静态成员:
       我们知道类的静态成员独立于所有对象之外,类的任意实例都可以使用它,那么在继承关系中静态成员具有怎样的性质呢?其实在基类中定义了static成员后整个继承体系中就都只有一个这样的成员,无论派生出多少个类,都只有这一个static实例,这个static实例可以被整个继承体系中所有的对象调用。
       下面我们通过代码来证明static成员的性质,我们给出如下代码,在基类中声明一个static成员,并且在基类和派生类中定义成员函数对其进行一次运算,如果上述性质不成立,那么最后这个static成员的值为1,如果成立则为2,我们来看结果是怎样的呢:
class Base
{
public:
	void Base_Test()
	{
		_b1++;
	}
	static int _b1;
};
int Base::_b1 = 0;
class Derive :public Base
{
public:
	void Derive_Tese()
	{
		_b1++;
	}
};
我们再通过如下代码测试:
int main()
{
	Base b;
	Derive d;
	b.Base_Test();
	d.Derive_Tese();

	return 0;
}
来看结果:

经测试分析,基类的static成员在继承体系中只有一个,且可以被基类对象和派生类对象调用。
继承关系中的构造函数调用:
       构造对象必定涉及到类的构造函数,在继承关系中基类和派生类都有构造函数,那么两者的构造函数的调用顺序是怎样的呢?是先调用基类的构造函数还是先调用派生类的构造函数呢?下面我们通过代码来探讨。
        我们给出如下继承关系:
class Base
{
public:
	Base()
	{
		cout << "Base()" << endl;
	}
	int _b;
};
class Derive :public Base
{
public:
	Derive()
	{
		cout << "Derive()" << endl;
	}
	int _d;
};

如下,先构建一个派生类对象,在构建对象过程中系统调用构造函数,执行构造函数的输出语句,我们就可以知道调用构造函数的先后顺序。
int main()
{
	Derive d;

	return 0;
}
我们来看看输出情况:

先输出Base(),然后输出Derive(),貌似先调用基类的构造函数,再调用派生类的构造函数,为什么会这样调用呢?我们用F11跟踪调用过程,发现程序执行过程与下列图片所示的执行过程一致:
先构造派类对象,然后:

执行派生类的构造函数
在执行派生类的构造函数体之前跳转到基类的构造函数,然后:
执行完基类的构造函数后回到派生类的构造函数中,继续执行派生类的构造函数体中的内容,之后:
之后回到主函数,构建派生类对象成功。
       通过上面的操作我们可以知道,程序在执行时,先进入派生类的构造函数,在初始化阶段跳转到基类的构造函数中,执行完基类的构造函数后回到派生类构造函数体内执行构造函数体内的代码,这就是一个派生类对象构建过程中构造函数的调用过程。
       那么如果在派生类中定义了一个类对象,其调用过程还是不是这样呢?答案是肯定的。我们知道派生类的对象模型是这样的:
在构建派生类对象时会先对派生类成员进行初始化,在此时系统会根据对象模型调用相应的构造函数,因此,整个调用过程为:派生类构造函数->基类构造函数->派生类内类成员构造函数->派生类构造函数体。
       上面我们讨论了构造函数的调用顺序,现在我们来看一下构造函数的申明规则。
1、如果基类满足:没有显式给出构造函数、给出构造函数但没有参数、给出构造函数有参数但有缺省
     值,那么派生类可以不用给出构造函数。
2、如果基类给出的构造函数有参数列表且没有缺省值,那么派生类必须显示给出构造函数,且必须
     在初始化列表中调用基类构造函数且进行传参。

继承关系中析构函数的调用:
     上面讨论了构造函数的调用过程,现在来看派生类的析构函数的调用过程。首先给出下列四个类,其中Base类和Derive类在一个继承体系中,在派生类Derive中申明Else1类和Else2类对象各一个。
class Else1
{
public:
	~Else1()
	{
		cout << "~Else1()" << endl;
	}
	int _es1;
};
class Else2
{
public:
	~Else2()
	{
		cout << "~Else2()" << endl;
	}
	int _es2;
};
class Base
{
public:
	~Base()
	{
		cout << "~Base()" << endl;
	}
	int _b;
};
class Derive :public Base
{
public:
	~Derive()
	{
		cout << "~Derive()" << endl;
	}
	Else1 _e1;
	Else2 _e2;
	int _d;
};
我们创建一个派生类对象,然后来看四个类的执行顺序:
int main()
{
	Derive d;

	return 0;
}
执行顺序如下:

可以到,程序是按Derive->Else2->Else1->Base 的顺序执行的,我们可以用F11调试跟踪此过程。
       通过上述操作我们可以知道,派生类析构函数的调用过程按照  派生类析构函数->类内类成员的析构函数(与定义顺序相反)->基类析构函数  的顺序执行的。
继承体系的作用域与赋值兼容原则:
       在继承体系中的两个类是两个不同的作用域,它们只能调用自己类里面的成员(基类中的静态成员除外),比如基类对象只能调用基类的成员,而不能调用子类特有的成员,因为类的封装特性,派生类也不能调用基类的私有成员。那么如果派生类中的一个成员与基类某个public或protected成员同名,当派生类对象调用这个成员时,到底是调用哪个成员呢?这就涉及到继承关系中同名隐藏问题。
       我们给出如下两个类,
class Base
{
public:
	int _b;
};
class Derive :public Base
{
public:
	int _b;
};
两个类中都声明了public类型的同名成员,我们通过派生类对象对其进行访问:
int main()
{
	Derive d;
	d._b = 1;

	return 0;
}
查看内存,看哪个成员被赋值为1了:
显然在赋值操作中调用的是派生类独有的成员,这就发生了同名隐藏,即咋继承关系中基类中的同名成员被派生类成员覆盖,在发生同名隐藏时访问的是基类成员。在发生同名隐藏时要访问基类成员只需要在基类成员前加上基类名及域操作符:
int main()
{
	Derive d;
	d.Base::_b = 1;
	d._b = 2;

	return 0;
}
此时内存中的值如下:
通过以上操作可知,派生类中来自基类的可见成员与新增成员同名时,发生同名隐藏,在访问时只访问到了派生类新增成员,如果要访问基类继承成员,则在成员前加 基类名:: 便可进行访问。我们在写代码时尽量不要在基类和派生类中定义名字相同的成员。
       继承关系中的赋值兼容规则前提必须是public继承,赋值兼容规则具体指:
1、子类对象可以给基类对象赋值(切割/切片),而基类对象不能给子类对象赋值。
2、基类的指针/引用可以指向子类对象,而子类的指针/引用不能指向基类对象(可通过强制类型转换)。
给出如下继承关系:
class Base
{
public:
	int _b;
};
class Derive :public Base
{
public:
	int _d;
};
我们用子类对象给基类对象赋值:
int main()
{
	Derive d;
	Base b;
	d._b = 1;
	d._d = 2;
	b = d;

	return 0;
}
赋值结果如下:

显然用子类对象给基类对象赋值成功。我们再用基类对象给子类对象进行赋值:
int main()
{
	Derive d;
	Base b;
	b._b = 1;
	d = b;

	return 0;
}
结果如下:
显然基类对象给子类对象的赋值失败。
       因此,子类对象可以给基类对象赋值,而基类对象不能给子类对象赋值。
       我们用基类指针指向子类对象:
int main()
{
	Derive d;
	Base *pb = &d;

	return 0;
}
能否通过编译呢?
显然,基类指针指向子类对象合法。那么子类指针是否可以指向基类对象呢?我们来看:
int main()
{
	Base b;
	Derive *pd = &b;

	return 0;
}
结果如下:
显然子类指针给基类对象赋值不能通过编译。当然,我们可以通过强制类型转换的方式将子类指针指向基类对象,但是这是不安全的做法,一般都不这样做。
        在继承关系中友元能否被继承呢?我们知道友元不是类的成员,而只有类的成员才可以被继承,那么这种友元关系能否继承呢?我们声明如下两个类:
class Base
{
public:
	friend void Test();
	int _b;
};
class Derive :public Base
{
private:
	int _d;
};
void Test()
{
	_d = 1;
}
如果友元关系可以继承,那么Test函数就可以访问Derite类的私有成员,即函数Test可以成功调用,我们来看看能否成功:
调用失败,可见,友元关系不能继承。
单继承&多继承&菱形继承:
        什么是单继承?在前面的例子中,用的继承关系都是单继承。
        什么是多继承?一个派生类有多个基类叫做多继承。(注意:多继承中每个基类前多要声明继承类型)
多继承示例:
class B1
{
public:
	int _b1;
};
class B2
{
public:
	int _b2;
};
class C :public B1, public B2
{
public:
	int _c;
};
          什么是菱形继承?顾名思义,菱形继承就是成菱形的继承方式,由两个单继承和一个多继承构成。
菱形继承示例:
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 _c;
};

虚继承:
       在前面的讨论中,我们知道了派生类的对象模型,那么在菱形继承中,类A分别被类B1和类B2继承,而B1和B2又被类C继承,那么类A的成员在类C中插入了两次,先来看类C的大小,如果A类中的数据在C中只有一个,那么C大小为16个字节,如果有两个则有20个字节,我们看结果如何:

显然在C类中有两个A类的成员。那么在访问时在这种情况下到底访问哪个呢?我们来试一下:
显然在这里产生了二义性,系统不知道到底访问继承自B1中的_a还是B2中_a,因此不能通过编译。
       既然菱形继承有二义性问题,那么怎么解决呢?虚继承就可以解决此问题!
       虚继承是什么?虚继承是面向对象程序设计中为解决多重继承而出现的。虚继承和普通继承有什么不同呢?我们给出如下继承关系:
class Base
{
public:
	int _b;
};
class Derive :virtual public Base
{
public:
	int _d;
};

我们用sizeof对派生类求大小,普通继承中,派生类大小为8个字节,在这里是否也是8个字节呢?


是12个字节,不是8个字节!为什么呢?难道除了继承基类的成员之外还有其他东西?我们创建一个派生类对象,将其成员赋值并对其取地址,观察内存中的分布:
int main()
{
	Derive d;
	d._b = 1;
	d._d = 2;

	return 0;
}
观察内存:


_b为从基类中继承的成员,_d为派生类新增成员,我们在这里看见的内存分布和我们前面得到的对象模型相反,而且在前4个字节中存储着不知名的数据,我们放入的数据都存放在后8个字节中,那么前4个字节中是什么呢?可以肯定得是它一定不是对象的成员数据,那么我们姑且猜测其为一个地址吧,我们来查看这个地址中的数据

前4个字节是0,之后是8,这是否表示什么呢?再回到之前的内存分布中:
可以看到,继承自基类的成员在内存中距派生类对象偏移了8个字节,结合前4个字节地址所指向的空间中所存储的0和8,我们可以推测0是派生类对象的起始地址相对于存放偏移量表地址的空间的偏移值,8是派生类继承自基类的成员相对于存放偏移量表地址空间的偏移值。那么虚继承的对象模型是这样的:


再回到菱形继承中,菱形继承采用虚继承的形式:
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 _c;
};
我们来看看菱形继承中采用虚继承后的内存分布:
因为类C继承自B1和B2属于普通继承中的多继承,因此对象模型是普通的对象模型,而类B1和B2继承A是虚继承,因此C中继承自类A的成员_a在所有成员之后,属于虚继承的对象模型,毫无疑问,继承自B1和B2的成员前4个字节存储的地址所指向的两个偏移表存放着虚继承体系中派生类的起始地址和来自基类的成员的偏移量。
        通过上面的探讨,我们知道,虚继承通过访问偏移表来管理继承关系中的数据,在访问时先访问偏移量表,再访问继承的成员。这样就解决了菱形继承中重复继承产生的二义性和数据冗杂的问题,但是我们也看到了虚继承非常复杂,当数据较多时会带来性能上的问题。因此我们一般不会定义菱形结构的虚继承体系。
        C++的继承所蕴含的知识非常多,希望我们在以后的学习运用中能不断学习所有知识。


  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值