堆区(heap) —— 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
栈区(stack)—— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。如果对象被建立在堆上,系统就不会自动调用。
所以,如果我们在析构函数中有清除堆数据的语句,调用两次意味着第二次会试图清理已经被清理过了的,根本不再存在的数据!这是件会导致运行时错误的问题,并且在编译的时候不会告诉你!
调用析构函数:
撤销类对象时会自动调用析构函数,如变量超出作用域时会自动撤销。动态分配的对象只有在指向该对象的指针被删除时才撤销,如果没有删除指向动态对象的指针,则不会运行该对象的析构函数,对象就一直存在,导致内存泄漏,而且,对象内部使用的任何资源也不会释放。
显式的调用析构函数是一件非常危险的事情,,我们自己所谓的显式调用析构函数,实际上只是调用了一个成员函数,并没有真正意义上的让对象“析构”。
1 Fred *p=new Fred();
2 delete p;//自动调用p->~Fred
但是如果我们将上一条语句改为:p->~Fred();呢。会出现什么情况呢?因为显示调用析构函数不会释放Fred对象本身的内存,也就是栈内存,所以不要这么做,记住delete做了2件事情:调用析构函数和回收内存。
编译器隐式调用析构函数,如分配了堆内存,显式调用析构的话引起重复释放堆内存的异常。把一个对象看作占用了部分栈内存,占用了部分堆内存(如果申请了的话),这样便于理解这个问题(显示调用的时候,系统还是会自动执行一遍,那么总共释放了两次heap数据,释放了一次stack数据)
系统隐式调用析构函数的时候,会加入释放栈内存的动作(而堆内存则由用户手工的释放)
用户显式调用析构函数的时候,只是单纯执行析构函数内的语句,不会释放栈内存,摧毁对象
1 class aaa 2 { 3 public: 4 aaa(){p = new char[1024];} 5 ~aaa(){cout<<"deconstructor"<<endl; delete []p; p=NULL;} 6 void disp(){cout<<"disp"<<endl;} 7 private: 8 char *p; 9 }; 10 void main() 11 { 12 aaa a; 13 a.~aaa(); 14 a.disp(); 15 }
这样的话,第一次显式调用析构函数,相当于调用一个普通成员函数,执行函数语句,释放了堆内存,但是并未释放栈内存,对象还存在(但已残缺,存在不安全因素);
第二次调用析构函数,再次释放堆内存(此时报异常),然后释放栈内存,对象销毁
撤销一个容器(不管是标准库容器还是内置数组),会运行容器中的类类型元素的析构函数。注意容器中的元素总是逆序撤销的,先撤销下标为size()-1的元素……最后撤销下标为0的元素
编写显示析构函数
许多类不需要显式析构函数,尤其具有构造函数的类不一定需要定义自己的析构函数。仅在有些工作需要析构函数完成时,才需要析构函数(显式的)。析构函数并不仅限于用来释放资源,一般而言,析构函数可以执行任意操作,该操作是类设计者希望该类对象在使用完毕后执行的。
三法则(rule of three):如果类需要析构函数,那么它也需要赋值操作符和复制构造函数,即需要析构函数,则需要这三个复制控制成员。
析构函数是一个成员函数,在类名前加一个代字号(~),它没有返回值,没有形参。因为不能指定形参,所以不能重载析构函数。所以只能为一个类提供一个析构函数。
合成析构函数:
与其他两个复制控制函数不同,编译器总是会合成一个析构函数(不论我们是否显示定义了一个析构函数),合成析构函数创建时逆序地撤销每一个非static成员(stack上面的成员)。析构函数和其他两个复制控制函数最大的不同是:即使我们编写了自己的析构函数,合成析构函数仍然运行。