C++拓展笔记2-1:C++析构函数学习心得

一、构造函数和析构函数的调用和析构顺序:

一般来说,析构函数的析构顺序为构造函数调用顺序的逆序,在程序离开对象所在域时被调用。然而,需要注意的特殊情况有以下几类:

  1. 对于全局对象(constructors and destructors for global scope)来说,构造函数的调用在主方法(main函数)之前。对于跨文件全局对象的构造函数调用,其顺序是不确定的。因此,全局对象的析构顺序也是不确定的。
  2. 有关在析构函数之前调用exit函数或abort函数的情况:若调用了exit函数,程序将会立即停止执行,并且所有本地对象(包括方法调用中的调用方法(caller)和被调用的方法(callee))的析构函数都将不被执行,而全局对象的析构函数还是会被执行的(exit函数通常用来结束不可恢复性严重程序错误)。当调用abort函数时,程序立即被停止执行,任何未执行过的析构函数也将不会被执行(abort函数往往用来程序不正常终止的情形)。
  3. 对于暂存( 被static关键字修饰过的对象)本地对象,构造函数仅在其被初始化分配内存时被调用一次。它们的析构函数是在main函数结束时或exit函数被调用时执行的。对于暂存全局对象来说,其析构顺序符合一般规律。

为说明这三条规则,下面举个例子(程序改编自《C++11程序设计》(英文版)第二版 p.g.274)

头文件:

#ifndef CREATEANDDESTROY_H
#define CREATEANDDESTROY_H

#include <string>
using namespace std;

class DemoObject 
{
public:
   DemoObject( int, string ); // constructor
   ~DemoObject(); // destructor
private:
   int objectID; // ID number for object
   string message; // message describing object
}; // end class CreateAndDestroy

#endif

源文件:

#include <iostream>
#include "CreateAndDestroy.h" // include CreateAndDestroy class definition
using namespace std;

// constructor sets object's ID number and descriptive message
DemoObject::DemoObject( int ID, string messageString )
   : objectID( ID ), message( messageString )
{
   cout << "Object " << objectID << "   constructor runs   " 
      << message << endl;
} // end CreateAndDestroy constructor

// destructor
DemoObject::~DemoObject()
{ 
   // output newline for certain objects; helps readability
   cout << ( objectID == 3 || objectID == 5 ? "\nStatic object" : "" );

   cout << "Object " << objectID << "   destructor runs    " 
      << message << endl; 
} // end ~CreateAndDestroy destructor

主方法源文件:

#include <cstdlib>
#include <iostream>
#include "CreateAndDestroy.h" // include CreateAndDestroy class definition
using namespace std;

DemoObject globalObj( 1, "Global object before main function created." ); // global object

void create( void ); // prototype

int main()
{
   cout << "\nMAIN FUNCTION: EXECUTION BEGINS" << endl;
   DemoObject mainObj1( 2, "Local object in main function created. " );
   static DemoObject mainStaticObj( 3, "Local static object in main function created." );
   cout << "\nMAIN FUNCTION: EXECUTION PAUSES" << endl;
   
   create(); // call function to create objects

#if 0
   /*If placed here, the destruct order is than 3, 5, 1.
    Proving that the destruct order of static objects depends on the creating order
    and has nothing to do with function calling. */

   static DemoObject mainStaticObj( 3, "Local static object in main function created." );
#endif

   cout << "\nMAIN FUNCTION: EXECUTION RESUMES" << endl;
   DemoObject mainObj2( 8, "The second local object in main function created." );   
   cout << "\nMAIN FUNCTION: EXECUTION ENDS" << endl;

#if 0
   //If have this sentence, both destructor 8 and 2 will not run. 
   exit(0);
#endif
   
   return 0;
} // end main

// function to create objects
void create( void )
{
   cout << "\nCREATE FUNCTION: EXECUTION BEGINS" << endl;
   DemoObject methodObj( 4, "Local object in called method created." );
   static DemoObject methodStaticObj( 5, "Local static object in called method created." );
   DemoObject methodObj2( 6, "The second local object in called method created" );
   cout << "CREATE FUNCTION: EXECUTION ENDS\n" << endl;
   
   DemoObject methodObj3( 7, "The second local object in called method created" );
   
#if 0
   //If have this sentence, destructor 7, 6, 4 and 2 will not run, constructor 8 will not be created at all.
   exit(0);
#endif
   
   cout << "CREATE FUNCTION: EXECUTION ENDS\n" << endl;
} // end function create

备注:其中预处理指令如#if 0等,请参考文章:https://my.oschina.net/SamYjy/blog/827660

以上代码中,构造函数被调用的顺序与对象的ID是一致的,那么我们分析它们的析构顺序。由于程序最先离开create中对象的所在域,根据前述一般规则,ID为7,6,4的对象最先被析构。注意,如果在7后面运行exit,则6, 4和2对象由于不是暂存对象,因此不会被析构(规则2和3,注意此时8对象尚未创建)。之后被调用方法执行完毕,回到主方法。继续按照一般规则,之后被析构的对象是8和2,到此为止,所有本地对象都被析构完毕。接下来,静态变量和全局变量的析构顺序就很好判断了。本例中,由于它们的被调用顺序是1, 3和5,因此按照一般规则,析构的顺序即为5, 3和1。

二、析构函数与动态内存分配处理:

C++允许编程者控制动态内存的分配(allocate)与释放(deallocate),其关键字分别为new和delete。其中,非动态分配的内存,如基本变量等因为没有用new关键字获得内存,也因此自然不需要用delete关键字释放内存。除了一些程序运行中需要重新分配内存的情形之外,内存的释放一般都在析构函数中实现。析构函数中动态内存的析构方式是与其析构建方式是对应的,有以下两种形式:

delete ptr;
delete [] arrayPtr;

这两种释放内存方式的区别在于第一种解除的是类、基本数据类型等未使用内置数组形成的内存,第二种用来释放内置数组造成的内存。举例如下:

分配非内置数组内存的方式举例:

double *ptr = new double(3.14159);
Time *tPtr = new Time(12, 45, 0);

对应释放方式:

delete ptr;
delete tPtr;

分配内置数组内存的方式举例:

int * gradesArray = new int[10]();

对应释放方式:

delete [] gradesArray;

需要注意的是,用delete而不是delete[]删除内置数组分配的内存容易导致的现象是析构函数只析构了数组中第一个元素,从而造成程序潜在的问题。反之,用delete[] 析构非内置数组分配的内存也会造成未定义动态内存操作行为,对于程序来说可能导致致命的问题(其他内存区域数据被误删)。C++11引入nullptr变量之后,不论是否内置型数组,在内存释放之前,均可设置指针指向nullptr,这样该指针就无权限读写之前内存中的数据了。对于nullptr来说,delete和delete[]效果也是一样的,因此可以避免一些潜在错误。

三、继承机制中的析构函数调用

在继承关系中,父类的构造函数最后被调用,子类的在其之前被调用。它们的析构顺序符合一般析构规则,即最后调用最先被析构。具体来说,父类的析构函数最先被执行,子类的析构函数在其之后被执行。以此类推,直到所有对象都被从内存中清除。

需要注意的是,父类的构造函数、析构函数以及操作符重载是不被子类自动继承的。然而子类中可以调用父类的这些元素。

(本文未完待续中。。。)

参考资料:《C++11程序设计(英文版)》第二版

转载于:https://my.oschina.net/Samyan/blog/828146

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值