zoukankan      html  css  js  c++  java
  • 构造函数为什么不能为虚函数 & 基类的析构函数为什么要为虚函数

    一、构造函数为什么不能为虚函数
    1. 从存储空间角度,虚函数相应一个指向vtable虚函数表的指针,这大家都知道,但是这个指向vtable的指针事实上是存储在对象的内存空间的。问题出来了,假设构造函数是虚的,就须要通过 vtable来调用,但是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。
    2. 从使用角度,虚函数主要用于在信息不全的情况下,能使重载的函数得到相应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候可以变成调用子类的那个成员函数。而构造函数是在创建对象时自己主动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
    3. 构造函数不须要是虚函数,也不同意是虚函数,由于创建一个对象时我们总是要明白指定对象的类型,虽然我们可能通过实验室的基类的指针或引用去訪问它但析构却不一定,我们往往通过基类的指针来销毁对象。这时候假设析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。
    4. 从实现上看,vbtl在构造函数调用后才建立,因而构造函数不可能成为虚函数从实际含义上看,在调用构造函数时还不能确定对象的真实类型(由于子类会调父类的构造函数);并且构造函数的作用是提供初始化,在对象生命期仅仅运行一次,不是对象的动态行为,也没有必要成为虚函数。
    5. 当一个构造函数被调用时,它做的首要的事情之中的一个是初始化它的VPTR。因此,它仅仅能知道它是“当前”类的,而全然忽视这个对象后面是否还有继承者。当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码——既不是为基类,也不是为它的派生类(由于类不知道谁继承它)。所以它使用的VPTR必须是对于这个类的VTABLE。并且,仅仅要它是最后的构造函数调用,那么在这个对象的生命期内,VPTR将保持被初始化为指向这个VTABLE, 但假设接着另一个更晚派生的构造函数被调用,这个构造函数又将设置VPTR指向它的 VTABLE,等.直到最后的构造函数结束。VPTR的状态是由被最后调用的构造函数确定的。这就是为什么构造函数调用是从基类到更加派生类顺序的还有一个理由。可是,当这一系列构造函数调用正发生时,每一个构造函数都已经设置VPTR指向它自己的VTABLE。假设函数调用使用虚机制,它将仅仅产生通过它自己的VTABLE的调用,而不是最后的VTABLE(全部构造函数被调用后才会有最后的VTABLE)。

    虚函数与非虚函数对照
      使用虚函数是有代价的,在内存和运行速度方面是有一定成本的,包含:

        l  每一个对象都将增大,增大量为存储虚函数表指针的大小;

        l  对于每一个类,编译器都创建一个虚函数地址表;

        l  对于每一个函数调用,都须要运行一项额外的操作,即到虚函数表中查找地址。

      尽管非虚函数比虚函数效率稍高,单不具备动态联编能力


    二、为什么基类的析构函数是虚函数?

      在实现多态时,当用基类操作派生类,在析构时防止仅仅析构基类而不析构派生类的状况发生。

      以下转自网络:源地址 http://blog.sina.com.cn/s/blog_7c773cc50100y9hz.html

      a.第一段代码

      

    复制代码
    复制代码
    #include<iostream>
    using namespace std;
    class ClxBase{
    public:
        ClxBase() {};
        ~ClxBase() {cout << "Output from the destructor of class ClxBase!" << endl;};
    
        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; };
    };
      int   main(){  
      ClxDerived *p =  new ClxDerived;
      p->DoSomething();
      delete p;
      return 0;
      }
    复制代码
    复制代码

      执行结果:

      Do something in class ClxDerived!            

      Output from the destructor of class ClxDerived!

      Output from the destructor of class ClxBase!  

      这段代码中基类的析构函数不是虚函数,在main函数中用继承类的指针去操作继承类的成员,释放指针P的过程是:先释放继承类的资源,再释放基类资源. 

      b.第二段代码

      

    复制代码
    复制代码
    #include<iostream>
    using namespace std;
    class ClxBase{
    public:
        ClxBase() {};
        ~ClxBase() {cout << "Output from the destructor of class ClxBase!" << endl;};
    
        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; }
    };
      int   main(){  
      ClxBase *p =  new ClxDerived;
      p->DoSomething();
      delete p;
      return 0;
      } 
    复制代码
    复制代码

      输出结果:

      Do something in class ClxBase!
      Output from the destructor of class ClxBase!

        这段代码中基类的析构函数相同不是虚函数,不同的是在main函数中用基类的指针去操作继承类的成员,释放指针P的过程是:仅仅是释放了基类的资源,而没有调用继承类的析构函数.调用  dosomething()函数运行的也是基类定义的函数.

        普通情况下,这种删除仅仅可以删除基类对象,而不能删除子类对象,形成了删除一半形象,造成内存泄漏.

        在公有继承中,基类对派生类及其对象的操作,仅仅能影响到那些从基类继承下来的成员.假设想要用基类对非继承成员进行操作,则要把基类的这个函数定义为虚函数.

        析构函数自然也应该如此:假设它想析构子类中的又一次定义或新的成员及对象,当然也应该声明为虚的. 

      c.第三段代码:

      

    复制代码
    复制代码
    #include<iostream>
    using namespace std;
    class ClxBase{
    public:
        ClxBase() {};
        virtual ~ClxBase() {cout << "Output from the destructor of class ClxBase!" << endl;};
        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; };
    };
    
      int   main(){  
      ClxBase *p =  new ClxDerived;
      p->DoSomething();
      delete p;
      return 0;
      }  
    复制代码
    复制代码

      执行结果:

      Do something in class ClxDerived!
      Output from the destructor of class ClxDerived!
      Output from the destructor of class ClxBase!

        这段代码中基类的析构函数被定义为虚函数,在main函数中用基类的指针去操作继承类的成员,释放指针P的过程是:仅仅是释放了继承类的资源,再调用基类的析构函数.调用dosomething()函数运行的也是继承类定义的函数.  

        假设不须要基类对派生类及对象进行操作,则不能定义虚函数,由于这样会添加内存开销.当类里面有定义虚函数的时候,编译器会给类加入一个虚函数表,里面来存放虚函数指针,这样就会添加类的存储空间.所以,仅仅有当一个类被用来作为基类的时候,才把析构函数写成虚函数.


  • 相关阅读:
    Sysinternals Suite
    扩展Visual Studio Test Project:自定义TestClassAttribute
    扩展Visual Studio Test Project:自定义TestClassAttribute
    SQL Server Single-user Mode
    MAXDOP(max degree of parallelism)
    关于log4net
    Go 切片的一种有趣内存泄漏方式
    Go 中的内联优化
    优化 Golang 服务来减少 40% 以上的 CPU
    Go 编译器内部知识:向 Go 添加新语句-第 2 部分
  • 原文地址:https://www.cnblogs.com/mengfanrong/p/4011342.html
Copyright © 2011-2022 走看看