原因是C++是一个静态绑定的语言. 在编译过程中, 所有的非虚函数调用都必须分析完成. 即使是虚函数, 也需检查可访问性. 因些, 当在栈(stack)上生成对象时, 对象会自动析构, 也就说析构函数必须可以访问. 而堆上生成对象, 由于析构时机由程序员控制, 所以不一定需要析构函数. 保证了不能在栈上生成对象后, 需要证明能在堆上生成它. 这里OnlyHeapClass与一般对象唯一的区别在于它的析构函数为私有, delete操作会调用析构函数, 所以不能编译.
那么如何释放它呢? 答案也很简单, 提供一个成员函数, 完成delete操作. 在成员函数中, 析构函数是可以访问的, 当然detele操作也是可以编译通过.
根本原因在于delete操作符的功能和类对象的内存模型。当一个类对象声明时,系统会为其分配内存空间。在类对象的内存空间中,只有数据成员和虚函数表指针,并不包含代码内容,类的成员函数单独放在代码段中。在调用成员函数时,隐含传递一个this指针,让成员函数知道当前是哪个对象在调用它。当 调用delete this时,类对象的内存空间被释放。在delete this之后进行的其他任何函数调用,只要不涉及到this指针的内容,都能够正常运行。一旦涉及到this指针,如操作数据成员,调用虚函数等,就会出现不可预期的问题。
为什么是不可预期的问题?delete this之后不是释放了类对象的内存空间了么,那么这段内存应该已经还给系统,不再属于这个进程。照这个逻辑来看,应该发生指针错误,无访问权限之类的令系统崩溃的问题才对啊?这个问题牵涉到操作系统的内存管理策略。delete this释放了类对象的内存空间,但是内存空间却并不是马上被回收到系统中,可能是缓冲或者其他什么原因,导致这段内存空间暂时并没有被系统收回。此时这段内存是可以访问的,你可以加上100,加上200,但是其中的值却是不确定的。当你获取数据成员,可能得到的是一串很长的未初始化的随机数;访问虚函数表,指针无效的可能性非常高,造成系统崩溃。
大致明白在成员函数中调用delete this会发生什么之后,再来看看另一个问题,如果在类的析构函数中调用delete this,会发生什么?实验告诉我们,会导致堆栈溢出。原因很简单,delete的本质是“为将被释放的内存调用一个或多个析构函数,然后,释放内存” (来自effective c++)。显然,delete this会去调用本对象的析构函数,而析构函数中又调用delete this,形成无限递归,造成堆栈溢出,系统崩溃。
另外重载delete, new为私有可以达到要求对象创建于栈上的目的, 用placement new也可以创建在栈上.
上面已经提到, 你决定禁止产生某种类型的堆对象, 这时你可以自己创建一个资源封装类, 该类对象只能在栈中产生, 这样就能在异常的情况下自动释放封装的资源.
class Resource ; // 代表需要被封装的资源类
class NoHashObject
{
private:
Resource *ptr ; // 指向被封装的资源
// ... //其它数据成员
void* operator new(size_t size) //非严格实现, 仅作示意之用
{
return malloc(size);
}
void operator delete(void* pp) //非严格实现, 仅作示意之用
{
free(pp);
}
public:
NoHashObject()
{
// 此处可以获得需要封装的资源, 并让ptr指针指向该资源
ptr = new Resource();
}
~NoHashObject()
{
delete ptr; // 释放封装的资源
}
};
NoHashObject现在就是一个禁止堆对象的类了, 如果你写下如下代码:
NoHashObject* fp = new NoHashObject(); // 编译期错误!
delete fp;
上面代码会产生编译期错误. 好了, 现在你已经知道了如何设计一个禁止堆对象的类了, 你也许和我一样有这样的疑问, 难道在类NoHashObject的定义不能改变的情况下, 就一定不能产生该类型的堆对象了吗? 不, 还是有办法的, 我称之为“暴力破解法”. C++是如此地强大, 强大到你可以用它做你想做的任何事情. 这里主要用到的是技巧是指针类型的强制转换.
int main()
{
char* temp = new char[sizeof(NoHashObject)] ;
//强制类型转换, 现在ptr是一个指向NoHashObject对象的指针
NoHashObject* obj_ptr = (NoHashObject*)temp ;
temp = NULL ; //防止通过temp指针修改NoHashObject对象
//再一次强制类型转换, 让rp指针指向堆中NoHashObject对象的ptr成员
Resource* rp = (Resource*)obj_ptr ;
//初始化obj_ptr指向的NoHashObject对象的ptr成员
rp = new Resource() ;
//现在可以通过使用obj_ptr指针使用堆中的NoHashObject对象成员了
... ...
delete rp ;//释放资源
temp = (char*)obj_ptr ;
obj_ptr = NULL ;//防止悬挂指针产生
delete [] temp ;//释放NoHashObject对象所占的堆空间.
return 0;
}
某块内存中的数据是不变的, 而类型就是我们戴上的眼镜, 当我们戴上一种眼镜后, 我们就会用对应的类型来解释内存中的数据, 这样不同的解释就得到了不同的信息.
所谓强制类型转换实际上就是换上另一副眼镜后再来看同样的那块内存数据.
另外要提醒的是, 不同的编译器对对象的成员数据的布局安排可能是不一样的, 比如, 大多数编译器将NoHashObject的ptr指针成员安排在对象空间的头4个字节, 这样才会保证下面这条语句的转换动作像我们预期的那样执行:
Resource* rp = (Resource*)obj_ptr ;
但是, 并不一定所有的编译器都是如此.
既然我们可以禁止产生某种类型的堆对象, 那么可以设计一个类, 使之不能产生栈对象吗? 当然可以.
五.禁止产生栈对象
前面已经提到了, 创建栈对象时会移动栈顶指针以“挪出”适当大小的空间, 然后在这个空间上直接调用对应的构造函数以形成一个栈对象, 而当函数返回时, 会调用其析构函数释放这个对象, 然后再调整栈顶指针收回那块栈内存. 在这个过程中是不需要operator new/delete操作的, 所以将operator new/delete设置为private不能达到目的. 当然从上面的叙述中, 你也许已经想到了: 将构造函数或析构函数设为私有的, 这样系统就不能调用构造/析构函数了, 当然就不能在栈中生成对象了.
这样的确可以, 而且我也打算采用这种方案. 但是在此之前, 有一点需要考虑清楚,那就是, 如果我们将构造函数设置为私有, 那么我们也就不能用new来直接产生堆对象了, 因为new在为对象分配空间后也会调用它的构造函数啊. 所以, 我打算只将析构函数设置为private. 再进一步, 将析构函数设为private除了会限制栈对象生成外, 还有其它影响吗? 是的, 这还会限制继承.
如果一个类不打算作为基类, 通常采用的方案就是将其析构函数声明为private.
为了限制栈对象, 却不限制继承, 我们可以将析构函数声明为protected, 这样就两全其美了. 如下代码所示:
class NoStackObject
{
protected:
~NoStackObject() { }
public:
void destroy()
{
delete this ;//调用保护析构函数
}
};
接着, 可以像这样使用NoStackObject类:
NoStackObject* hash_ptr = new NoStackObject() ;
... ... //对hash_ptr指向的对象进行操作
hash_ptr->destroy() ;
呵呵, 是不是觉得有点怪怪的, 我们用new创建一个对象, 却不是用delete去删除它, 而是要用destroy方法. 很显然, 用户是不习惯这种怪异的使用方式的. 所以, 我决定将构造函数也设为private或protected. 这又回到了上面曾试图避免的问题, 即不用new, 那么该用什么方式来生成一个对象了? 我们可以用间接的办法完成, 即让这个类提供一个static成员函数专门用于产生该类型的堆对象. (设计模式中的singleton模式就可以用这种方式实现. )让我们来看看:
class NoStackObject
{
protected:
NoStackObject() { }
~NoStackObject() { }
public:
static NoStackObject* creatInstance()
{
return new NoStackObject() ;//调用保护的构造函数
}
void destroy()
{
delete this ;//调用保护的析构函数
}
};
现在可以这样使用NoStackObject类了:
NoStackObject* hash_ptr = NoStackObject::creatInstance() ;
... ... //对hash_ptr指向的对象进行操作
hash_ptr->destroy() ;
hash_ptr = NULL ; //防止使用悬挂指针
现在感觉是不是好多了, 生成对象和释放对象的操作一致了.