浅谈C++的继承与多态

jackxiang 2008-7-15 13:03 | |
前段时间有人问关于C++的继承与多态的问题,当时一边调试一边讲解也算是解释通了,但后来又有朋友问起此问题,便想写点这方面的东西,问题是这样的:
#include<iostream.h>
class base
{
public:
func()
{
cout<<”this is base class”<<endl;
}
};
class x: public base
{
public:
func()
{
cout<<”this is x class”<<endl;
}
};
void main()
{
base* p=new x;
p->func();
}
就是这样一个程序,得到的结果是什么呢?答案是:”this is base class”,而问题就在于这一句话:base* p=new x,程序的本意可能是要创建一个x 的对象,但为什么执行的是base的func呢?我们先来看一下new用法,new的作用在堆里为象分配内存并为这块内存调用构造函数,并且内置了长度计算,类型转换和安全检查,通用的格式是Type *p=new Type,而在程序中使用的是base* p=new x,这样就产生了第一个问题,p到底是指向base还是指向x,按照通用的格式应该指向Type,但到底是前面的Type还是后面的Type呢?于是我便把base* p=new x 改成了x* p=new base结果出错,编译器提示不能把类型base转变成x,看来当前一个Type和后一个Type的型类不一致是会发生类型转换,正好new的内置功能里带用类型转换,不难看出这里是把第二个Type转换成第一个Type,但为什么当把base转换成x时会出误错呢?这里就引出了个上向类型转换的概念,所谓上向类型转换就是取一个对象的地址,并将其作为基类的地址一处理,也就是说只能由子类向父类转换.综合上面的解释,程序中实际上是创建了一个base类型的对象,为了证明的我结论是正确的,我又作了以下调式,在base类中添加一个函数a,在x中添加了一函数b,因为x是继承base.所以如果p是x的话那么它可以调用父类的函数a,也可以调用x类的函数b,反之则只能调到base类的函数a,而不能调用x的函数b,调式发现,结果和我想像的是相符合的,也证明了以上的结论.
其实到这里只是完成了整个问题的第一步,于是继续修改程序:
我把程序改成:
#include<iostream.h>
class base
{
public:
virtual func()
{
cout<<”this is base class”<<endl;
}
};
class x: public base
{
public:
func()
{
cout<<”this is x class”<<endl;
}
};
void main()
{
base* p=new x;
p->func();
}
这种里的修改是把func改成了虚函数,再执行,结果是:”this is x class”,这个结果似乎有点出乎意料,似乎和我第一步的结论相反,但第一步是论证是比较严谨的,于是我把注意力集中到了虚函数上.要找到本质原因,还得从虚函数的现实讲起,我们来看一下虚函数是怎样实现的,
把程序改成:
#include<iostream.h>
class base
{
public:
virtual func()
{
cout<<”this is base class”<<endl;
}
};
class x: public base
{
public:
func()
{
cout<<”this is x class”<<endl;
}
};
void main()
{
x* p=new x;
p->func();
}
注意,这里主要改的是x* p=new x,建一个x对象,则由于func函数是一个虚函数,所在这里应该调用的是x的func而当func不是虚函数时,则调用base的func函数,虚函数和普通函数到底有什么区别?编译器又是怎么处理的?我们来作一个测式,写如下程序:
#include <iostream.h>
class X{
int i;
public:
a()
{
}
};
class Y{
int i;
public:
virtual a()
{
}
};
class Z{
int i;
public:
virtual a()
{
}
virtual b()
{
}
};
void main()
{
cout<<sizeof(X)<<endl;
cout<<sizeof(Y)<<endl;
cout<<sizeof(Z)<<endl;
}
这样我们可以得到三个类的大小,分别是:4,8,8类X与类Y的构构基本相同但Y的大小是X的两倍,而X与Y的不同点就是Y的a是一个虚函数,而Y与Z的大小是相同的,但Z有两个虚函数,而Y只是一个虚函数,得出这样的结果的原因是因为不带虚函数的对象的长度是单个int的长度,而带有单个虚函数的对象长度是不带虚函数的长度再加上一个指针的长度,实际是编译器在Y中插入了一个指针(VPTR),所以带一个虚函数和带两个虚函数没有区别,都只插入了一个指针,而这个指针指向一个存放函数地址的表(虚函数据表),这个表存放着具体函数。我们用函数图来画出函数的结构:


难点就在于虚函数表的内容是依据类中的虚函数声明次序,一一填入函数指针,子类会继承父类的虚函数表,当我们在改写子类时,虚函数表就受了影响:表中元素指的函数地址不再是父类的函数地址,而是子类的函数地址。所以当我们创建一个X对像的时候,编译器能知道执行的是X的func
我们再来看:base* p=new X
前面我们讲了这里是按照向上类型转换的原则把X转换成了base,而此时baser的VPTR指针会指向X的虚函数表,所以,p->func()会执行X的func函数.

作者:jackxiang@向东博客 专注WEB应用 构架之美 --- 构架之美,在于尽态极妍 | 应用之美,在于药到病除
地址:https://jackxiang.com/post/1140/
版权所有。转载时必须以链接形式注明作者和原始出处及本声明!

评论列表
发表评论

昵称

网址

电邮

打开HTML 打开UBB 打开表情 隐藏 记住我 [登入] [注册]