我们知道,程序内存布局将内存划分为堆、栈、BSS 段、数据段和代码段。既然如此,我们称位于堆上的对象叫堆对象,位于栈上的对象的叫栈对象,位于BSS段和数据段的对象叫全局对象或静态对象。通常情况下,对象创建在堆上还是在栈上,创建多少个,这都是没有限制的。但是有时会遇到一些特殊需求。

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

C++ 控制对象的创建方式和数量相关推荐

  1. 转 cocos2d-x 3.0 常用对象的创建方式

    cocos2d-x 3.0 中所有对象几乎都可以用create函数来创建,其他的创建方式也是有create函数衍生. 下面来介绍下create函数创建一般对象的方法,省得开发中经常忘记啥的. 1.精灵 ...

  2. c++类对象的创建方式

    对象创建限制在堆或栈 c++类对象的创建方式 对象创建限制在堆或栈 C++ 中的类的对象的建立模式 如何将类限制在堆上呢? C++ 中的类的对象的建立模式 C++ 中的类的对象的建立模式分为两张:静态 ...

  3. c#类属性和实例属性_Visual C#类和对象的创建方式,定义类,实例化对象,实例讲解...

    定义类 类由class member类成员组成,包含字段.属性.方法和事件.其中字段和属性为类的数据成员,用来存储数据:方法负责数据的传递和运算.使用类之前,要进行声明,声明的语法如下: Class ...

  4. JavaScript对象的创建方式

    JavaScript对象创建 1.直接式 //1.直接创建对象var student = new Object();student.name = "zhangsan";studen ...

  5. 【Spring 工厂】反转控制与依赖注入、Spring工厂创建复杂对象3种方式

    反转控制与依赖注入 反转控制 与 依赖注入 反转控制(IOC Inverse of Control) 依赖注入 (Dependency Injection - DI) Spring工厂创建复杂对象(3 ...

  6. activexobject对象不能创建_【设计模式】建造者模式:你创建对象的方式有它丝滑吗?...

    目录 什么是建造者模式 为什么要使用建造者模式 构造函数创建对象 set方式构建对象 java实现建造者模式 第一种实现方式 第二种方式 建造者模式与构造函数的对比 建造者模式与工厂模式的对比 总结 ...

  7. Java代理模式:如何优雅地控制对象访问?

    文章目录 一.引言 1.1 简介 二.什么是代理模式 2.2 概述 2.3 使用场景 2.4 优缺点 三.静态代理模式 3.1 定义 3.2 实现方式 3.3 示例代码 3.4 优缺点 四.动态代理模 ...

  8. 041_对象的创建和销毁

    1. 对象的创建和销毁都在JavaScript执行过程中发生, 理解这种方式的含义对理解整个语言至关重要. 2. 声明和实例化 2.1. 对象的创建方式是用关键字new后面跟上实例化的类的名字: va ...

  9. 单例对象会被jvm的gc时回收吗_设计模式专题02-单例五种创建方式

    单例五种创建方式(下一篇:工厂模式) 什么是单例 保证一个类只有一个实例,并且提供一个访问该全局访问点 单例应用场景 1. Windows的Task Manager(任务管理器)就是很典型的单例模式( ...

最新文章

  1. 从硬件角度看,无人车商业化落地难点
  2. java进度条字体颜色_java – Nimbus LF – 改变进度条的背景颜色
  3. L1-046. 整除光棍(模拟除法)
  4. windows 10下载链接
  5. html5监听动画结束,js判断css动画是否完成 animation,transition
  6. treelistview 所有节点失去焦点_垃圾询盘过滤,焦点科技的 Milvus 实践
  7. 信息学奥赛一本通 1008:计算(a+b)/c的值 | OpenJudge NOI 1.3 03
  8. python学习笔记8-列表、集合、字典推导式
  9. 【SpringMVC 笔记】结果跳转、数据处理、乱码问题
  10. linux中quota信息查看,Linux quota命令参数及用法详解---Linux磁盘配额限制设置和查看命令...
  11. 设计模式—单例模式(思维导图)
  12. windows下mysql-8.0.11的安装
  13. IPSEC 004 ---- 模板海纳百川,不定对端有容乃大
  14. GPS卫星定位基本原理
  15. android模拟器pc版 安装软件,不用花钱,电脑端Android模拟器安装使用教程
  16. Hibernate的一对一,一对多/多对一关联保存
  17. Hello CTP(一)——期货业务
  18. 彻底解决The last packet successfully received from the server was * milliseconds ago问题
  19. 使用Docker Compose构建ZigBee基础架构
  20. arr和arr的区别以及数组首元素地址和整个数组地址的区别

热门文章

  1. MFC如何让输入框只能输入数字
  2. Jenkins任务优先分配到原来的执行节点上
  3. 数据分析需要权衡哪些要素?
  4. linux 比较两个文件夹不同 (diff命令, md5列表)
  5. [转] Asp.net mvc 3 beta 新特性介绍
  6. Vlan配置详解之三层交换
  7. L2-001. 紧急救援-PAT团体程序设计天梯赛GPLT(Dijkstra算法)
  8. 蓝桥杯 ALGO-11算法训练 瓷砖铺放(递归/动态规划)
  9. vs code react-native 安卓调试_实战|C++在vscode上的调试配置
  10. RedHat Linux下获取snmp信息不全的解决办法