一、析构函数的定义

析构函数为成员函数的一种,名字与类名相同,在前面加‘~’没有参数和返回值在C++中“~”是位取反运算符。
一个类最多只能有一个析构函数。析构函数不返回任何值,没有函数类型,也没有函数参数,因此它不能被重载。

构造函数可能有多个,但析构函数只能有一个,就像人来到人世间,可能出生的环境家庭不同(重载构造函数),但最终都会死亡(析构函数)。

class C
{
public:
~C ( ) { }
~C (int i) { } // error C2524: “C”: 析构函数 必须有“void”参数列表
// warning C4523: “C”: 指定了多个析构函数
};

析构函数对象消亡时即自动被调用。可以定义析构函数来在对象消亡前做善后工作,比如释放分配的空间等。
如果定义类时没写析构函数,则编译器生成缺省析构函数。缺省析构函数什么也不做。如果定义了析构函数,则编译器不生成缺省析构函数。
析构函数的作用并不是删除对象,而是在撤销对象占用的内存之前完成一些清理工作。

例:

class A
{
private :
char * p;
public:
A ( )
{
p = new char[];
}
~ A ( )
{
delete [] p;
}
};

若A类没写析构函数,则在生成A对象后,new出来的内存空间未清除,可能造成内存泄露。

在创建一类的对象数组时,对于每一个数组元素,都会执行缺省的构造函数。同样,对象数组生命期结束时,对象数组的每个元素的析构函数都会被调用。

#include<iostream>
using namespace std;
unsigned count = ;
class A
{
public:
A ( )
{
i = ++count;
cout << "Creating A " << i <<endl;
}
~A ( )
{
cout << "A Destructor called " << i <<endl;
}
private :
int i;
};
int main( )
{
A ar[]; // 对象数组
return ;
}

程序执行结果为:
Creating A 1
Creating A 2
Creating A 3
A Destructor called 3
A Destructor called 2
A Destructor called 1

类似于栈的后进先出

二、析构函数的调用

如果出现以下几种情况,程序就会执行析构函数:
(1)如果在一个函数中定义了一个对象(它是自动局部对象),当这个函数被调用结束时,对象应该释放,在对象释放前自动执行析构函数。
(2)static局部对象在函数调用结束时对象并不释放,因此也不调用析构函数,只在main函数结束或调用exit函数结束程序时,才调用static局部对象的析构函数。
(3)如果定义了一个全局对象,则在程序的流程离开其作用域时(如main函数结束或调用exit函数) 时,调用该全局对象的析构函数。
(4)如果用new运算符动态地建立了一个对象,当用delete运算符释放该对象时,先调用该对象的析构函数。
(5)调用复制构造函数后。

例:

例4.
#include <iostream>
using namespace std;
class CMyclass
{
public:
~CMyclass( )
{
cout << "destructor" << endl;
}
};
CMyclass obj;
CMyclass fun(CMyclass sobj )
{
return sobj; //函数调用返回时生成临时对象返回
}
void main( )
{
obj = fun(obj); //函数调用的返回值(临时对象)被用过后,该临时对象析构函数被调用
}

程序执行结果为:
destructor   // 形参和实参结合,会调用复制构造函数,临时对象析构
destructor   // return sobj函数调用返回,会调用复制构造函数,临时对象析构
destructor   // obj对象析构

总之,在临时对象生成的时候会有构造函数被调用,临时对象消亡导致析构函数调用。

三、构造函数和析构函数的调用情况

构造函数用于对对象中的变量赋初值,析构函数用于释放所定义的对象的所有内存空间。构造函数和析构函数都不需要用户调用的,构造函数在定义对象时自动调用,析构函数当对象的生存期结束的时候会自动的调用。一般来说,析构函数的调用顺序与构造函数相反。但对象存储类型可以改变析构函数的调用顺序。
全局范围中定义的对象的构造函数在文件中的任何其他函数(包括main)执行之前调用(但不同文件之间全局对象构造函数的执行顺序是不确定的)。全局变量是需要在进入main()函数前即初始化的,所以在一个程序中一个全局变量的构造函数应该是第一个被调用的,比main()函数都要早同时全局对象又必须在main()函数返回后才被销毁,当main终止或调用exit函数时调用相应的析构函数,所以它的析构函数是最后才被调用。
当程序执行到对象定义时,调用自动局部对象的构造函数。该对象的析构函数在对象离开范围时调用(即离开定义对象的块时)。自动对象的构造函数与析构函数在每次对象进人和离开范围时调用。
static局部对象的构造函数只在程序执行首次到达对象定义时调用一次,对应的析构函数在main终止或调用exit函数时调用。

#include <iostream>
using namespace std;
class A
{
public:
A( int value )
{
i = value;
cout << "Object "<<i<<" constructor";
}
~A() // destructor
{
cout <<"Object "<<i<<" destructor"<< endl;
}
private:
int i;
};
A first(); // global object全局变量
void func()
{
A fifth();
cout <<" (local automatic in create)"<< endl;
static A sixth( );
cout <<" (local static in create)"<< endl;
A seventh();
cout <<" (local automatic in create)"<< endl;
}
int main()
{
cout <<" (global created before main)" <<endl;
A second();
cout <<" (local automatic in main)"<< endl;
static A third(); // local object
cout <<" (local static in main)"<< endl;
func(); // call function to create objects
A fourth(); // local object
cout <<" (local automatic in main)"<< endl;
return ;
}

程序执行结果为:
Object 1 constructor (global creasted before main)
Object 2 constructor (local automatic in main)
Object 3 constructor (local static in main)
Object 5 constructor (local automatic in create)
Object 6 constructor (local static in create)
Object 7 constructor (local automatic in create)
Object 7 destructor
Object 5 destructor
Object 4 constructor (local automatic in main}
Object 4 destructor
Object 2 destructor
Object 6 destructor
Object 3 destructor
Object 1 destructor

上例中,main函数中声明3个对象,对象second和fourth是局部自动对象,对象third是static局部对象。这些对象的构造函数在程序执行到对象定义时调用。对象fourth和second的析构函数在到达main函数结尾时候依次调用。由于对象third是static局部对象,因此到程序结束时才退出,在程序终止时候删除所有其他对象之后和调用first的析构函数之前调用对象third的析构函数。函数func声明3个对象。对象fifth和seventh是局部自动对象,对象sixth是static局部对象。对象seventh和fifth的析构函数在到func结束时候依次调用。由于对象sixth是static局部对象,因此到程序结束时才退出。sixth的析构函数在程序终止时删除所有其他对象之后和调用third和first的析构函数之前调用。

若函数参数是类类型,调用函数时要调用复制构造函数,用实际参数初始化形式参数。当函数返回类类型时,也要通过复制构造函数建立临时对象。

例:

#include <iostream>
using namespace std;
class A
{
public:
A()
{
cout<<"A constructor"<<endl;
}
~A()
{
cout<<"A destructor"<<endl;
}
A(A &)
{
cout <<"A copy constructor"<<endl;
}
};
class B
{
public:
B()
{
cout<<"B constructor"<<endl;
}
~B()
{
cout<<"B destructor"<<endl;
}
B(B &)
{
cout<<"B copy constructor"<<endl;
}
};
A a;
B b;
void func1(A obj) {}
void func2(B &obj) {}
int main()
{
func1(a);
func2(b);
return ;
}

程序执行结果为:
A constructor       // a构造
B constructor       // b构造
A copy constructor   // func1函数参数调用A的复制构造函数
A destructor   // func1函数参数析构
B destructor   // b析构
A destructor   // a析构

上例中函数func1的参数是A的值传递方式,实参初始化形参时需要调用复制构造函数,函数调用结束后,栈上的形参消亡时要调用A的析构函数。函数func2的参数是B的引用传递方式,形参只是实参的一个别名,并没有创建新的对象,因此不会调用复制构造函数和析构函数。

这里再附上一道面试题:

#include <iostream>
using namespace std;
void hello( )
{
cout << " Hello, world!\n";
}
int main( )
{
hello( );
return ;
}

试修改上面的程序,使其输出变成:
 Begin
   Hello, world!
 End
限制:(1)不能对main()进行任何修改;(2)不能修改hello()函数。
这里用到了构造函数和析构函数的调用了。

#include <iostream>
using namespace std;
class A {
public:
A ( ) { cout << "Begin\n"; }
~A ( ) { cout << "End\n"; }
}; void hello( ) {cout << " Hello, world!\n"; } A a; // a是一个全局对象
int main( ) {
hello( );
return ;
}

下面总结下不同存储类型构造函数和析构函数的调用。

构造函数的调用:
(1)全局变量:程序运行前;
(2)函数中静态变量:函数开始前;
(3)函数参数:函数开始前;
(4)函数返回值:函数返回前;
析构函数的调用:
(1)全局变量:程序结束前;
(2)main中变量:main结束前;
(3)函数中静态变量:程序结束前;
(4)函数参数:函数结束前;
(5)函数中变量:函数结束前;
(6)函数返回值:函数返回值被使用后;

对于相同作用域和存储类别的对象,调用析构函数的次序正好与调用构造函数的次序相反

四、私有析构函数

有时你想这样管理某些对象,要让某种类型的对象能够自我销毁,也就是能够“delete this”。很明显这种管理方式需要此类型对象被分配在堆中。若我们必须在堆中创建对象,为了执行这种限制,你必须找到一种方法禁止以调用“new”以外的其它手段建立对象。这很容易做到,非堆对象(non-heap object),即栈对象在定义它的地方被自动构造,在生存时间结束时自动被释放,所以只要禁止使用隐式的析构函数,就可以实现这种限制。
把这些调用变得不合法的一种最直接的方法是把析构函数声明为private,把构造函数声明为public。你可以增加一个专用的伪析构函数,用来访问真正的析构函数,客户端调用伪析构函数释放他们建立的对象。

#include <iostream>
using namespace std;
class A
{
public:
A()
{
cout<<"A"<<endl;
}
void destroy() const
{
cout<<"delete A"<<endl;
delete this;
}
private:
~A() {}
};
int main( )
{
A *pa = new A;
pa->destroy();
return ;
}

程序执行结果为:
        A
        delete A
若A类对象是在栈上创建:
    A a;  // error C2248: “A::~A”: 无法访问 private 成员(在“A”类中声明)
编译时会提示不能访问私有成员。因为在栈上生成对象时,类对象在离开作用域时会调用析构函数释放空间,此时无法调用私有的析构函数。C++在编译过程中,所有的非虚函数调用都必须分析完成。即使是虚函数,也需检查可访问性。因此,当在栈上生成对象时,对象会自动析构,也就说析构函数必须可以访问。而堆上生成对象,由于析构时机由程序员控制,所以不一定需要析构函数。

被声明为私有析构函数的类对象只能在堆上创建,并且该类不能被继承。

class A
{
private:
~A() {}
};
class B : public A
{
// error C2248: “A::~A”: 无法访问 private 成员(在“A”类中声明)
} // warning C4624: “B”: 未能生成析构函数,因为基类析构函数不可访问

C++ 类 析构函数的更多相关文章

  1. C++多态性中基类析构函数声明为虚函数

    在用基类指针指向派生类时, 在基类析构函数声明为virtual的时候,delete基类指针,会先调用派生类的析构函数,再调用基类的析构函数. 在基类析构函数没有声明为virtual的时候,delete ...

  2. 【C++ Primer 第15章】定义派生类析构函数

    学习资料 • 基类和派生类析构函数执行顺序 定义派生类析构函数 [注意]定义一个对象时先调用基类的构造函数.然后调用派生类的构造函数:析构的时候恰好相反:先调用派生类的析构函数.然后调用基类的析构函数 ...

  3. (C++)浅谈多态基类析构函数声明为虚函数

    主要内容: 1.C++类继承中的构造函数和析构函数 2.C++多态性中的静态绑定和动态绑定 3.C++多态性中析构函数声明为虚函数 1.C++类继承中的构造函数和析构函数 在C++的类继承中, 建立对 ...

  4. C++类继承--基类析构函数加上Virtual

    下面的内容要说明两个问题:1. 基类的析构函数为什么要加上Virtual--防止内存泄露 1. 基类虚构函数无virtual,派生类无法析构,会导致内存泄露 #include <stdio.h& ...

  5. C++中继承 声明基类析构函数为虚函数作用,单继承和多继承关系的内存分布

    1,基类析构函数不为虚函数 #include "pch.h" #include <iostream> class CBase { public: CBase() { m ...

  6. 条款7:为多态基类析构函数声明为virtual

    基类指针指向子类对象. 子类对象必须位于堆.因此为了避免泄漏内存资源,当指针不使用时,delete掉每一个对象非常重要.但是如果基类的析构函数不声明为virtual.那么指向子类对象的指针delete ...

  7. 不要在基类析构函数中调用纯虚函数,否则运行时会报错“pure virtual method called”

    如上. 这是因为:delete派生类对象时,先调用派生类的析构函数,然后再调用基类的析构函数:此时如果调用纯虚函数的话,派生类的对象已经被破坏了,所以会报错. http://www.cnblogs.c ...

  8. 124-PHP类析构函数

    <?php class myclass{ //定义一个类 public function __destruct(){ //定义析构方法 echo '析构方法执行.<br />'; } ...

  9. c++, 派生类的构造函数和析构函数 , [ 以及operator=不能被继承 or Not的探讨]

    说明:文章中关于operator=实现的示例,从语法上是对的,但逻辑和习惯上都是错误的. 参见另一篇专门探究operator=的文章:<c++,operator=>http://www.c ...

随机推荐

  1. 在存放源程序的文件夹中建立一个子文件夹 myPackage。例如,在“D:\java”文件夹之中创建一个与包同名的子文件夹 myPackage(D:\java\myPackage)。在 myPackage 包中创建一个YMD类,该类具有计算今年的年份、可以输出一个带有年月日的字符串的功能。设计程序SY31.java,给定某人姓名和出生日期,计算该人年龄,并输出该人姓名、年龄、出生日期。程序使用YM

    题目补充: 在存放源程序的文件夹中建立一个子文件夹 myPackage.例如,在“D:\java”文件夹之中创建一个与包同名的子文件夹 myPackage(D:\java\myPackage).在 m ...

  2. YUV与RBG的装换公式

    Y’ = 0.257*R' + 0.504*G' + 0.098*B' + 16 Cb Cr R) G) - 0.392*(Cb'-128) B)

  3. webpack一小时入门

    什么是 webpack? webpack是近期最火的一款模块加载器兼打包工具,它能把各种资源,例如JS(含JSX).coffee.样式(含less/sass).图片等都作为模块来使用和处理. 我们可以 ...

  4. 如何解析json字符串及返回json数据到前端

    前言:最近需要实现的任务是:写若干个接口,并且接口中的请求数据是json格式,然后按照请求参数读取前端提前整理好的json数据,并且将json数据返回到服务器端. 主要的工具:Gson  2.8.2 ...

  5. Php5.6.31连接sqlserver 2008R2数据库问题sqlsrv(php5.3及以上版本)与mssql(php5.3以前版本)②

    Php5.6.31连接sqlserver 2008R2数据库 1.环境配置 Win7(win8.1)  64 +Apache2.4 + PHP5.6.31 + SQL Server 2008 R2数据 ...

  6. KVM虚拟机IO处理过程(二) ----QEMU/KVM I/O 处理过程

    接着KVM虚拟机IO处理过程中Guest Vm IO处理过程(http://blog.csdn.net/dashulu/article/details/16820281),本篇文章主要描述IO从gue ...

  7. iOS如何在一个包上切换正式环境和测试环境

    最近项目处于测试阶段,所以免不了每天都得打包给测试人员,由于我们公司规模比较大,项目环境也分为了三种:测试环境.预上线(预生产)环境.上线(生产)环境.所以每到了测试后期,每天打包的时间也占了不少,遇 ...

  8. c# 访问postgressql,使用pghelper访问pgsql

    由于工作需要,数据库是postgressql的,本来以为很简单的,结果弄了一晚上,为了总结经验,现将C#连接PGSQL(postgres sql)的资料整理如下. 一.总体思路 1.通过第三方Npgs ...

  9. d3 js emberjs handlerbarjs

    http://handlebarsjs.com/ http://emberjs.com/ http://jsbin.com/d3ember-barchart/13/edit?html,output

  10. SSM-CRUD入门项目——环境搭建

    一.项目概述 项目功能点: 1.分页 2.数据校验: jQuery前端校验+JSR303后端校验 3.ajax 4.RESTful风格的URI 技术点: 1.基础框架——SSM 2.数据库——MySQ ...