第四十四讲里面我们看到一个奇怪的东西:
—————————————-
1)employ m_em = m_Sup[0];
2)m_em.Show_Employy_Info();
3)employ&
m_em2 = m_Sup[0];
4)m_em2.Show_Employy_Info();
——————————————
这两句代码看上去大意差不多,差别就在于一个是引用而一个不是引用之上,然后结果却相去甚远,大家明白这是为什么吗?
大家在看到结果的时候第一反应一定是这两个看似相同的对象调用了不同的方法(Show_Employy_Info()),想到这里说明已经足够了,那么我们来看这中间到底发生了什么呢?
第一句,看上去不难懂,是将一个Supervisor的对象用来初始化employ对象,这中间发生了强制转换,强制转换有问题吗?我们不都会经常用到强制转换,就好比很多时候我们为了数据的精准而将int转换为double,但是大家有没有为了数据精准而把double转换为int的呢?显然没有,就好比如果将double数据转换为int,那么小数点后面的东西就会被扔掉,同样,如果把派生类转换为基类,也可以是说把子类转换为父类,由于子类是继承了基类的所有方法,所以,子类中的方法只会比基类多不会少,这样以来,大家应该就明白了,同样,这种转换发生了切割,简单点说来,此时m_em虽然是用Supervisor对象来初始化,事实上他彻彻底底的就是employ对象,所以他调用的一切方法都是employ的。
那么,引用的为什么就可以了正确显示呢?这就是关键了,这里和大家说一下,不但引用的可以做到,指针也一样可以做到。所以,当我们使用父类引用或者指针来引用子类时,子类仍然正确保留他的覆盖的方法,简单点,这样只是还了中表述方式而已。
上面这点要记住,下面我们来继续新内容,先看下面的:
————————————-
class parent{
public:
parent()
{ cout<<"
1"
<<endl;
}
}
class something{
public:
comething()
{ cout<<2<<endl;
}
}
class child:public parent{
public:
child()
{ cout<<"
3<<endl;
}
protected:
something Mysometing;
}
int main()
{
child MyChild;
return 0;
}
——————————————
现在大家思考一下,我们这个程序会输出什么?可能有些朋友会问,主函数不过只是构造了一个child的对象而已,并没有什么输出语句,主页君是不是脑子被门夹了?当然,可能有些又会想,这个程序构造了一个child对象,理所应当要调用child的构造函数,所以应该会输出3,这种想法合情合理,当然,厉害的朋友一定看出来了,要构造child对象,首先要调用child的父类的构造函数,所以最开始会输出1,接着在child构造出对象之前会初始化child的相关数据(Mysomething),这时就会调用something的构造函数,于是又输出2,最后才是child的构造函数,所以我们的程序应该输出:
————————-
1
2
3
—————————
嗯,看来确实是123,如果说大家都能够想到这一层,那么关于继承我真没啥好给大家说的了,不过为了照顾一下其他朋友,还是决定说说。
还是上面的例子,我们再把析构函数加上去看看会发生什么:
—————————————-
class parent{
public:
parent()
{ cout<<"
1"
<<endl;
}
~parent()
{cout<<"
1"
<<endl;
}
}
class something{
public:
comething()
{ cout<<"
2"
<<endl;
}
~something()
{ cout<<"
2"
<<endl;
}
}
class child:public parent{
public:
child()
{ cout<<3<<endl;
}
virtrual ~child()
{ cout<<3<<endl;
}
protected:
something Mysometing;
}
int main()
{
child * point = new child();
delete point;
return 0;
}
——————————————-
通过上面的例子,大家应该能够想到会输出什么了。
child * point = new child();
构造对象,所以当程序执行这句代码的时候和上面的例子一样,自然会输出123,但是当程序执行delete point的时候就会逐一调用相应的析构函数,那么从哪里开始呢?当然从child开始,然后再析构相关数据,最后才析构父类。所以我们会看到如下输出:
————————–
1
2
3
3
2
1
————————–
我们不妨再换个思路去思考一个问题,如果我们将上面的析构函数中的virtual去掉,会是什么样的呢?输出还是一样的输出:
——————-
1
2
3
3
2
1
———————
我们再把执行片段修改一下:
——————————
int main()
{
parent * point = new child();
delete point;
return 0;
}
———————————–
上面的class同样没有virtual,这时又会输出什么呢?我们会发现这时输出竟然如下:
——————-
1
2
3
1
——————–
哦,好像哪里不对,怎么会这样呢?我们构造对象的时候调用了三个构造函数,而且我们在堆上构造,所以在回收资源的时候理所应当要将所有的资源回收,也便内存泄漏,然后我们上面这个例子,却只收回了一块内存,这自然就会造成传说中的内存泄漏了,如果用在大程序中,会带来严重的后果,当然如果我们很严谨的在所有析构函数面前加上了virtual的话,输出就会正常,那么现在大家是不是明白了virtual的重要性了呢?
现在我们再来看看另一种形式:
———————————–
class parent{
public:
parent()
{ cout<<"
1"
<<endl;
}
void A()
{ cout<<"
this is parent"
<<endl;
}
~parent()
{cout<<"
1"
<<endl;
}
}
class something{
public:
comething()
{ cout<<"
2"
<<endl;
}
void B()
{ cout<<"
this is something"
<<endl;
}
~something()
{ cout<<"
2"
<<endl;
}
}
class child:public parent,public something{
public:
child()
{ cout<<3<<endl;
}
virtrual ~child()
{ cout<<3<<endl;
}
}
int main()
{
child * point = new child();
point->A();
point->B();
delete point;
return 0;
}
———————————-
这个程序不难看懂,输出如下:
——————————–
1
2
3
this is parent
this is something
3
2
1
———————————-
我们再来变换一下:
———————————-
class parent{
public:
parent()
{ cout<<"
1"
<<endl;
}
void A()
{ cout<<"
this is parent"
<<endl;
}
~parent()
{cout<<"
1"
<<endl;
}
}
class something{
public:
comething()
{ cout<<"
2"
<<endl;
}
void A()
{ cout<<"
this is something"
<<endl;
}
~something()
{ cout<<"
2"
<<endl;
}
}
class child:public parent,public something{
public:
child()
{ cout<<3<<endl;
virtrual ~child()
{ cout<<3<<endl;
}
}
int main()
{
child * point = new child();
point->A();
delete point;
return 0;
}
——————————
这段程序会编译不过,因为出现时方法名字二义性,为什么呢?因为我们在parent里面定义了方法A,在something里面也定义了一个方法A,而这两个类都是child的父类,当child调用A方法的时候编译器却蒙了,到底要使用那个呢?或许我们继承something其实只是想要调用他的A方法,所以我们可以这样来解决:
——————————-
point->something::A();
//调用something的
point->parent::A();
//调用parent的
——————————–
当然,如同上面我们所说的,我们继承something其实只想调用他重用A方法,所以我们不要这么麻烦,我们可以这样重写这个方法:
————————————–
class parent{
//
};
class something{
//
};
class child:public parent,public something
{
//
virtural
void A()
{ something::A();
}
};
—————————————-
这样就可以完美解决我们上面的问题了。那么大家是不是会想怎么会有这样现象呢?就比如,小鸟,猫,猫头鹰
,他们都同属于动物,他们都吃东西,都睡觉,猫头鹰和猫一样都会吃老鼠,而小鸟不会
,但是猫头鹰和小鸟又属于鸟一类,他们的作息方式应该差不多(我也只是猜测,反正这里只是给大家作为例子来说的)。
现在,我们看到,猫头鹰不但具有小鸟的属性还同时具有猫的一些特性,所以我们可以让他继承猫和小鸟。我们应该怎样来实现呢?
——————————
class 动物{
public:
virtual
吃()=0;
virtual
睡()=0;
};
class 小鸟:public 动物{
public:
virtual 吃()
{ 实现; }
virtual 睡()
{ 实现; }
};
//纯虚寒可以实现,但我一般推荐不必要在虚基类里实现,他的实现留待派生类中会更好一些。
class 猫:public 动物{
public:
virtual 吃()
{ 实现; }
virtual 睡()
{ 实现; }
};
class 猫头鹰:public 猫,public 小鸟
{
public:
virtual 吃()
{ 猫::吃(); }
virtual 睡()
{ 鸟::睡(); }
///
}
;
——————————–
这样以来,就各取所需,什么都不用写,直接调用就好,不过,大家应该注意到了我们的动物这个class,里面全部是纯虚函数,哦,对了,什么是纯虚函数呢?就是在声明虚函数的同时让他等于0,这样一来,拥有纯虚函数的类就成了抽象类,抽象类天生就是作为基类的,就是为了解决名字二义性问题的,所以他不能像普通的类一样,他不能定义对象,他所定义的方法都是在派生类中实现,根据不同的要求来实现。
关于继续,我们说了很多,第四十三 四十四讲我们主要通过例子进入继续,第四十五讲我们讲了关于继承的
很多知识,今天的例子都极为简单,但都极为经典,正所谓是麻雀虽小五脏俱全,大家只要将这些短小的例子全都弄懂了,那么关于继承也就是手到擒来了。
======================
回复D直接查看目录
原文始发于微信公众号(
C/C++的编程教室
):第四十五讲 继承(3)
|