关于中兴 金山的一道题中虚函数 构造、析构函数

来源:互联网 发布:mac chili铁锈红 编辑:程序博客网 时间:2024/05/16 05:52
class Value
{
public:
        Value(int   nVal){m_nVal = nVal;printf("Call Value::Value(int nValue)\n");}
        ~Value(){printf("Call Value::~Value()\n");}
        Value& operator=(int nVal){m_nVal = nVal;printf("Call Value::operator=\n");return *this;}
        void Dump(){printf("Value::m_nVal=%d\n",m_nVal);}
protected:
        int   m_nVal;
};

class Base
{
public:
        Base(){Init();}
        virtual ~Base(){Release();}
        virtual void Init(){printf("Call Base::Init()\n");}
        virtual void Release(){printf("Call Base::Release()\n");}
        virtual void Dump(){printf("Call   Base::Dump()\n");}
};


class Derive:public Base
{
public:
        Derive(){printf("Call Derive::Derive()\n");}
        ~Derive(){printf("Call   Derive::~Derive()\n");}
        virtual void Init(){m_Val=2;printf("Call Derive::Init()\n");}
        virtual void Release(){printf("Call Derive::Release()\n");}
        virtual void Dump(){m_Val.Dump();}
protected:
        static Value m_Val;
};

Value Derive::m_Val=0;

void DestroyObj(Base* pOb)
{
        pOb->Dump();
        delete pOb;
}

int main()
{
        Derive *pOb = new Derive;
        DestroyObj(pOb);

        return 0;
}

题目问的是执行该程序,输出什么?

下面是在别人blog上看到的一个解释

虚函数与构造函数、析构函数

1、构造函数能不能是虚函数:

          1.1 从存储空间角度

虚函数对应一个vtable,这大家都知道,可是这个vtable其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚的,就需要通过 vtable来调用,可是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。

       1.2从使用角度

       虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。

       1.3从作用

       虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
       1.4
       vbtl在构造函数调用后才建立因而构造函数不可能成为虚函数在调用构造函数时还不能确定对象的真实类型(因为子类会调父类的构造函数);而且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有太大的必要成为虚函数


2、析构函数可以为虚函数,甚至是纯虚的
    我们往往通过基类的指针来销毁对象。这时候如果析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。
class A
{
public:
    virtual ~A()=0;   // 纯虚析构函数
};
       当一个类打算被用作其它类的基类时,它的析构函数必须是虚的。考虑下面的例子:
class A
{
public:
    A() { ptra_ = new char[10];}
    ~A() { delete[] ptra_;}        // 非虚析构函数
private:
    char * ptra_;
};
class B: public A
{
public:
    B() { ptrb_ = new char[20];}
    ~B() { delete[] ptrb_;}
private:
    char * ptrb_;
};
void foo()
{

    A * a = new B;
    delete a;
}
       在这个例子中,程序也许不会象你想象的那样运行,在执行delete a的时候,实际上只有A::~A()被调用了,而B类的析构函数并没有被调用!这是否有点儿可怕?
      如果将上面A::~A()改为virtual,就可以保证B::~B()也在delete a的时候被调用了。因此基类的析构函数都必须是virtual的。

      纯虚的析构函数并没有什么作用,是虚的就够了。通常只有在希望将一个类变成抽象类(不能实例化的类),而这个类又没有合适的函数可以被纯虚化的时候,可以使用纯虚的析构函数来达到目的。

3、关于构造函数

      编译器对每个包含虚函数的类创建一个表(称为VTABLE)。在VTABLE中,编译器放置特定类的虚函数地址。在每个带有虚函数的类中,编译器秘密地置一指针,称为vpointer(缩写为VPTR),指向这个对象的VTABLE。

      当一个构造函数被调用时,它做的首要的事情之一是初始化它的VPTR。因此,它只能知道它是“当前”类的,而完全忽视这个对象后面是否还有继承者当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码--既不是为基类,也不是为它的派生类(因为类不知道谁继承它)
      所以它使用的VPTR必须是对于这个类的VTABLE。而且,只要它是最后的构造函数调用,那么在这个对象的生命期内,VPTR将保持被初始化为指向这个VTABLE。但如果接着还有一个更晚派生的构造函数被调用,这个构造函数又将设置VPTR指向它的 VTABLE,等.直到最后的构造函数结束。VPTR的状态是由被最后调用的构造函数确定的。这就是为什么构造函数调用是从基类到派生类顺序的另一个理由。

       但是,当这一系列构造函数调用正发生时,vbtl在构造函数调用后才建立因而构造函数不可能成为虚函数,每个构造函数都已经设置VPTR指向它自己的VTABLE。如果函数调用使用虚机制,它将只产生通过它自己的VTABLE的调用,而不是最后的VTABLE(所有构造函数被调用后才会有最后的VTABLE)。

也就是 它调用的只是当前的VTABLE,最后的VTABLE需要再后续的构造函数完成后才能掉哦那个。

4、  虚函数

是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。假设我们有下面的类层次:
class A
{
public:
    virtual void foo() { cout << "A::foo() is called" << endl;}
};
class B: public A
{
public:
    virtual void foo() { cout << "B::foo() is called" << endl;}
};

那么,在使用的时候,
A * a = new B();
a->foo();       // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!
      这个例子是虚函数的一个典型应用,通过这个例子,也许你就对虚函数有了一些概念。它虚就虚在所谓“推迟联编”或者“动态联编”上,一个类函数的调用并不是在编译时刻被确定的而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为“虚”函数。
      虚函数只能借助于指针或者引用来达到多态的效果,如果是下面这样的代码,则虽然是虚函数,但它不是多态的:
class A
{
public:
    virtual void foo();
};
class B: public A
{
    virtual void foo();
};
void bar()
{
    A a;
    a.foo();   // A::foo()被调用
}
5、 多态
      在了解了虚函数的意思之后,再考虑什么是多态就很容易了。仍然针对上面的类层次,但是使用的方法变的复杂了一些:
void bar(A * a)
{
    a->foo();  // 被调用的是A::foo() 还是B::foo()?
}
    因为foo()是个虚函数,所以在bar这个函数中,只根据这段代码,无从确定这里被调用的是A::foo()还是B::foo(),但是可以肯定的说:如果a指向的是A类的实例,则A::foo()被调用,如果a指向的是B类的实例,则B::foo()被调用。
这种同一代码可以产生不同效果的特点,被称为“多态”。

5.1 多态有什么用?
       多态这么神奇,但是能用来做什么呢?这个命题我难以用一两句话概括,一般的C++教程(或者其它面向对象语言的教程)都用一个画图的例子来展示多态的用途,我就不再重复这个例子了,如果你不知道这个例子,随便找本书应该都有介绍。我试图从一个抽象的角度描述一下,回头再结合那个画图的例子,也许你就更容易理解。
     在面向对象的编程中,首先会针对数据进行抽象(确定基类)和继承(确定派生类),构成类层次。这个类层次的使用者在使用它们的时候,如果仍然在需要基类的时候写针对基类的代码,在需要派生类的时候写针对派生类的代码,就等于类层次完全暴露在使用者面前。如果这个类层次有任何的改变(增加了新类),都需要使用者“知道”(针对新类写代码)。这样就增加了类层次与其使用者之间的耦合,有人把这种情况列为程序中的“bad smell”之一。
     多态可以使程序员脱离这种窘境。再回头看看上面的例子,bar()作为A-B这个类层次的使用者,它并不知道这个类层次中有多少个类,每个类都叫什么,但是一样可以很好的工作,当有一个C类从A类派生出来后,bar()也不需要“知道”(修改)。这完全归功于多态--编译器针对虚函数产生了可以在运行时刻确定被调用函数的代码。

5.2 如何“动态联编”
      编译器是如何针对虚函数产生可以再运行时刻确定被调用函数的代码呢?也就是说,虚函数实际上是如何被编译器处理的呢?Lippman在深度探索C++对象模型[1]中的不同章节讲到了几种方式,这里把“标准的”方式简单介绍一下。

所说的“标准”方式,也就是所谓的“VTABLE”机制。编译器发现一个类中有被声明为virtual的函数,就会为其搞一个虚函数表,也就是VTABLE。VTABLE实际上是一个函数指针的数组,每个虚函数占用这个数组的一个slot。一个类只有一个VTABLE,不管它有多少个实例。派生类有自己的VTABLE,但是派生类的VTABLE与基类的VTABLE有相同的函数排列顺序,同名的虚函数被放在两个数组的相同位置上。在创建类实例的时候,编译器还会在每个实例的内存布局中增加一个vptr字段,该字段指向本类的VTABLE。通过这些手段,编译器在看到一个虚函数调用的时候,就会将这个调用改写。
void bar(A * a){    a->foo();   }
会被改写为:
void bar(A * a){    (a->vptr[1])();  }
因为派生类和基类的foo()函数具有相同的VTABLE索引,而他们的vptr又指向不同的VTABLE,因此通过这样的方法可以在运行时刻决定调用哪个foo()函数。虽然实际情况远非这么简单,但是基本原理大致如此。

5.3 overload和override
      虚函数总是在派生类中被改写,这种改写被称为“override”。我经常混淆“overload”和“override”这两个单词。澄清一下:
    override是指派生类重写基类的虚函数,就象我们前面B类中重写了A类中的foo()函数。重写的函数必须有一致的参数表和返回值(C++标准允许返回值不同的情况,这个我会在“语法”部分简单介绍,但是很少编译器支持这个feature)。这个单词好象一直没有什么合适的中文词汇来对应,有人译为“覆盖”,还贴切一些。
    overload约定成俗的被翻译为“重载”。是指编写一个与已有函数同名但是参数表不同的函数。例如一个函数即可以接受整型数作为参数,也可以接受浮点数作为参数。


6、 虚函数的语法
6.1  virtual关键字
class A
{
public:
    virtual void foo();
};
class B: public A
{
public:
    void foo();    // 没有virtual关键字!
};
class C: public B  // 从B继承,不是从A继承!
{
public:
    void foo();    // 也没有virtual关键字!
};
      这种情况下,B::foo()是虚函数,C::foo()也同样是虚函数。因此,可以说,基类声明的虚函数,在派生类中也是虚函数,即使不再使用virtual关键字。

6.2  private的虚函数
class A
{
public:
    void foo() { bar();}
private:
    virtual void bar() { ...}
};

class B: public A
{
private:
    virtual void bar() { ...}
};
      在这个例子中,虽然bar()在A类中是private的,但是仍然可以出现在派生类中,并仍然可以与public或者protected的虚函数一样产生多态的效果。并不会因为它是private的,就发生A::foo()不能访问B::bar()的情况,也不会发生B::bar()对A::bar()的override不起作用的情况。
     这种写法的语意是:A告诉B,你最好override我的bar()函数,但是你不要管它如何使用,也不要自己调用这个函数。

6.3 构造函数和析构函数中的虚函数调用
    一个类的虚函数在它自己的构造函数和析构函数中被调用的时候,它们就变成普通函数了,不“虚”了。也就是说不能在构造函数和析构函数中让自己“多态”。

当构造函数内部有虚函数时,会出现什么情况呢?结果是,只有在该类中的虚函数被调用,也就是说,在构造函数中,虚函数机制不起作用了,调用虚函数如同调用一般的成员函数一样。

当析构函数内部有虚函数时,又如何工作呢?与构造函数相同,只有“局部”的版本被调用。但是,行为相同,原因是不一样的。构造函数只能调用“局部”版本,是因为调用时还没有派生类版本的信息。析构函数则是因为派生类版本的信息已经不可靠了。我们知道,析构函数的调用顺序与构造函数相反,是从派生类的析构函数到基类的析构函数。当某个类的析构函数被调用时,其下一级的析构函数已经被调用了,相应的数据也已被丢失,如果再调用虚函数的最后一级的版本,就相当于对一些不可靠的数据进行操作,这是非常危险的。因此,在析构函数中,虚函数机制也是不起作用的。例如:
class A
{
public:
    A() { foo();}        // 在这里,无论如何都是A::foo()被调用!
    ~A() { foo();}       // 同上
    virtual void foo();
};

class B: public A
{
public:
    virtual void foo();
};

void bar()
{
    A * a = new B;
    delete a;
}
     如果你希望delete a的时候,会导致B::foo()被调用,那么你就错了。同样,在new B的时候,A的构造函数被调用,但是在A的构造函数中,被调用的是A::foo()而不是B::foo()。

6.4 什么时候使用虚函数
    在你设计一个基类的时候,如果发现一个函数需要在派生类里有不同的表现,那么它就应该是虚的。从设计的角度讲,出现在基类中的虚函数是接口,出现在派生类中的虚函数是接口的具体实现。通过这样的方法,就可以将对象的行为抽象化。
“如果你发现基类提供了虚函数,那么你最好override它”。
  

7、Things to Remember

       定义一个函数为虚函数,不代表函数为不被实现的函数。定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
       定义一个函数为纯虚函数,才代表函数没有被实现。定义他是为了实现一个接口,起到一个规范的作用,规范继承这个。类的程序员必须实现这个函数。
    有纯虚函数的类是不可能生成类对象的,如果没有纯虚函数则可以。
     多态一般就是通过指向基类的指针来实现的。

4.有一点你必须明白,就是用父类的指针在运行时刻来调用子类:
例如,有个函数是这样的:
void animal::fun1(animal *maybedog_maybehorse)
{
     maybedog_maybehorse->born();

}
参数maybedog_maybehorse在编译时刻并不知道传进来的是dog类还是horse类,所以就把它设定为animal类,具体到运行时决定了才决定用那个函数。也就是说用父类指针通过虚函数来决定运行时刻到底是谁而指向谁的函数。


8、用虚函数和不用虚函数
class animal
{
public:
     animal();
     ~animal();
     void fun1(animal *maybedog_maybehorse);
     virtual void born();
};
void animal::fun1(animal *maybedog_maybehorse)
{
     maybedog_maybehorse->born();
}

animal::animal() { }
animal::~animal() { }
void animal::born()
{
     cout<< "animal";
}
class horse:public animal
{
public:
     horse();
     ~horse();
     virtual void born();
};
horse::horse() { }
horse::~horse() { }
void horse::born()

{
     cout<<"horse";
}
void main()
{
     animal a;
     horse b;
     a.fun1(&b); //output: horse //如果不用虚函数则输出animal
}
9、例子

class CBase{
public:

CBase(){

  cout<<"CBase Constructor! ";

  func();

   }

~CBase(){

cout<<"CBase deconstructor! ";

func();

}

virtual void func(){ cout<<"CBase::func() called! "; }

};
class CDerive: public CBase{

public:

    CDerive(){

cout<<"CDerive Constructor! ";

func();

}

~CDerive(){

   cout<<"CDerive deconstructor! ";

func();

}

void func(){ cout<<"CDerive::func() called! ";}

void func1(){ func();  }      //调用虚函数

};

class CSubDerive: public CDerive{

public:

CSubDerive(){

cout<<"CSubDerive Constructor! ";

func();

}

~CSubDerive(){

cout<<"CSubDerive deconstructor! ";

func();

}

void func(){ cout<<"CSubDerive::func() called! ";}

};

void main(){

CSubDerive obj;    //will produce "CBase::func() called!"

obj.func1();       //will produce "CSubDerive::func() called!"

}    

所以题目的输出的结果:

Call Value::Value(int nValue)
Call Base::Init()
Call Derive::Derive()
Value::m_nVal=0
Call Derive::~Derive()
Call Base::Release()
Call Value::~Value()
 
你不应该在构造或析构期间调用虚函数,因为这样的调用不会如你想象那样工作,而且它们做的事情保证会让你很郁闷。如果你转为 Java 或 C# 程序员,也请你密切关注本文,因为在 C++ 急转弯的地方,那些语言也紧急转了一个弯。

  假设你有一套模拟股票处理的类层次结构,例如,购入流程,出售流程等。对这样的处理来说可以核查是非常重要的,所以随时会创建一个 Transaction 对象,将这个创建记录在核查日志中是一个适当的要求。下面是一个看起来似乎合理的解决问题的方法:

class Transaction { // base class for all
 public: // transactions
  Transaction();

  virtual void logTransaction() const = 0; // make type-dependent
  // log entry
  ...
};

Transaction::Transaction() // implementation of
{
 // base class ctor
 ...
 logTransaction(); // as final action, log this
} // transaction

class BuyTransaction: public Transaction {
 // derived class
 public:
  virtual void logTransaction() const; // how to log trans-
  // actions of this type
  ...
};

class SellTransaction: public Transaction {
// derived class
public:
 virtual void logTransaction() const; // how to log trans-
 // actions of this type
...
};

  考虑执行这行代码时会发生什么:

BuyTransaction b;

  很明显 BuyTransaction 的构造函数会被调用,但是首先,Transaction 的构造函数必须先被调用,派生类对象中的基类部分先于派生类部分被构造。Transaction 的构造函数的最后一行调用虚函数 logTransaction,但是结果会让你大吃一惊,被调用的 logTransaction 版本是在 Transaction 中的那个,而不是 BuyTransaction 中的??即使被创建的对象类型是 BuyTransaction。基类构造期间,虚函数从来不会向下匹配(go down)到派生类。取而代之的是,那个对象的行为就好像它的类型是基类。非正式地讲,基类构造期间,虚函数禁止。 这个表面上看起来匪夷所思的行为存在一个很好的理由。因为基类的构造函数在派生类构造函数之前执行,当基类构造函数运行时,派生类数据成员还没有被初始化。如果基类构造期间调用的虚函数向下匹配(go down)到派生类,派生类的函数理所当然会涉及到本地数据成员,但是那些数据成员还没有被初始化。这就会为未定义行为和悔之晚矣的调试噩梦开了一张通行证。调用涉及到一个对象还没有被初始化的部分自然是危险的,所以 C++ 告诉你此路不通。

  在实际上还有比这更多的更深层次的原理。在派生类对象的基类构造期间,对象的类型是那个基类的。不仅虚函数会解析到基类,而且语言中用到运行时类型信息(runtime type information)的配件(例如,dynamic_cast和 typeid),也会将对象视为基类类型。在我们的例子中,当 Transaction 构造函数运行初始化 BuyTransaction 对象的基类部分时,对象的类型是 Transaction。C++ 的每一个配件将以如下眼光来看待它,并对它产生这样的感觉:对象的 BuyTransaction 特有的部分还没有被初始化,所以安全的对待它们的方法就是视若无睹。在派生类构造函数运行之前,一个对象不会成为一个派生类对象。

  同样的原因也适用于析构过程。一旦派生类析构函数运行,这个对象的派生类数据成员就被视为未定义的值,所以 C++ 就将它们视为不再存在。在进入基类析构函数时,对象就成为一个基类对象,C++ 的所有配件??虚函数,dynamic_casts 等??都如此看待它。

  在上面的示例代码中,Transaction 的构造函数直接调用了虚函数,对本 Item 的规则的违例是显而易见的。这一违例是如此显见,以致一些编译器会给出警告。(其它的则不会)甚至除了这样的警告之外,这一问题几乎肯定会在运行之前暴露出来,因为 logTransaction 函数在 Transaction 中是一个纯虚函数。除非它被定义(看似不可能,但确实可能),否则程序将无法连接:连接程序无法找到 Transaction::logTransaction 的必需的实现。
在构造函数和析构函数中调用虚函数的问题并不总是如此容易被察觉。如果 Transaction 有多个构造函数,每一个都必须完成一些相同的工作,好的软件工程为避免代码重复,会将共用的初始化代码,包括对 logTransaction 的调用,放入一个私有的非虚的初始化函数,叫做 init:

class Transaction {
public:
 Transaction()
 { init(); } // call to non-virtual...

 virtual void logTransaction() const = 0;
 ...

private:
 void init()
 {
  ...
  logTransaction(); // ...that calls a virtual!
 }
};

  这个代码在概念上和早先那个版本相同,但是它更阴险,因为它很具代表性地会躲过编译器和连接程序的抱怨。在这种情况下,因为 logTransaction 在 Transaction 中是纯虚函数,大多数运行时系统在纯虚函数被调用时,程序会异常中止(典型的结果就是给出一条信息)。然而,如果 logTransaction 是一个“常规的”虚函数(也就是说,非纯的虚函数),而且在 Transaction 中有其实现,那个版本被调用,程序会继续一路小跑,让你想象不出为什么派生类对象创建的时候会调用 logTransaction 的错误版本。避免这个问题的唯一办法就是确保在你的构造函数和析构函数中,决不在你创建或销毁的对象上调用虚函数,构造函数和析构函数所调用的函数也要服从同样的约束。

  但是,如何保证在任何时间 Transaction 层次结构中的对象被创建时,都能调用 logTransaction 的正确版本呢?显然,在 Transaction 的构造函数中在这个对象上调用虚函数的做法是错误的。

  有不同的方法来解决这个问题。其中之一是将 Transaction 中的 logTransaction 转变为一个非虚函数,这就需要派生类的构造函数将必要的日志信息传递给 Transaction 的构造函数。那个函数就可以安全地调用非虚的 logTransaction。如下:

class Transaction {
public:
 explicit Transaction(const std::string& logInfo);

 void logTransaction(const std::string& logInfo) const; // now a non-
 // virtual func
 ...
};

Transaction::Transaction(const std::string& logInfo)
{
 ...
 logTransaction(logInfo); // now a non-
} // virtual call

class BuyTransaction: public Transaction {
public:
 BuyTransaction( parameters )
 : Transaction(createLogString( parameters )) // pass log info
 { ... } // to base class
 ... // constructor

private:
 static std::string createLogString( parameters );
};

  换句话说,因为在基类的构造过程中你不能使用虚函数,就改为由派生类传递必要的构造信息给基类的构造函数作为补偿。 在此例中,注意 BuyTransaction 中那个(私有的)static 函数 createLogString 的使用。使用一个辅助函数创建一个值传递给基类的构造函数,通常比通过在成员初始化列表给基类它所需要的东西更加便利(也更加具有可读性)。将那个函数做成 static,就不会有偶然涉及到一个初生的 BuyTransaction 对象的仍未初始化的数据成员的危险。这很重要,因为实际上那些数据成员在一个未定义状态,这就是为什么在基类构造和析构期间虚函数不能首先匹配到派生类的原因。

  Things to Remember

  ·在构造和析构期间不要调用虚函数,因为这样的调用不会匹配到当前执行的构造函数或析构函数所属的类的更深的派生层次.