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

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的状态是由被最后调用的构造函数确定的。这就是为什么构造函数调用是从基类到派生类顺序的另一个理由。
但是,当这一系列构造函数调用正发生时,每个构造函数都已经设置VPTR指向它自己的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!"
}     
转载自:http://blog.sina.com.cn/s/blog_3c6889fe0100rpsh.html

 

你可能感兴趣的:(构造函数)