《认清C++语言》---堆对象

本文介绍如何通过修改构造函数和析构函数的访问级别,限制对象只能在堆上创建,以及如何判断一个对象是否位于堆中。同时探讨了禁止堆分配的方法。

强制在堆中建立对象:

有时候,我们要求某种类型的对象能够自我销毁,也就是能够“delelte this”,因此,我们需要将此类型的对象分配在堆上。由于非堆对象在定义它的地方被自动构造,在生存期结束时被自动释放,因此我们只要禁止使用隐式的构造函数和析构函数就可以限制对象在堆上分配内存。

最直接的方法是将构造函数和析构函数都声明为private,但这样副作用太大。因此,最好将析构函数置为private,构造函数置为public(这样可以实现限制是因为C++的异常处理体系要求所有在栈中的对象的析构函数必须申明为public的!)。同时引入一个专用的伪析构函数,用来访问真正的析构函数。

建立对象如下:

class ASCENumber

{

public:

ASCENumber();

ASCENumber(int iValue);

ASCENumber(double dValue);

ASCENumber(const ASCENumber& other);

//伪析构函数,const成员函数

void destroy() const {delete this;}

...

private:

~ASCENumber();

};

调用如下:

ASCENumber num; //错误,虽然在这里是合法的,但是当num的析构函数被隐式调用时,会出错!

ASCENumber *pnum = new ASCENumber; //正确

...

delete pnum; //错误,试图调用private析构函数

pnum->destroy(); //正确

上面说到将全部构造函数都声明为private的副作用体现在:一个类经常有很多构造函数,我们必须记住把它们声明为private,否则,编译器会默认生成它们,如拷贝构造函数、缺省构造函数,而且是public的。

通过限制访问一个类的析构函数或构造函数来阻止建立非堆对象,还有一个缺点是这种方法同时也禁止了类的继承。

class ASCENumber {...}; //声明析构函数或构造函数为private

class LittleAsceNumber :

public ASCENumber {...}; //错误,析构函数或构造函数不能编译

class Asset

{

private:

ASCENumber value;

... //错误,析构函数或构造函数不能编译

};

为了解决继承问题,我们可以把ASCENumber的析构函数声明为protected,同时保持构造函数为public。而需要包含ASCENumber对象的类可以修改为包含指向ASCENumber的指针:

class ASCENumber {...}; //声明析构函数为protected

class LittleAsceNumber :

public ASCENumber {...}; //正确,派生类能够访问protected成员

class Asset

{

public:

Asset(int iValue);

~Asset();

...

private:

ASCENumber *value; //包含指针,而不是对象

};

Asset::Asset(int iValue) : value(new ASCENumber(iValue)) //正确

{

...

}

Asset::~Asset()

{

value->destroy(); //正确

}

判断一个对象是否在堆中:

上面的ASCENumber的构造函数不能判断出它是否作为堆对象的基类而被调用,即无法检测下面两种环境的区别:

ASCENumber *pasce = new ASCENumber ; //在堆中

ASCENumber asce; //不在堆中

方案一:很多系统上程序的地址空间被作为线性地址管理,程序的栈从地址空间的顶部向下扩展,堆则从底部向上扩展。利用这个事实,我们可以使用下面的函数来判断某个特定的地址是否在堆中:

//不可移植,且没考虑静态变量!!来判断一个地址是否在堆中

bool OnHeap(const void *address)

{

char onTheStack; //局部栈变量

return address < &onTheStack;

}

上面函数最根本的问题是对象被分配在三个不同地方,而不是两个。除了堆和栈外,还有静态存储区。静态对象是那些在程序运行时仅能初始化一次的对象,包括static对象、全局和命名空间里的对象,它们都放在静态存储区中。且这个区域是根据系统而定的,在很多栈和堆相向扩展的系统里,静态存储区位于堆的底端。因此,当考虑静态对象时,上面的OnHeap函数就不能辨别堆对象和静态对象了!!:

void allocateObjects()

{

char *pc = new char; //堆对象,OnHeap(pc)将返回true

char c; //栈对象,OnHeap(&c)将返回false

static char sc; //静态对象,OnHeap(&sc)将返回true

}

方案二:“判断是否能够删除一个指针”比“判断一个指针指向的事物是否在堆上”要简单。这里为了能够不污染全局命名空间,且没有额外的开销,没有正确性问题,我们使用抽象混合基类来给派生类提供判断指针指向的内存是否由operator new分配的能力,该类的声明如下:

class ASCEHeapTracked //混合类,跟踪从operator new返回的ptr

{

public:

class MissingAddress{}; //异常类

virtual ~ASCEHeapTracked() = 0; //虚析构函数,是ASCEHeapTracked成为抽象类

static void *operator new(size_t size);

static void operator delete(void *ptr);

bool IsOnHeap() const;

private:

typedef const void* RawAddress;

static std::list<RawAddress> addresses;

};

这个类使用list数据结构来跟踪从operator new返回的所有指针。operator new函数分配内存并将地址加入到list中;operator delete用来释放内存并从list中移去地址元素;IsOnHeap函数判断一个对象的地址是否在list中。ASCEHeapTracked的实现如下:

//静态成员变量的定义(必须)

std::list<RawAddress> ASCEHeapTracked::addresses;

//析构函数是纯虚函数,使得该类变成抽象类

//析构函数必须被定义

ASCEHeapTracked::~ASCEHeapTracked(){}

void *ASCEHeapTracked::operator new(size_t size)

{

void *ascePtr = ::operator new(size); //全局operator new,获得内存

addresses.push_front(ascePtr); //把地址放到list的前端

return ascePtr;

}

void ASCEHeapTracked::operator delete(void *ptr)

{

//得到iterator

std::list<RawAddress>::iterator it =

find(addresses.begin(), addresses.end(), ptr);

if(it != addresses.end()) //如果发现一个元素

{

addresses.erase(it); //删除它

::operator delete(ptr); //释放内存

}

else //ptr不是operator new分配的,所以抛出一个异常

{

throw MissingAddress();

}

}

bool ASCEHeapTracked::IsOnHeap() const

{

//得到一个指针,指向*this占据的内存空间的起始处

//也就是指向当前对象其实地址的指针(由ASCEHeapTracked::operator new返回的)

const void *rawAddress = dynamic_cast<const void*>(this);

//operator new返回的地址list中查到指针

list<RawAddress>::iterator it =

find(addresses.begin(), addresses.end(), rawAddress);

return it != addresses.end(); //返回it是否被找到

}

因此,以后想要在类中加入跟踪堆中指针的功能,只需要继承ASCEHeapTracked就行:

class Asset : public ASCEHeapTracked

{

private:

ASCENumber value;

...

};

//我们可以这样查询Asset*指针

void checkAsset(const Asset *pa)

{

if(pa->IsOnHeap())

{

//pa建立在堆上的对象

}

else

{

//pa不是建立在堆上的

}

}

禁止堆分配:

1)通常,指明一些特定类的对象不应该被分配到堆(heap)上是个好主意。通常这是为了确保该对象的析构函数一定会得到调用。因为堆分配的对象必须被显示地销毁,忘记了的话就造成内存泄漏了!通常对象的建立有这样三种情况:对象被直接实例化;对象作为派生类的基类被实例化;对象被嵌入到其他对象中(组合composite)。

指明对象不应该被分配到堆上的方式之一,是将其堆内存分配定义为不合法的。直接实例化总是调用new来建立对象,但是我们不能重载new操作符(它内置与C++语言中),但是我们能够重载operator new函数来达到目的:

class NoHeap

{

public:

//.....

protected:

void *operator new(size_t) { return 0;}; //注意,这里是operator new而不是new operator

//因为只有operator new能够被重载

void operator delete(void*){};

};

任何在堆上分配一个NoHeap对象的操作,都会导致编译期错误:

NoHeap *nh = new NoHeap; //ErrNoHeap::operator new是受保护的

//..........

delete nh; //ErrNoHeap::operator delete是受保护的

之所以将operator newoperator delete声明为protected,是因为它们可能会被派生类的构造函数和析构函数隐式地调用。如果NoHeap不打算用作基类,则这两个函数也可以声明为private

有趣的是,当把operator new声明为private时:

(1) 而且NoHeap用作基类时,由于operator newoperator delete是自动继承的,所以派生类建立堆对象时会报错,除非派生类自己改写operator newoperator delete,并声明为public

(2) 不会对包含NoHeap成员对象的对象的分配产生任何影响:

class Asset

{

public:

Asset(int iValue);

...

private:

NoHeap value;

};

Asset *pa = new Asset(10); //正确,调用Asset::operator new::operator new

//而不是ºNoHeap::operator new

2)注意还要防止在堆上分配NoHeap对象的数组。此时,只要将array newarray delete声明为private

且不予定义即可:

class NoHeap

{

public:

//...........

protected:

void *operator new(size_t) {return 0;}

void operator delete(void*){}

private:

void *operator new[] (size_t);

void operator delete[] (void*);

};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值