当前位置:编程学习 > C/C++ >>

程序员编程艺术(c/c++卷):第八章、从头至尾漫谈虚函数

前奏
有关虚函数的问题层出不穷,有关虚函数的文章千篇一律,那为何还要写这一篇有关虚函数的文章呢?看完本文后,相信能懂其意义之所在。同时,原狂想曲系列已经更名为程序员编程艺术系列,因为不再只专注于“面试”,而在“编程”之上了。ok,如果有不正之处,望不吝赐教。谢谢。


第一节、一道简单的虚函数的面试题
题目要求:写出下面程序的运行结果?
view plaincopy to clipboardprint?
//谢谢董天喆提供的这道百度的面试题   
#include <iostream>  
using namespace std;  
 
class A  
{  
public:  
    virtual void p()   
    {   
        cout << "A" << endl;   
    }  
      
};  
 
class B : public A  
{  
public:  
    virtual void p()   
    {   
        cout << "B" << endl;  
    }  
};  
 
int main()   
{  
    A * a = new A;  
    A * b = new B;  
    a->p();  
    b->p();  
    delete a;  
    delete b;  
    return 0;  

//谢谢董天喆提供的这道百度的面试题
#include <iostream>
using namespace std;

class A
{
public:
 virtual void p()
 {
  cout << "A" << endl;
 }
 
};

class B : public A
{
public:
 virtual void p()
 {
  cout << "B" << endl;
 }
};

int main()
{
 A * a = new A;
 A * b = new B;
 a->p();
 b->p();
 delete a;
 delete b;
    return 0;
}

    我想,这道面试题应该是考察虚函数相关知识的相对简单的一道题目了。然后,希望你碰到此类有关虚函数的面试题,不论其难度是难是易,都能够举一反三,那么本章的目的也就达到了。ok,请跟着我的思路,咱们步步深入(上面程序的输出结果为A B)。

第二节、有无虚函数的区别
      1、当上述程序中的函数p()不是虚函数,那么程序的运行结果是如何?即如下代码所示:

class A
{
public:
 void p()
 {
  cout << "A" << endl;
 }
 
};

class B : public A
{
public:
 void p()
 {
  cout << "B" << endl;
 }
};

对的,程序此时将输出两个A,A。为什么?
我们知道,在构造一个类的对象时,如果它有基类,那么首先将构造基类的对象,然后才构造派生类自己的对象。如上,A* a=new A,调用默认构造函数构造基类A对象,然后调用函数p(),a->p();输出A,这点没有问题。
    然后,A * b = new B;,构造了派生类对象B,B由于是基类A的派生类对象,所以会先构造基类A对象,然后再构造派生类对象,但由于当程序中函数是非虚函数调用时,B类对象对函数p()的调用时在编译时就已静态确定了,所以,不论基类指针b最终指向的是基类对象还是派生类对象,只要后面的对象调用的函数不是虚函数,那么就直接无视,而调用基类A的p()函数。

      2、那如果加上虚函数呢?即如最开始的那段程序那样,程序的输出结果,将是什么?
在此之前,我们还得明确以下两点:
    a、通过基类引用或指针调用基类中定义的函数时,我们并不知道执行函数的对象的确切类型,执行函数的对象可能是基类类型的,也可能是派生类型的。
    b、如果调用非虚函数,则无论实际对象是什么类型,都执行基类类型所定义的函数(如上述第1点所述)。如果调用虚函数,则直到运行时才能确定调用哪个函数,运行的虚函数是引用所绑定的或指针所指向的对象所属类型定义的版本。

根据上述b的观点,我们知道,如果加上虚函数,如上面这道面试题,

class A
{
public:
 virtual void p()
 {
  cout << "A" << endl;
 }
 
};

class B : public A
{
public:
 virtual void p()
 {
  cout << "B" << endl;
 }
};

int main()
{
 A * a = new A;
 A * b = new B;
 a->p();
 b->p();
 delete a;
 delete b;
    return 0;
}

那么程序的输出结果将是A B。

所以,至此,咱们的这道面试题已经解决。但虚函数的问题,还没有解决。


第三节、虚函数的原理与本质
    我们已经知道,虚(virtual)函数的一般实现模型是:每一个类(class)有一个虚表(virtual table),内含该class之中有作用的虚(virtual)函数的地址,然后每个对象有一个vptr,指向虚表(virtual table)的所在。

请允许我援引自深度探索c++对象模型一书上的一个例子:

class Point {
public:
   virtual ~Point();  

   virtual Point& mult( float ) = 0; 

   float x() const { return _x; }     //非虚函数,不作存储
   virtual float y() const { return 0; } 
   virtual float z() const { return 0; } 
   // ...

protected:
   Point( float x = 0.0 );
   float _x;
};

      1、在Point的对象pt中,有两个东西,一个是数据成员_x,一个是_vptr_Point。其中_vptr_Point指向着virtual table point,而virtual table(虚表)point中存储着以下东西:

virtual ~Point()被赋值slot 1,
mult() 将被赋值slot 2.
y() is 将被赋值slot 3
z() 将被赋值slot 4.
class Point2d : public Point {
public:
   Point2d( float x = 0.0, float y = 0.0 ) 
      : Point( x ), _y( y ) {}
   ~Point2d();   //1

   //改写base class virtual functions
   Point2d& mult( float );  //2
   float y() const { return _y; }  //3

protected:
   float _y;
};

      2、在Point2d的对象pt2d中,有三个东西,首先是继承自基类pt对象的数据成员_x,然后是pt2d对象本身的数据成员_y,最后是_vptr_Point。其中_vptr_Point指向着virtual table point2d。由于Point2d继承自Point,所以在virtual table point2d中存储着:改写了的其中的~Point2d()、Point2d& mult( float )、float y() const,以及未被改写的Point::z()函数。

class Point3d: public Point2d {
public:
   Point3d( float x = 0.0,
            float y = 0.0, float z = 0.0 )
      : Point2d( x, y ), _z( z ) {}
   ~Point3d();

   // overridden base class virtual functions
   Point3d& mult( float );
   float z() const { return _z; }

   // ... other operations ...
protected:
   float _z;
};

      3、在Point3d的对象pt3d中,则有四个东西,一个是_x,一个是_vptr_Point,一个是_y,一个是_z。其中_vptr_Point指向着virtual table point3d。由于point3d继承自point2d,所以在virtual table point3d中存储着:已经改写了的point3d的~Point3d(),point3d::mult()的函数地址,和z()函数的地址,以及未被改写的point2d的y()函数地址。

ok,上述1、2、3所有情况的详情,请参考下图。

\

(图:virtual table(虚表)的布局:单一继承情况)

本文,日后可能会酌情考虑增补有关内容。ok,更多,可参考深度探索c++对象模型一书第四章。
最近几章难度都比较小,是考虑到狂想曲有深有浅的原则,后续章节会逐步恢复到相应难度。

第四节、虚函数的布局与汇编层面的考察

      ivan、老梦的两篇文章继续

补充:软件开发 , C++ ,
CopyRight © 2022 站长资源库 编程知识问答 zzzyk.com All Rights Reserved
部分文章来自网络,