为什么析构函数可以能声明为虚函数,构造函数不可以

转自:http://blog.csdn.NET/chen825919148/article/details/8020550


构造函数不能声明为虚函数,析构函数可以声明为虚函数,而且有时是必须声明为虚函数。
不建议在构造函数和析构函数里面调用虚函数。


构造函数不能声明为虚函数的原因是:
1 构造一个对象的时候,必须知道对象的实际类型,而虚函数行为是在运行期间确定实际类型的。而在构造一个对象时,由于对象还未构造成功。编译器无法知道对象 的实际类型,是该类本身,还是该类的一个派生类,或是更深层次的派生类。无法确定。。。

2 虚函数的执行依赖于虚函数表。而虚函数表在构造函数中进行初始化工作,即初始化vptr,让他指向正确的虚函数表。而在构造对象期间,虚函数表还没有被初 始化,将无法进行。

虚函数的意思就是开启动态绑定,程序会根据对象的动态类型来选择要调用的方法。然而在构造函数运行的时候,这个对象的动态类型还不完整,没有办法确定它到底是什么类型,故构造函数不能动态绑定。(动态绑定是根据对象的动态类型而不是函数名,在调用构造函数之前,这个对象根本就不存在,它怎么动态绑定?)
编译器在调用基类的构造函数的时候并不知道你要构造的是一个基类的对象还是一个派生类的对象。

析构函数设为虚函数的作用:
解释:在类的继承中,如果有基类指针指向派生类,那么用基类指针delete时,如果不定义成虚函数,派生类中派生的那部分无法析构。
例:
#include "stdafx.h"
#include "stdio.h"
class A
{
public:
A();
virtual~A();
};
A::A()
{
}

A::~A()
{
printf("Delete class APn");
}
class B : public A
{
public:
B();
~B();
};

B::B()
{ }

B::~B()
{
printf("Delete class BPn");
}
int main(int argc, char* argv[])
{
A *b=new B;
delete b;
return 0;
}


输出结果为:Delete class B
Delete class A

如果把A的virtual去掉:那就变成了Delete class A也就是说不会删除派生类里的剩余部分内容,也即不调用派生类的虚函数

因此在类的继承体系中,基类的析构函数不声明为虚函数容易造成内存泄漏。所以如果你设计一定类可能是基类的话,必须要声明其为虚函数。正如Symbian中的CBase一样。

Note:
1. 如果我们定义了一个构造函数,编译器就不会再为我们生成默认构造函数了。
2. 编译器生成的析构函数是非虚的,除非是一个子类,其父类有个虚析构,此时的函数虚特性来自父类。
3. 有虚函数的类,几乎可以确定要有个虚析构函数。
4. 如果一个类不可能是基类就不要申明析构函数为虚函数,虚函数是要耗费空间的。
5. 析构函数的异常退出会导致析构不完全,从而有内存泄露。最好是提供一个管理类,在管理类中提供一个方法来析构,调用者再根据这个方法的结果决定下一步的操作。
6. 在构造函数不要调用虚函数。在基类构造的时候,虚函数是非虚,不会走到派生类中,既是采用的静态绑定。显然的是:当我们构造一个子类的对象时,先调用基类的构造函数,构造子类中基类部分,子类还没有构造,还没有初始化,如果在基类的构造中调用虚函数,如果可以的话就是调用一个还没有被初始化的对象,那是很危险的,所以C++中是不可以在构造父类对象部分的时候调用子类的虚函数实现。但是不是说你不可以那么写程序,你这么写,编译器也不会报错。只是你如果这么写的话编译器不会给你调用子类的实现,而是还是调用基类的实现
7.
析构函数中也不要调用虚函数。在析构的时候会首先调用子类的析构函数,析构掉对象中的子类部分,然后在调用基类的析构函数析构基类部分,如果在基类的析构函数里面调用虚函数,会导致其调用已经析构了的子类对象里面的函数,这是非常危险的。

8. 记得在写派生类的拷贝函数时,调用基类的拷贝函数拷贝基类的部分,不能忘记了。


转自:http://blog.sina.com.cn/s/blog_7c773cc50100y9hz.html

1.第一段代码

#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的过程是:先释放继承类的资源,再释放基类资源. 

 

2.第二段代码

#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()函数执行的也是基类定义的函数.

    一般情况下,这样的删除只能够删除基类对象,而不能删除子类对象,形成了删除一半形象,造成内存泄漏.

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

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

 

3.第三段代码:

#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()函数执行的也是继承类定义的函数. 

 

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


转自:http://www.51projob.com/a/bishimianshi/2012/0414/195.html

其实这个问题最终将回答一个问题:

如果Base * pbase = new Derived;那么如果delete pbase的话,怎样避免内存泄露?

对比1:父类的普通成员函数和虚函数均是非虚函数

来看看这时候会发生什么,具体代码和运行结果如下:

  1. #include <iostream> 
  2. using namespace std; 
  3.  
  4. //情景1:普通成员函数和析构函数,都不是虚函数 
  5. class Base{ 
  6. public
  7.     Base(){} 
  8.     //父类的析构函数,不是虚函数 
  9.     ~Base(){ 
  10.         cout<<"Base destructor"<<endl; 
  11.     } 
  12.     //普通成员 
  13.     void dosomething(){ 
  14.         cout<<"do something in Base"<<endl; 
  15.     } 
  16. }; 
  17. //派生类 
  18. class Derived : public Base{ 
  19. public
  20.     Derived(){} 
  21.     ~Derived(){ 
  22.         cout<<"Derived destructor"<<endl; 
  23.     } 
  24.     void dosomething(){ 
  25.         cout<<"do something in Derived"<<endl; 
  26.     } 
  27. }; 
  28.  
  29. int main(){ 
  30.     //这个时候,虽然父类指针实际指向子类,可是没有虚函数表,所以不能调用实际的类型,因此输出的只能是父类指针自身能看到的内容 
  31.     Base *base = new Derived; 
  32.     base->dosomething();//输出的是父类的函数 
  33.     delete base;//调用的是父类的析构函数 
  34.     system("pause"); 
  35.     return 0; 

 

其实涉及到虚函数,必然要想到虚函数表,虚函数表就是对类中的所有虚函数维持的数据结构,若是父类指针调用子类对象,这时候会查找虚函数表到实际的类型。可是如果没有虚函数,父类指针只能“看到”自身的成员,这时候自然只能调用本身的方法(普通方法和析构方法);
因为没有虚构函数,那么pbase指针只能调用子类对象中的父类对象部分的方法,因此只能调用父类的析构函数。

对比2:父类析构函数是虚函数(将调用哪个析构函数?)

将代码的父类的虚构函数加上virtual关键字,其他完全相同,得到如下代码和运行结果:

  1. #include <iostream> 
  2. using namespace std; 
  3.  
  4. //情景1:普通成员函数和析构函数,都不是虚函数 
  5. class Base{ 
  6. public
  7.     Base(){} 
  8.     //父类的析构函数,是虚函数,只做了这里的更改 
  9.     virtual ~Base(){ 
  10.         cout<<"Base destructor"<<endl; 
  11.     } 
  12.     //普通成员 
  13.     void dosomething(){ 
  14.         cout<<"do something in Base"<<endl; 
  15.     } 
  16. }; 
  17. //派生类 
  18. class Derived : public Base{ 
  19. public
  20.     Derived(){} 
  21.     ~Derived(){ 
  22.         cout<<"Derived destructor"<<endl; 
  23.     } 
  24.     void dosomething(){ 
  25.         cout<<"do something in Derived"<<endl; 
  26.     } 
  27. }; 
  28.  
  29. int main(){ 
  30.     //这个时候,虽然父类指针实际指向子类,可是没有虚函数表,所以不能调用实际的类型,因此输出的只能是父类指针自身能看到的内容 
  31.     Base *base = new Derived; 
  32.     base->dosomething();//输出的是父类的函数 
  33.     delete base;//调用的是父类的析构函数 
  34.     system("pause"); 
  35.     return 0; 

 

由此可以看出,这个时候,就真正实现了将实际类型对象进行释放的。同时可以得到,如果父类析构是虚函数,子类调用析构函数的话,会先调用子类的析构函数,之后会调用父类的析构函数
其实这里父类的析构函数加上了virtual,并不是说pbase释放的时候,同时调用了子类的析构函数和父类的析构函数,它实际上指向的是子类的虚函数表,那么就是说父类指针最终只调用了子类的析构函数由C++类本身特性,当子类析构函数调用的时候,会自动调用父类的析构函数,完成了释放。

相关小结

对于Base *pbase = new Derived;

  • 如果父类函数不是析构函数,那么pbase只能“看见”父类本身的函数,这是因为没有虚函数表让它可以找到本身
  • 如果父类析构函数是虚函数,如果delete pbase,将会先调用子类函数的析构函数,然后子类析构函数自动调用父类的析构函数,真正实现了资源释放,防止了内存泄露
  • 构造派生类的时候,会先构造基类部分,然后构造子类部分;撤销派生类对象的时候,会先撤销派生类部分,然后撤销基类部分

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/459427.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

信号集操作函数,信号未决、阻塞、递达

转载&#xff1a;信号集操作函数&#xff0c;信号阻塞与未决 一&#xff0c;信号集及相关操作函数 信号集被定义为一种数据类型&#xff1a; typedef struct { unsigned long sig[_NSIG_WORDS]&#xff1b; } sigset_t 信号集用来描述信号的集合&#xff0c;每个信号占用一位&a…

线程安全和可重入函数的联系与区别

1、 线程安全&#xff1a; 线程安全是多线程访问时&#xff0c;采用了加锁机制&#xff0c;当一个线程访问该类的某个数据时&#xff0c;进行保护&#xff0c;其他线程不能进行访问直到该线程访问完&#xff0c;其他线程才可以使用。不会出现数据不一致或数据污染。 线程不…

C++11 多线程 基础

C11开始支持多线程编程&#xff0c;之前多线程编程都需要系统的支持&#xff0c;在不同的系统下创建线程需要不同的API如pthread_create()&#xff0c;Createthread()&#xff0c;beginthread()等&#xff0c;使用起来都比较复杂&#xff0c;C11提供了新头文件<thread>、…

LB负载均衡集群--LVS

LB集群&#xff1a;LB集群是load balance 集群的简写&#xff0c;翻译成中文就是负载均衡集群。常用的负载均衡开源软件有nginx、lvs、keepalived &#xff0c;商业的硬件负载设备F5、Netscale。LB集群架构&#xff1a;当用户的请求过来时&#xff0c;会直接发到分发器&#xf…

2015 UESTC 搜索专题B题 邱老师降临小行星 记忆化搜索

邱老师降临小行星 Time Limit: 20 Sec Memory Limit: 256 MB 题目连接 http://acm.uestc.edu.cn/#/contest/show/61Description 人赢邱老师和任何男生比&#xff0c;都是不虚的。有一天&#xff0c;邱老师带妹子(们)来到了一个N行M列平面的小行星。对于每一个着陆地点&#xf…

优化表的数据类型

我们可以使用PROCEDURE ANALYSE()对当前已有应用的表类型的判断&#xff0c;该函数可以对数据表中的列的数据类型提出优化建议&#xff0c;可以根据应用的实际情况酌情考虑是否实施优化。语法&#xff1a; SELECT * FROM tbl_name PROCEDURE ANALYSE(); SELECT * FROM tb…

Linux 信号之mysleep

一、 用alarm和pause实现sleep(3)函数,称为mysleep。 1. main函数调用mysleep函数,后者调用sigaction注册了SIGALRM信号的处理函数sig_alrm。 2. 调用alarm(seconds)设定闹钟。 3. 调用pause等待,内核切换到别的进程运行。 4. seconds秒之后,闹钟超时,内核发SIGALRM给这个…

JAVA 操作系统已经来到第五个版本了 现陆续放出三个版本 这是第二个版本

1 package System2;2 3 import javax.swing.*;4 5 import java.awt.*;6 import java.awt.event.ActionEvent;7 import java.awt.event.ActionListener;8 import java.awt.event.KeyListener;9 import java.util.*;10 /**11 * 作者:范铭祥12 * 内容及功能&#xff1a; 显示框…

标准Web系统的架构分层

1、架构体系分层图 在上图中我们描述了Web系统架构中的组成部分。并且给出了每一层常用的技术组件/服务实现。需要注意以下几点&#xff1a; 系统架构是灵活的&#xff0c;根据需求的不同&#xff0c;不一定每一层的技术都需要使用。例如&#xff1a;一些简单的CRM系统可能在产…

数据链路层差错检测:CRC(循环冗余检验)

1、循环冗余检验&#xff08;CRC&#xff09;&#xff1a; 在发送端&#xff0c;先把数据划分为祖&#xff0c;假定每组K个比特。现假定待传送的数据M 101001&#xff08;k6&#xff09;。CRC运算就是在数据M后面添加提供差错检测的n位冗余码&#xff0c;然后构成一个帧发送出…

算法导论笔记:25所有节点对的最短路径问题

本章考虑在给定的有向加权图G(V, E)&#xff0c;对于所有的节点u,v∈V&#xff0c;找到一条从节点u到节点v的最短路径。希望以表格的形式表示输出&#xff1a;第u行第v列给出的是节点u到节点v的最短路径权重。 对于这个问题&#xff0c;如果是运行|V|次单源最短路径算法来解决所…

iOS开发~UI布局(二)storyboard中autolayout和size class的使用详解

一、概要&#xff1a;前一篇初步的描述了size class的概念&#xff0c;那么实际中如何使用呢&#xff0c;下面两个问题是我们一定会遇到的&#xff1a;1、Xcode6中增加了size class&#xff0c;在storyboard中如何使用&#xff1f; 2、auto layout该如何与size class配合来进行…

Linux:守护进程解析、如何实现守护进程

1、守护进程&#xff1a; 守护进程也称精灵进程&#xff08;Daemon&#xff09;&#xff0c;是运行在后台的⼀一种特殊进程。它独立于控制终端且周期性地执行某种任务或等待处理某些发生的事件。守护进程是⼀一种很有用的进程。Linux的大多数服务器就是用守护进程实现的。比如…

CAS实现单点登录方案(SSO完整版)

一、简介 1、cas是由耶鲁大学研发的单点登录服务器 2、本教材所用环境 Tomcat7.2JDK1.7CAS Service 版本 cas-server-3.4.8-releaseCAS Client版本 cas-client-3.2.1-release 二、生成证书 证书对于实现此单点登录非常之重要&#xff0…

PHP、C#、通用的DES加密

2019独角兽企业重金招聘Python工程师标准>>> PHP class JoDES {private static $_instance NULL;/*** return JoDES*/public static function share() {if (is_null(self::$_instance)) {self::$_instance new JoDES();}return self::$_instance;}/*** 加密* para…

如何防止头文件被重复包含或引用?

一、#pragma once ( 比较常用&#xff09; 只要在头文件的最开始加入这条指令就能够保证头文件被编译一次&#xff0c;这条指令实际上在VC6中就已经有了&#xff0c;但是考虑到兼容性并没有太多的使用。 #pragmaonce是编译相关&#xff0c;就是说这个编译系统上能用&#xff…

如何解决类模板的分离编译问题?

一模板&#xff1a; 模板不是数据类型&#xff0c;只能算是一种行为集合的表示。编译器在使用模板时&#xff0c;通过更换模板参数来创建数据类型。这个过程就是模板实例化(Instantiation)&#xff0c; 从模板类创建得到的类型称之为特例(specialization)&#xff0c;说白了就是…

结对项目 刘静 201303014059 计科高职13-2

结对&#xff1a;人&#xff1a;孙帅 博客地址&#xff1a; http://www.cnblogs.com/s3366181/p/4509260.html一、 题目简介 1.所选题目&#xff1a;输出圆的面积 2.编程工具&#xff1a;Eclipse 3、实现功能&#xff1a;用户给定一圆的半径运行程序系统会给出给定半径圆的面…

弹出div或者弹出新窗口的固定位置、固定大小

2019独角兽企业重金招聘Python工程师标准>>> js代码&#xff1a; //打开一个新窗口&#xff0c;固定的位置&#xff0c;固定的大小 //window.open("push_add.html",newwindow, height550, width1000, top200, left500, toolbarno, menubarno, scro…

NAT(网络地址转换)技术与代理服务器原理

一、 Nat技术&#xff1a; NAT英文全称是“Network Address Translation”&#xff0c;中文意思是“网络地址转换”&#xff0c;它是一个IETF(Internet Engineering Task Force,Internet工程任务组)标准&#xff0c;允许一个整体机构以一个公用IP&#xff08;Internet Prot…