C++多态的实现

林小竹 2015-05-13 07:13:56
http://www.cppblog.com/franksunny/archive/2010/09/20/50424.html

1、 多态是如何实现绑定的

多态的绑定可以分为运行是多态和编译时多态

编译时的多态性

编译时的多态性是通过重载来实现的。对于非虚的成员来说,系统在编译时,根据传递的参数、返回的类型等信息决定实现何种操作。

运行时的多态性

运行时的多态性就是指直到系统运行时,才根据实际情况决定实现何种操作。C#中,运行时的多态性通过虚成员实现。

编译时的多态性为我们提供了运行速度快的特点,而运行时的多态性则带来了高度灵活和抽象的特点。

今天才正式弄清楚原来虚函数是可以实现运行时多态的,以前只知道虚函数可以使得基类对象的的方法调用派生类的方法。

2、 析构函数是虚函数的优点是什么

用C++开发的时候,用来做基类的类的析构函数一般都是虚函数。可是,为什么要这样做呢?下面用一个小例子来说明:

有下面的两个类:

class ClxBase

{

public:

ClxBase() {};

virtual ~ClxBase() {};



virtual void DoSomething() { cout << "Do something in class ClxBase!" << endl; };

};



class ClxDerived : public ClxBase

{

public:

ClxDerived() {};

~ClxDerived() { cout << "Output from the destructor of class ClxDerived!" << endl; };



void DoSomething() { cout << "Do something in class ClxDerived!" << endl; };

};



代码



ClxBase *pTest = new ClxDerived;

pTest->DoSomething();

delete pTest;



输出结果是:



Do something in class ClxDerived!

Output from the destructor of class ClxDerived!



这个很简单,非常好理解。

但是,如果把类ClxBase析构函数前的virtual去掉,那输出结果就是下面的样子了:

Do something in class ClxDerived!

也就是说,类ClxDerived的析构函数根本没有被调用!一般情况下类的析构函数里面都是释放内存资源,而析构函数不被调用的话就会造成内存泄漏。我想所有的C++程序员都知道这样的危险性。当然,如果在析构函数中做了其他工作的话,那你的所有努力也都是白费力气。

所以,文章开头的那个问题的答案就是--这样做是为了当用一个基类的指针删除一个派生类的对象时,派生类的析构函数会被调用。
当然,并不是要把所有类的析构函数都写成虚函数。因为当类里面有虚函数的时候,编译器会给类添加一个虚函数表,里面来存放虚函数指针,这样就会增加类的存储空间。所以,只有当一个类被用来作为基类的时候,才把析构函数写成虚函数。


说实话,这个也是今天才深刻认识到的。



当然还问到很多数据结构和算法方面(空间复杂度和时间复杂度之类的东东,说真的也是基础性的)的问题,至于那些东西,自己说实话抛开没用他们已经很长时间了,真可以说忘的差不多了,考这种真的很怕,也怪平时没怎么用到。不知道大家用的多不?

好久没有正式参加过面试了,今天突然来一次觉得自己基础还是不够扎实。





http://leowzy.javaeye.com/blog/720949



多态(Polymorphism)是面向对象的核心概念,本文以C++为例,讨论多态的具体实现。C++中多态可以分为基于继承和虚函数的动态多态以及基于模板的静态多态,如果没有特别指明,本文中出现的多态都是指前者,也就是基于继承和虚函数的动态多态。至于什么是多态,在面向对象中如何使用多态,使用多态的好处等等问题,如果大家感兴趣的话,可以找本面向对象的书来看看。
为了方便说明,下面举一个简单的使用多态的例子(From [1] ):

class Shape
{
protected:
int m_x; // X coordinate
int m_y; // Y coordinate
public:
// Pure virtual function for drawing
virtual void Draw() = 0;

// A regular virtual function
virtual void MoveTo(int newX, int newY);

// Regular method, not overridable.
void Erase();

// Constructor for Shape
Shape(int x, int y);

// Virtual destructor for Shape
virtual ~Shape();
};

// Circle class declaration
class Circle : public Shape
{
private:
int m_radius; // Radius of the circle
public:
// Override to draw a circle
virtual void Draw();

// Constructor for Circle
Circle(int x, int y, int radius);

// Destructor for Circle
virtual ~Circle();
};

// Shape constructor implementation
Shape::Shape(int x, int y)
{
m_x = x;
m_y = y;
}
// Shape destructor implementation
Shape::~Shape()
{
//...
}

// Circle constructor implementation
Circle::Circle(int x, int y, int radius) : Shape (x, y)
{
m_radius = radius;
}

// Circle destructor implementation
Circle::~Circle()
{
//...
}

// Circle override of the pure virtual Draw method.
void Circle::Draw()
{
glib_draw_circle(m_x, m_y, m_radius);
}


main()
{
// Define a circle with a center at (50,100) and a radius of 25
Shape *pShape = new Circle(50, 100, 25);

// Define a circle with a center at (5,5) and a radius of 2
Circle aCircle(5,5, 2);

// Various operations on a Circle via a Shape pointer
//Polymorphism
pShape->Draw();
pShape->MoveTo(100, 100);

pShape->Erase();
delete pShape;

// Invoking the Draw method directly
aCircle.Draw();
}

例子中使用到多态的代码以黑体标出了,它们一个很明显的特征就是通过一个基类的指针(或者引用)来调用不同子类的方法。
那么,现在的问题是,这个功能是怎样实现的呢?我们可以先来大概猜测一下:对于一般的方法调用,到了汇编代码这一层次的时候,一般都是使用 Call funcaddr 这样的指令进行调用,其中funcaddr是要调用函数的地址。按理来说,当我使用指针pShape来调用Draw的时候,编译器应该将Shape::Draw的地址赋给funcaddr,然后Call 指令就可以直接调用Shape::Draw了,这就跟用pShape来调用Shape::Erase一样。但是,运行结果却告诉我们,编译器赋给funcaddr的值却是Circle::Drawde的值。这就说明,编译器在对待Draw方法和Erase方法时使用了双重标准。那么究竟是谁有这么大的法力,使编译器这个铁面无私的判官都要另眼相看呢?virtual!!
Clever!!正是virtual这个关键字一手导演了这一出“乾坤大挪移”的好戏。说道这里,我们先要明确两个概念:静态绑定和动态绑定。
1、静态绑定(static bingding),也叫早期绑定,简单来说就是编译器在编译期间就明确知道所要调用的方法,并将该方法的地址赋给了Call指令的funcaddr。因此,运行期间直接使用Call指令就可调用到相应的方法。
2、动态绑定(dynamic binding),也叫晚期绑定,与静态绑定不同,在编译期间,编译器并不能明确知道究竟要调用的是哪一个方法,而这,要知道运行期间使用的具体是哪个对象才能决定。
好了,有了这两个概念以后,我们就可以说,virtual的作用就是告诉编译器:我要进行动态绑定!编译器当然会尊重你的意见,而且为了完成你这个要求,编译器还要做很多的事情:编译器自动在声明了virtual方法的类中插入一个指针vptr和一个数据结构VTable(vptr用以指向VTable;VTable是一个指针数组,里面存放着函数的地址),并保证二者遵守下面的规则:
1、VTable中只能存放声明为virtual的方法,其它方法不能存放在里面。在上面的例子中,Shape的VTable中就只有Draw,MoveTo和~Shape。方法Erase的地址并不能存放在VTable中。此外,如果方法是纯虚函数,如 Draw,那么同样要在VTable中保留相应的位置,但是由于纯虚函数没有函数体,因此该位置中并不存放Draw的地址,而是可以选择存放一个出错处理的函数的地址,当该位置被意外调用时,可以用出错函数进行相应的处理。
2、派生类的VTalbe中记录的从基类中继承下来的虚函数地址的索引号必须跟该虚函数在基类VTable中的索引号保持一致。如在上例中,如果在Shape的VTalbe中,Draw为 1 号, MoveTo 2 号,~Shape为 3 号,那么,不管这些方法在Circle中是按照什么顺序定义的,Circle的VTable中都必须保证Draw为 1 号,MoveTo为 2号。至于 3号,这里是~Circle。为什么不是~Shape啊?嘿嘿,忘啦,析构函数不会继承的。
3、vptr是由编译器自动插入生成的,因此编译器必须负责为其进行初始化。初始化的时间选在对象创建时,而地点就在构造函数中。因此,编译器必须保证每个类至少有一个构造函数,若没有,自动为其生成一个默认构造函数。
4、vptr通常放在对象的起始处,也就是Addr(obj) == Addr(obj.vptr)。
你看,天下果然没有免费的午餐,为了实现动态绑定,编译器要为我们默默干了这么多的脏话累活。如果你想体验一下编译器的辛劳,那么可以尝试用C语言模拟一下上面的行为,【1】中就有这么一个例子。好了,现在万事具备,只欠东风了。编译,连接,载入,GO!当程序执行到 pShape->Draw()的时候,上面的设施也开始起作用了。。
前面已经提到,晚期绑定时之所以不能确定调用哪个函数,是因为具体的对象不确定。好了,当运行到pShape->Draw()时,对象出来了,它由pShape指针标出。我们找到这个对象后,就可以找到它里面的vptr(在对象的起始处),有了vptr后,我们就找到了VTable,调用的函数就在眼前了。。等等,VTable中方法那么多,我究竟使用哪个呢?不用着急,编译器早已为我们做好了记录:编译器在创建VTable时,已经为每个virtual函数安排好了座次,并且把这个索引号记录了下来。因此,当编译器解析到pShape->Draw()的时候,它已经悄悄的将函数的名字用索引号来代替了。这时候,我们通过这个索引号就可以在VTable中得到一个函数地址,Call it!
在这里,我们就体会到为什么会有第二条规定了,通常,我们都是用基类的指针来引用派生类的对象,但是不管具体对象是哪个派生类的,我们都可以使用相同的索引号来取得对应的函数实现。
现实中有一个例子其实跟这个蛮像的:报警电话有110,119,120(VTable中不同的方法)。不同地方的人拨打不同的号码所产生的结果都是不一样的。譬如,在三环外的一个人(具体对象)跟一环内的一个人(另外一个具体对象)打119,最后调用的消防队肯定是不一样的,这就是多态了。这是怎么实现的呢,每个人都知道一个报警中心(VTable,里面有三个方法 110,119,120)。如果三环外的一个人需要火警抢险(一个具体对象)时,它就拨打119,但是他肯定不知道最后是哪一个消防队会出现的。这得有报警中心来决定,报警中心通过这个具体对象(例子中就是具体位置了)以及他说拨打的电话号码(可以理解成索引号),报警中心可以确定应该调度哪一个消防队进行抢险(不同的动作)。
这样,通过vptr和VTable的帮助,我们就实现了C++的动态绑定。当然,这仅仅是单继承时的情况,多重继承的处理要相对复杂一点,下面简要说一下最简单的多重继承的情况,至于虚继承的情况,有兴趣的朋友可以看看 Lippman的《Inside the C++ Object Model》,这里暂时就不展开了。(主要是自己还没搞清楚,况且现在多重继承都不怎么使用了,虚继承应用的机会就更少了)
首先,我要先说一下多重继承下对象的内存布局,也就是说该对象是如何存放本身的数据的。

class Cute
{
public:
int i;
virtual void cute(){ cout<<"Cute cute"<<endl; }
};

class Pet
{
public:
int j;
virtual void say(){ cout<<"Pet say"<<endl; }
};

class Dog : public Cute,public Pet
{
public:
int z;
void cute(){ cout<<"Dog cute"<<endl; }
void say(){ cout<<"Dog say"<<endl; }
};

在上面这个例子中,一个Dog对象在内存中的布局如下所示:





Dog


Vptr1


Cute::i


Vptr2


Pet::j


Dog::z


也就是说,在Dog对象中,会存在两个vptr,每一个跟所继承的父类相对应。如果我们要想实现多态,就必须在对象中准确地找到相应的vptr,以调用不同的方法。但是,如果根据单继承时的逻辑,也就是vptr放在指针指向位置的起始处,那么,要在多重继承情况下实现,我们必须保证在将一个派生类的指针隐式或者显式地转换成一个父类的指针时,得到的结果指向相应派生类数据在Dog对象中的起始位置。幸好,这工作编译器已经帮我们完成了。上面的例子中,如果Dog向上转换成Pet的话,编译器会自动计算Pet数据在Dog对象中的偏移量,该偏移量加上Dog对象的起始位置,就是Pet数据的实际地址了。

int main()
{
Dog* d = new Dog();
cout<<"Dog object addr : "<<d<<endl;
Cute* c = d;
cout<<"Cute type addr : "<<c<<endl;
Pet* p = d;
cout<<"Pet type addr : "<<p<<endl;
delete d;
}


output:
Dog object addr : 0x3d24b0
Cute type addr : 0x3d24b0
Pet type addr : 0x3d24b8 // 正好指向Dog对象的vptr2处,也就是Pet的数据


好了,既然编译器帮我们自动完成了不同父类的地址转换,我们调用虚函数的过程也就跟单继承统一起来了:通过具体对象,找到vptr(通常指针的起始位置,因此Cute找到的是vptr1,而Pet找到的是vptr2),通过vptr,我们找到VTable,然后根据编译时得到的VTable索引号,我们取得相应的函数地址,接着就可以马上调用了。

在这里,顺便也提一下两个特殊的方法在多态中的特别之处吧:第一个是构造函数,在构造函数中调用虚函数是不会有多态行为的,例子如下:

class Pet
{
public:
Pet(){ sayHello(); }
void say(){ sayHello(); }

virtual void sayHello()
{
cout<<"Pet sayHello"<<endl;
}

};

class Dog : public Pet
{
public:
Dog(){};
void sayHello()
{
cout<<"Dog sayHello"<<endl;
}
};

int main()
{
Pet* p = new Dog();
p->sayHello();
delete p;
}

output:
Pet sayHello //直接调用的是Pet的sayHello()
Dog sayHello //多态


第二个就是析构函数,使用多态的时候,我们经常使用基类的指针来引用派生类的对象,如果是动态创建的,对象使用完后,我们使用delete来释放对象。但是,如果我们不注意的话,会有意想不到的情况发生。

class Pet
{
public:
~Pet(){ cout<<"Pet destructor"<<endl; }
//virtual ~Pet(){ cout<<"Pet virtual destructor"<<endl; }
};

class Dog : public Pet
{
public:
~Dog(){ cout<<"Dog destructor"<<endl;};
//virtual ~Dog(){ cout<<"Dog virtual destructor"<<endl; }
};

int main()
{
Pet* p = new Dog();
delete p;
}

output:
Pet destructor //糟了,Dog的析构函数没有调用,memory leak!

如果我们将析构函数改成virtual以后,结果如下
Dog virtual destructor
Pet virtual destructor // That's OK!

所以,如果一个类设计用来被继承的话,那么它的析构函数应该被声明为virtual的。

Reference:
[1] Comparing C++ and C (Inheritance and Virtual Functions)
[2] C++对象布局及多态实现的探索
[3] Multiple inheritance and the this pointer 讲述多重继承下的类型转换问题
[4] Memory Layout for Multiple and Virtual Inheritance 详细描述了多重菱形多重继承下的对象内存布局以及类型转换

后记:当我完成了本篇%90的时候,我试图提交,谁知道登陆太久没有动作,session超时,让我重新登陆,然后提交的内容就全部不见了,剩下最开始的%10。。。。当时的心情啊,用吕大哥的话来说就是:寻死的心都有了。
...全文
288 4 打赏 收藏 转发到动态 举报
写回复
用AI写文章
4 条回复
切换为时间正序
请发表友善的回复…
发表回复
sandshaw 2015-08-11
  • 打赏
  • 举报
回复
little_bai_ 2015-08-11
  • 打赏
  • 举报
回复
楼主加油
paschen 版主 2015-05-14
  • 打赏
  • 举报
回复
楼主应该发到博客。。
FightForProgrammer 2015-05-14
  • 打赏
  • 举报
回复
这么长,你确定要大家看。。

64,637

社区成员

发帖
与我相关
我的任务
社区描述
C++ 语言相关问题讨论,技术干货分享,前沿动态等
c++ 技术论坛(原bbs)
社区管理员
  • C++ 语言社区
  • encoderlee
  • paschen
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
  1. 请不要发布与C++技术无关的贴子
  2. 请不要发布与技术无关的招聘、广告的帖子
  3. 请尽可能的描述清楚你的问题,如果涉及到代码请尽可能的格式化一下

试试用AI创作助手写篇文章吧