控制对象的创建方式(禁止创建栈对象or堆对象)和创建的数量

我们知道,C++将内存划分为三个逻辑区域:堆、栈和静态存储区。既然如此,我称位于它们之中的对象分别为堆对象,栈对象以及静态对象。通常情况下,对象创建在堆上还是在栈上,创建多少个,这都是没有限制的。但是有时会遇到一些特殊需求。

1.禁止创建栈对象

禁止创建栈对象,意味着只能在堆上创建对象。创建栈对象时会移动栈顶指针以“挪出”适当大小的空间,然后在这个空间上直接调用类的构造函数以形成一个栈对象。而当栈对象生命周期结束,如栈对象所在函数返回时,会调用其析构函数释放这个对象,然后再调整栈顶指针收回那块栈内存。在这个过程中是不需要operator new/delete操作的,所以将operator new/delete设置为private不能达到目的。

可以将构造函数或析构函数设为私有的,这样系统就不能调用构造/析构函数了,当然就不能在栈中生成对象了。这样的确可以,但有一点需要注意,那就是如果我们将构造函数设置为私有,那么我们也就不能用new来直接产生堆对象了,因为new在为对象分配空间后也会调用它的构造函数。所以,如果将构造函数和析构函数都声明为private会带来较大的副作用,最好的方法是将析构函数声明为private,而构造函数保持为public。

再进一步,将析构函数设为private除了会限制栈对象生成外,还有其它影响吗?是的,这还会限制继承。如果一个类不打算作为基类,通常采用的方案就是将其析构函数声明为private。为了限制栈对象,却不限制继承,我们可以将析构函数声明为protected,这样就两全其美了。如下代码所示:

class NoStackObject{ 
protected: 
    ~NoStackObject(){} 
public: 
    void destroy(){ 
        delete this ;//调用保护析构函数 
    } 
};

上面的类在创建栈对象时,如NoStackObject obj;时编译将会报错,而采用new的方式,编译就会通过。需要注意一点的是,通过new创建堆对象时,在手动释放对象内存时,我们需要调用其析构函数,这时就需要一点技巧来辅助——引入伪析构函数destory,如上面的代码所示。

方法拓展。 仔细一看,我们会发现上面的方法让人别扭。我们用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 ; //防止使用悬挂指针

现在感觉是不是好多了,生成对象和释放对象的操作一致了。

2.禁止创建堆对象

我们已经知道,产生堆对象的唯一方法是使用new操作,如果我们禁止使用new不就行了么。再进一步,new操作执行时会调用operator new,而operator new是可以重载的。方法有了,就是使new operator 为private,为了对称,最好将operator delete也重载为private。

class NoStackObject{
private:
    static void* operator new(size_t size);
    static void operator delete(void* ptr);
};

//用户代码
NoStackObject obj0;              //OK
static NoStackObject obj1;       //OK
NoStackObject * pObj2 = new NoStackObject;   //ERROR

如果也想禁止堆对象数组,可以把operator new[]和operator delete[]也声明为private。

这里同样在继承时存在问题,如果派生类改写了operator new和operator delete并声明为public,则基类中原有的private版本将失效,参考如下代码:

class NoStackObject{
protected:
    static void* operator new(size_t size);
    static void operator delete(void* ptr);
};


class NoStackObjectSon:public NoStackObject{
public:
    static void* operator new(size_t size){   //非严格实现,仅作示意之用 
        return malloc(size);
    };
    static void operator delete(void* ptr){   //非严格实现,仅作示意之用 
        free(ptr);
    };
};

//用户代码
NoStackObjectSon* pObj2 = new NoStackObjectSon;   //OK

3.控制实例化对象的个数

在游戏设计中,我们采用类CGameWorld作为游戏场景的抽象描述。然而在游戏运行过程中,游戏场景只有一个,也就是对CGameWorld对象的只有一个。对于对象的实例化,有一点是十分确定的:要调用构造函数。所以,如果想控制CGameWorld的实例化对象只有一个,最简单的方法就是将构造函数声明为private,同时提供一个static对象。如下:

class CGameWorld
{
public:
    bool Init();
    void Run();
private:
    CGameWorld();
    CGameWorld(const CGameWorld& rhs);

    friend CGameWorld& GetSingleGameWorld();
};

CGameWorld& GetSingleGameWorld()
{
    static CGameWorld s_game_world;
    return s_game_world;
}

这个设计有三个要点: (1)类的构造函数是private,阻止对象的建立; (2)GetSingleGameWorld函数被声明为友元,避免了私有构造函数引起的限制; (3)s_game_world为一个静态对象,对象唯一。

当用到CGameWorld的唯一实例化对象时,可以如下:

GetSingleGameWorld().Init();
GetSingleGameWorld().Run();

如果有人对GetSingleGameWorld是一个全局函数有些不爽,或者不想使用友元,将其声明为类CGameWorld的静态函数也可以达到目的,如下:

class CGameWorld
{
public:
    bool Init();
    void Run();
    static CGameWorld& GetSingleGameWorld();
private:
    CGameWorld();
    CGameWorld(const CGameWorld& rhs);
};

这就是设计模式中著名的单件模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

如果我们想让对象产生的个数不是一个,而是最大为N(N>0)个。可以在类内部设置一个静态计数变量,在调用构造函数时,该变量加1,当调用析构函数时,该变量减1。如下:

class CObject
{
public:
    CObject();
    ~CObject();
private:
    static size_t m_nObjCount;
    ...
};

CObject::CObject()
{
    if (m_nObjCount > N)
        throw;
    m_nObjCount++;
}

CObject::~CObject()
{
    m_nObjCount--;
}
size_t CObject::m_nObjCount;

掌握控制类的实例化对象个数的方法。当实例化对象唯一时,采用设计模式中的单件模式;当实例化对象为N(N>0)个时,设置计数变量是一个思路。

阅读上面的示例代码还需要注意抛出异常时没有对象,即throw后没有对象,有两种含义: (1)如果throw;在catch块中或被catch块调用的函数中出现,表示重新抛出异常。throw;表达式将重新抛出当前正在处理的异常。 我们建议采用该形式,因为这将保留原始异常的多态类型信息。重新引发的异常对象是原始异常对象,而不是副本。

(2)如果throw;出现在非catch块中,表示抛出不能被捕获的异常,即使catch(…)也不能将其补捕获。

4.小结

堆对象,栈对象以及静态对象统称为内存对象,如果要把内存对象理解的更为深入,推荐看看《深入探索C++对象模型》这本书。


参考文献

[1]C++——内存对象 禁止产生堆对象 禁止产生栈对象 [2]李健.编写高质量代码:改善C++程序的150个建议.第一版.北京:机械工业出版社,2012.1:299-301

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏tkokof 的技术,小趣及杂念

再谈谈列表元素的删除

之前(以及更早之前)都提到了列表元素的删除,也提到过几种方法,有兴趣的朋友可以去看看,其中一种个人比较倾向的写法大概是这个样子(C++):

7910
来自专栏用户画像

外部排序的方法

在实际应用中,由于外存设备的不同,通常又可分配磁盘文件排序和磁带文件排序两大类。磁带排序和磁盘排序的基本步骤相类似,主要的不同之处在于初始归并段在外存介质中的分...

13410
来自专栏C语言及其他语言

实例说明

上一节,我们大致总揽了一个简单C程序的框架,程序如下: #include<stdio.h> /*引入头文件*/ int main(void) /*一个简单的C程...

28880
来自专栏我的技术专栏

细说new与malloc的10点区别

17540
来自专栏Hongten

看来就知道的模板方法模式---java版_源码下载

=================================================

9230
来自专栏算法修养

位运算总结

用了那么多位运算,这里总结一下把。 先看常用的位运算有哪些吧: 1 & a&b 就是a的二进制形式与b的二进制形式,相同的位置必须两个都是1,那么结果...

34980
来自专栏杨熹的专栏

Day 1-Java-imooc-2.变量常量

课程地址:http://www.imooc.com/learn/85 总结图片来自 http://www.imooc.com/article/10535 ? 本...

37550
来自专栏Python入门

python是如何进行内存管理的

python内部使用引用计数,来保持追踪内存中的对象,Python内部记录了对象有多少个引用,即引用计数,当对象被创建时就创建了一个引用计数,当对象不再需要时,...

21310
来自专栏JAVA高级架构

Java面试2018常考题目汇总(一)

一、JAVA基础篇-概念 1.简述你所知道的Linux: Linux起源于1991年,1995年流行起来的免费操作系统,目前, Linux是主流的服务器操作系统...

355100
来自专栏开发与安全

从零开始学C++之boost库(一):详解 boost 库智能指针(scoped_ptr<T> 、shared_ptr<T> 、weak_ptr<T> 源码分析)

一、boost 智能指针 智能指针是利用RAII(Resource Acquisition Is Initialization:资源获取即初始化)来管理资源。关...

32500

扫码关注云+社区

领取腾讯云代金券