您的位置:首页 > 其它

对于多态性的学习总结---虚函数的有与没有

2006-03-11 18:29 253 查看
对于多态性的学习总结

我不想在这里讲整个关于C++中的多态性机制,那些资料你可以在网上搜索到一大堆。我想着重讲一下我对于多态性中的动态多态性的一些研究总结。
还是得先大致地说说多态性这个东西。
所谓多态性,简单地说就是一个名称,但是具有多种语义。多态性的发明有什么意义呢?考虑一下,假如现在你要实现一个打印不同信息的功能。情况有很多种,程序必须去根据不同的情况打印不同的信息。于是你用 switch来根据不同的情况来打印不同的信息。但是这样做太过时了,意识到没有,它一点扩展性都没有,一但需要加入新打印的信息,你就不得不去修改代码----而不是扩展代码。采用多态性就不同了,利用多态性,你可以把一大堆的case语句替换为一个Display函数。这就是它的神奇功能。
多态性分为静态多态性和动态多态性。3
函数重载可以实现静态多态性,而要实现动态多态性就可以用----虚函数(virtual function),这也牵扯到继承。
好了,这些死板的介绍就到这里,下面着重看一下虚函数在多态性中的种种表现。
以下继承论述的都是public继承。
先看,当我们不用虚函数时,在一个继承层次中,《深入浅出MFC》中C++部分为我们总结的:
1. 如果以一个基础类别之指标指向一个衍生类别之物件,那么经由此指标,你就只能呼叫基础类别(而不是衍生类别)所定义的函数。

2. 如果你以一个衍生类别之指针,指向一个基础类别之物件,你必须先做明显的转型动作(explicit cast),这种作法很危险,不符合真实生活经验,在程式设计上也会带给程式员困惑。
3. 如果基础类别和衍生类别都定义了相同名称之成员函数,那么透过物件指标呼叫成员函数时,到底呼叫哪一个函数,必须视该指标的原始型别而定,而不是视指标之实际所指之物件的型别而定。

于是我们得到代码:
#include <iostream>

using namespace std;

class CBase
{
public:
void Display()
{
cout << "CBase::Display was called" << endl;
}

};

class CDerive:public CBase
{
public:
void Display()
{
cout << "CDerive::Display was called"<< endl;
}
void Display2()
{
cout << "CDerive::Display2 was called" << endl;
}
};

int main()
{
CDerive d;
CBase a;

CBase *p=&d;
p->Display(); //result:CBase::Display was called
//p->Display2(); // error C2039: 'Display2' : is not a member of 'CBase'

//CDerive *p2=&a; // error C2440: 'initializing' :.....不能自动转换
CDerive *p2=(CDerive*)&a;
p2->Display(); //result:CDerive::Display was called
p2->Display2(); //correct

a=d; //correct
//a.Display2(); //error C2039: 'Display2' : is not a member of 'CBase'
//d=a; //error

return 0;
}
以上代码执行结果:
CBase::Display was called
CDerive::Display was called
CDerive::Display2 was called
代码基本上反映了上面三点,那么为什么基类指针能够名正言顺地指向其派生类对象呢(甚至可以把一个派生类对象赋值给一个基类对象)?原因:派生类以public继承基类时,由于包含了基类所有的元素(private除外),所有的派生类对象同时也是基类对象。(为什么是这样的,我也不知道,高手指教!)。另一方面,基类对象就不能赋值给派生类对象,但是如果是指针的话,我们可以强制转换。那为什么说“不符合真实生活经验,在程式设计上也会带给程式员困惑。”呢?正如侯先生论述的,物件导向观念是描绘现实世界用的。水果类经过添加各种特征(属性或方法)派生出苹果类。我们可以说苹果是水果,但是却不能说水果就是苹果的。(细细品位一下,面向对象真的是一种对思维的洗礼!)
看这一句://a.Display2(); //error C2039: 'Display2' : is not a member of 'CBase'
这里就涉及到切割(object slicing),
看这段话你就会明白为什么它会出错:“衍生物件通常都比基类物件大(记忆体空间),因为衍生物件不但继承其基础类别的成员,又有自己的成员。那么所谓的upcasting(向上强制转型)将会造成物件的内容被切割(object slicing)。”
在这里我要提取出第三点,先放在这里:如果基础类别和衍生类别都定义了相同名称之成员函数,那么透过物件指标呼叫成员函数时,到底呼叫哪一个函数,必须视该指标的原始型别而定,而不是视指标之实际所指之物件的型别而定。
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
接下来让我们看,当虚拟函数参合进来时,那些指针啊对象啊之类会出现什么情况。

OK~~改动不需要太多,你只需要把上文中的那些代码复制到你的编辑器中,再在Cbase类的void Display()前加上virtual 即可,一个注释一个注释地去掉看看编译器给你的结果,呵呵,一样的。
OK,现在把注释放回去,值的我们看的结果来了:
CDerive::Display was called
CBase::Display was called
CDerive::Display2 was called

仔细品位一下~~联合上面我单独提出来的侯先生总结的第三点,我给出我的总结:
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
当在基类中不使用virtual声明会在其派生类中出现同名的函数时,如果基础类别和衍生类别都定义了相同名称之成员函数,那么透过物件指标呼叫成员函数时,到底呼叫哪一个函数,必须视该指标的原始型别而定,而不是视指标之实际所指之物件的型别而定;
当在基类中使用virtual声明会在其派生类中出现同名的函数时,如果基础类别和衍生类别都定义了相同名称之成员函数(且基类中的该函数被定义为virtual),那么透过物件指标呼叫成员函数时,到底呼叫哪一个函数,必须视该指标之实际所指之物件的型别而定,而不是视指标的原始型别而定!(当然,该指标必须是基类类型的指标---pointer也就是指针)

OK了~差不多写到这里就把我的主要目的给表达出来了。文章写出来,自己的思路也清晰了很多~大家看了后觉得不对的还希望能给我指正出来,批评的表扬的都说出来哈~~转载的话请注明作者的信息!欢迎大家给我发邮件交流!
Kevin Lynx
zmhn320@163.com
2006.3.11
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: