您的位置:首页 > 其它

要求或禁止在堆中产生对象

2010-04-08 21:26 260 查看
http://blog.chinaunix.net/u2/84425/showart_2037715.html

1.1要求或禁止在堆中产生对象

有时你想这样管理某些对象,要让某种类型的对象能够自我销毁,也就是能够“deletethis”。很明显这种管理方式需要此类型对象被分配在堆中。而其它一些时候你想获得一种保障:“不在堆中分配对象,从而保证某种类型的类不会发生内存泄漏。”如果你在嵌入式系统上工作,就有可能遇到这种情况,发生在嵌入式系统上的内存泄漏是极其严重的,其堆空间是非常珍贵的。有没有可能编写出代码来要求或禁止在堆中产生对象(heap-basedobject)呢?通常是可以的,不过这种代码也会把“ontheheap”的概念搞得比你脑海中所想的要模糊。

l要求在堆中建立对象
让我们先从必须在堆中建立对象开始说起。为了执行这种限制,你必须找到一种方法禁止以调用“new”以外的其它手段建立对象。这很容易做到。非堆对象(non-heapobject)在定义它的地方被自动构造,在生存时间结束时自动被释放,所以只要禁止使用隐式的构造函数和析构函数,就可以实现这种限制。
把这些调用变得不合法的一种最直接的方法是把构造函数和析构函数声明为private。这样做副作用太大。没有理由让这两个函数都是private。最好让析构函数成为private,让构造函数成为public。处理过程与条款26相似,你可以引进一个专用的伪析构函数,用来访问真正的析构函数。客户端调用伪析构函数释放他们建立的对象。(WQ加注:注意,异常处理体系要求所有在栈中的对象的析构函数必须申明为公有!)
例如,如果我们想仅仅在堆中建立代表unlimitedprecisionnumbers(无限精确度数字)的对象,可以这样做:
classUPNumber{
public:
UPNumber();
UPNumber(intinitValue);
UPNumber(doubleinitValue);
UPNumber(constUPNumber&rhs);
//伪析构函数(一个const成员函数,因为
//即使是const对象也能被释放。)
voiddestroy()const{deletethis;}
...
private:
~UPNumber();
};
然后客户端这样进行程序设计:
UPNumbern;//错误!(在这里合法,但是
//当它的析构函数被隐式地
//调用时,就不合法了)
UPNumber*p=newUPNumber;//正确
...
deletep;//错误!试图调用
//private析构函数
p->destroy();//正确
另一种方法是把全部的构造函数都声明为private。这种方法的缺点是一个类经常有许多构造函数,类的作者必须记住把它们都声明为private。否则如果这些函数就会由编译器生成,构造函数包括拷贝构造函数,也包括缺省构造函数;编译器生成的函数总是public(参见EffecitveC++条款45)。因此仅仅声明析构函数为private是很简单的,因为每个类只有一个析构函数。
通过限制访问一个类的析构函数或它的构造函数来阻止建立非堆对象,但是在条款26已经说过,这种方法也禁止了继承和包容(containment):
classUPNumber{...};//声明析构函数或构造函数
//为private
classNonNegativeUPNumber:
publicUPNumber{...};//错误!析构函数或
//构造函数不能编译
classAsset{
private:
UPNumbervalue;
...//错误!析构函数或
//构造函数不能编译
};
这些困难不是不能克服的。通过把
UPNumber
的析构函数声明为protected(同时它的构造函数还保持public)就可以解决继承的问题,需要包含
UPNumber
对象的类可以修改为包含指向
UPNumber
的指针:
classUPNumber{...};//声明析构函数为protected
classNonNegativeUPNumber:
publicUPNumber{...};//现在正确了;派生类
//能够访问
//protected成员
classAsset{
public:
Asset(intinitValue);
~Asset();
...
private:
UPNumber*value;
};
Asset::Asset(intinitValue)
:value(newUPNumber(initValue))//正确
{...}
Asset::~Asset()
{value->destroy();}//也正确
l判断一个对象是否在堆中
如果我们采取这种方法,我们必须重新审视一下“在堆中”这句话的含义。上述粗略的类定义表明一个非堆的
NonNegativeUPNumber
对象是合法的:

NonNegativeUPNumbern;//正确
那么现在
NonNegativeUPNumber
对象
n
中的
UPNumber
部分也不在堆中,这样说对么?答案要依据类的设计和实现的细节而定,但是让我们假设这样说是不对的,所有
UPNumber
对象
—即使是做为其它派生类的基类—也必须在堆中。我们如何能强制执行这种约束呢?

没有简单的办法。
UPNumber
的构造函数不可能判断出它是否做为堆对象的基类而被调用。也就是说对于
UPNumber
的构造函数来说没有办法侦测到下面两种环境的区别:

NonNegativeUPNumber*n1=
newNonNegativeUPNumber;//在堆中
NonNegativeUPNumbern2;//不再堆中
不过你可能不相信我。也许你想你能够在new操作符、operatornew和new操作符调用的构造函数的相互作用中玩些小把戏(参见条款M8)。可能你认为你比他们都聪明,可以这样修改UPNumber,如下所示:
classUPNumber{
public:
//如果建立一个非堆对象,抛出一个异常
classHeapConstraintViolation{};
staticvoid*operatornew(size_tsize);
UPNumber();
...
private:
staticboolonTheHeap;//在构造函数内,指示
//对象是否被构造在
...//堆上
};
//obligatorydefinitionofclassstatic
boolUPNumber::onTheHeap=false;
void*UPNumber::operatornew(size_tsize)
{
onTheHeap=true;
return::operatornew(size);
}
UPNumber::UPNumber()
{
if(!onTheHeap){
throwHeapConstraintViolation();
}
proceedwithnormalconstructionhere;
onTheHeap=false;//为下一个对象清除标记
}
如果不再深入研究下去,就不会发现什么错误。这种方法利用了这样一个事实:“当在堆上分配对象时,会调用operatornew来分配rawmemory”,operatornew设置onTheHeap为true,每个构造函数都会检测onTheHeap,看对象的rawmemory是否被operatornew所分配。如果没有,一个类型为
HeapConstraintViolation
的异常将被抛出。否则构造函数如通常那样继续运行,当构造函数结束时,
onTheHeap
被设置为
false
,然后为构造下一个对象而重置到缺省值。

这是一个非常好的方法,但是不能运行。请考虑一下这种可能的客户端代码:

UPNumber*numberArray=newUPNumber[100];
第一个问题是为数组分配内存的是operatornew[],而不是operatornew,不过(倘若你的编译器支持它)你能象编写operatornew一样容易地编写operatornew[]函数。更大的问题是numberArray有100个元素,所以会调用100次构造函数。但是只有一次分配内存的调用,所以100个构造函数中只有第一次调用构造函数前把onTheHeap设置为true。当调用第二个构造函数时,会抛出一个异常,你真倒霉。
即使不用数组,bit-setting操作也会失败。考虑这条语句:
UPNumber*pn=newUPNumber(*newUPNumber);
这里我们在堆中建立两个UPNumber,让pn指向其中一个对象;这个对象用另一个对象的值进行初始化。这个代码有一个内存泄漏,我们先忽略这个泄漏,这有利于下面对这条表达式的测试,执行它时会发生什么事情:
newUPNumber(*newUPNumber)
它包含new操作符的两次调用,因此要调用两次operatornew和调用两次UPNumber构造函数(参见条款8)。程序员一般期望这些函数以如下顺序执行:
调用第一个对象的operatornew
调用第一个对象的构造函数
调用第二个对象的operatornew
调用第二个对象的构造函数
但是C++语言没有保证这就是它调用的顺序。一些编译器以如下这种顺序生成函数调用:
调用第一个对象的operatornew
调用第二个对象的operatornew
调用第一个对象的构造函数
调用第二个对象的构造函数
编译器生成这种代码丝毫没有错,但是在operatornew中set-a-bit的技巧无法与这种编译器一起使用。因为在第一步和第二步设置的bit,第三步中被清除,那么在第四步调用对象的构造函数时,就会认为对象不再堆中,即使它确实在。
这些困难没有否定让每个构造函数检测*this指针是否在堆中这个方法的核心思想,它们只是表明检测在operatornew(或operatornew[])里的bitset不是一个可靠的判断方法。我们需要更好的方法进行判断。
如果你陷入了极度绝望当中,你可能会沦落进不可移植的领域里。例如你决定利用一个在很多系统上存在的事实,程序的地址空间被做为线性地址管理,程序的栈从地址空间的顶部向下扩展,堆则从底部向上扩展:

在以这种方法管理程序内存的系统里(很多系统都是,但是也有很多不是这样),你可能会想能够使用下面这个函数来判断某个特定的地址是否在堆中:
//不正确的尝试,来判断一个地址是否在堆中
boolonHeap(constvoid*address)
{
charonTheStack;//局部栈变量
returnaddress<&onTheStack;
}
这个函数背后的思想很有趣。在onHeap函数中onTheSatck是一个局部变量。因此它在堆栈上。当调用onHeap时,它的栈框架(stackframe)(也就是它的activationrecord)被放在程序栈的顶端,因为栈在结构上是向下扩展的(趋向低地址),onTheStack的地址肯定比任何栈中的变量或对象的地址小。如果参数address的地址小于onTheStack的地址,它就不会在栈上,而是肯定在堆上。
到目前为止,这种逻辑很正确,但是不够深入。最根本的问题是对象可以被分配在三个地方,而不是两个。是的,栈和堆能够容纳对象,但是我们忘了静态对象。静态对象是那些在程序运行时仅能初始化一次的对象。静态对象不仅仅包括显示地声明为static的对象,也包括在全局和命名空间里的对象(参见条款47)。这些对象肯定位于某些地方,而这些地方既不是栈也不是堆。
它们的位置是依据系统而定的,但是在很多栈和堆相向扩展的系统里,它们位于堆的底端。先前内存管理的图片到讲述的是事实,而且是很多系统都具有的事实,但是没有告诉我们这些系统全部的事实,加上静态变量后,这幅图片如下所示:

onHeap不能工作的原因立刻变得很清楚了,不能辨别堆对象与静态对象的区别:
voidallocateSomeObjects()
{
char*pc=newchar;//堆对象:onHeap(pc)
//将返回true
charc;//栈对象:onHeap(&c)
//将返回false
staticcharsc;//静态对象:onHeap(&sc)
//将返回true
...
}
现在你可能不顾一切地寻找区分堆对象与栈对象的方法,在走头无路时你想在可移植性上打主意,但是你会这么孤注一掷地进行一个不能获得正确结果的交易么?绝对不会。我知道你会拒绝使用这种虽然诱人但是不可靠的“地址比对”技巧。
令人伤心的是不仅没有一种可移植的方法来判断对象是否在堆上,而且连能在多数时间正常工作的“准可移植”的方法也没有。如果你实在非得必须判断一个地址是否在堆上,你必须使用完全不可移植的方法,其实现依赖于系统调用,只能这样做了。因此你最好重新设计你的软件,以便你可以不需要判断对象是否在堆中。
如果你发现自己实在为对象是否在堆中这个问题所困扰,一个可能的原因是你想知道对象是否能在其上安全调用delete。这种删除经常采用“deletethis”这种声明狼籍的形式。不过知道“是否能安全删除一个指针”与“只简单地知道一个指针是否指向堆中的事物”不一样,因为不是所有在堆中的事物都能被安全地delete。再考虑包含UPNumber对象的Asset对象:
classAsset{

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: