要把对象分配到栈上,需要使用到new operator,而new operator会调用operator new和placement new。
根据侯捷先生的书所说,STL(书中的版本)对于某些对象做了统一的分配和统一的构造,而不把这两个步骤通过直接调用new operator合二为一。
实际上,placement new也有其他作用,例如可以将一个对象构造在已知的栈空间,或者全局静态区空间上。(当然那样就不能对那个对象调用delete啦,会core dump的),代码就像这样:
char buf[100]{};
new (buf) HeapOnly();
HeapOnly *h = static_cast<HeapOnly *>((void*)buf);
delete h; //!!! crash
可见C++给了程序员足够多的自由。
一个很自然的想法是,既然只能在堆上,那么分配的方法就只有new了,那把构造函数private掉就可以了。事实上这是不对的,因为如上文所讲,new operator会隐式调用placement new,placement new会隐式调用构造函数,造成了在类外访问构造函数,所以不能把构造函数设置为private。如果这么干,只要在类外,堆和栈上都分配不了。
解决方案如下:
class HeapOnly
{
public:
HeapOnly() = default;
void Destroy(){delete this;} // 必须要提供
private: // none protected!
~HeapOnly() = default;
};
那么为什么要把析构函数声明为private呢?这里是两个问题,依次解决。
为什么要在析构函数上动手脚?根据前文,把构造函数private掉是不行了,但是如果对析构函数这么做,就会形成能在栈上分配,但是却无法在函数结束时释放的情况,编译器会在编译期发现这种错误,报出编译错误。
但是请注意,这样做的话,就无法显式的delete掉HeapOnly及其派生类了。和new operator类似,delete operator也会先调用对象的析构函数再释放内存,这样又造成了在类外访问private成员的情况,所以必须要提供一个销毁该对象的接口Destroy()。
为什么是声明为private而不是protected呢?众所周知,protected的意思是子类可见,private是只有自己可见。写这样一个HeapOnly的类的意义类似于boost::noncopyable,要让所有继承该类的子类都只能被分配到堆上。
Call to implicitly-deleted default constructor of ‘Derive‘
可见,编译器认为既然Derive无法调用基类的析构函数,那就认为Derive的析构函数也是隐式的被删除了吧。所以Derive就无法在栈上分配了。
题外话:什么时候析构函数应该是virtual的?就算析构函数不是virtual的,显式的删除一个栈上的派生对象,也会根据继承层次一路从派生类析构到基类。但是如果是根据一个基类指针删除堆上的派生对象,如果析构函数不是virtual的,那么派生类的析构函数就不会被调用……一句话总结,就是:这样做是为了当用一个基类的指针删除一个派生类的对象时,派生类的析构函数会被调用。
这个就比只能在堆上分配要简单一些,只需要把类中operator new[]和operator new[]声明为protected就可以了。
class NoHeap
{
protected:
static void *operator new(std::size_t);
static void *operator new [] (std::size_t);
};
需要注意的有两点:
首先需要明确的是,前面所讲的都是在编译期对于程序员使用一个类型的方法的约束#,当然在运行期是没办法做这个约束了(毕竟程序都跑起来了),不过有没有办法判断一个对象到底是在堆上还是在栈上呢?可以试试下面这个类:
class Detect {
public:
Detect()
{
int i;
Check(&i);
}
void Check(int *i) {
int j;
if ((i < &j) == ((void*)this < (void*)&j))
{
std::cout << "Stack" << std::endl;
return;
}
else
{
std::cout << "Heap" << std::endl;
return;
}
}
};
在Detect的构造函数中分配一个局部变量i,再在调用的Check函数中分配局部变量j,如果Detect在栈上分配,那么典型的情况应该像这样:
this ----> Detect() ---> int i ---> Check() ---> int j
高地址------------------------------------------>低地址
当然堆栈也有可能是从低地址向高地址生长的,视机器而定。Check里通过判断&i/&j/this三者的相对位置,使得这两种情况都可以应对。当然并不保证所有机器都适用,在我的x86_64电脑上测试通过。
原文:https://www.cnblogs.com/jo3yzhu/p/12558449.html