当前位置: 首页 > java >正文

通过限制对象的内存分配位置来实现特定的设计目标

《More Effective C++》中的条款27聚焦于如何通过语言特性强制或禁止对象在堆上分配,其核心目标是通过控制内存分配位置来提升代码的安全性、可维护性和资源管理效率。
个人觉得,这个条款看看就可以了,可能在个别情况下需要考虑条款中说的情况。
以下是该条款的详细解析:

一、核心设计思想

条款27的核心是通过限制对象的内存分配位置来实现特定的设计目标。例如:

  • 强制堆分配:确保对象生命周期由开发者显式管理(如多态对象需通过指针操作)。
  • 禁止堆分配:避免内存泄漏(如嵌入式系统中堆空间珍贵),或确保资源自动释放(如RAII类)。

二、强制对象在堆上分配

1. 析构函数私有化
  • 原理:栈上对象的析构由编译器自动调用,若析构函数为私有,编译器无法生成析构代码,导致栈分配失败。
  • 实现步骤
    class UPNumber {
    private:~UPNumber() {} // 析构函数私有
    public:static UPNumber* create() { return new UPNumber(); } // 工厂函数void destroy() { delete this; } // 显式释放内存
    };
    
  • 问题与解决方案
    • 继承问题:若类需被继承,析构函数应设为protected,并通过工厂函数创建对象。
    • 拷贝构造函数:若未声明拷贝构造函数,编译器会生成公有的默认版本,可能导致栈上拷贝。需显式删除拷贝构造函数:
      UPNumber(const UPNumber&) = delete;
      UPNumber& operator=(const UPNumber&) = delete;
      
2. 构造函数私有化(配合工厂函数)
  • 原理:禁止直接调用构造函数,强制通过工厂函数创建对象。
  • 实现示例
    class Singleton {
    private:Singleton() {}static Singleton* instance;
    public:static Singleton* getInstance() {if (!instance) instance = new Singleton();return instance;}
    };
    
  • 注意点:需处理编译器生成的默认构造函数(如拷贝构造函数),避免意外创建栈对象。
3. 处理数组分配
  • 问题new UPNumber[10]会调用operator new[],若未重载该运算符,可能绕过限制。
  • 解决方案:同时重载operator newoperator new[],并设为私有。

三、禁止对象在堆上分配

1. 删除operator new
  • 原理new操作符调用operator new分配内存,若该函数被删除,堆分配会编译失败。
  • 实现示例
    class StackOnly {
    public:void* operator new(size_t) = delete; // 禁止newvoid* operator new[](size_t) = delete; // 禁止new[]
    };
    
  • 应用场景:RAII类(如文件句柄、锁)需确保资源自动释放,禁止堆分配可避免手动管理内存。
2. 构造函数结合内存检测(非移植方案)
  • 原理:利用栈和堆在内存中的位置差异(栈向下生长,堆向上生长)判断分配位置。
  • 实现代码(仅作演示,依赖平台特性):
    class HeapProhibited {
    public:HeapProhibited() {void* stackAddr = &stackAddr;void* thisAddr = this;if (stackAddr < thisAddr) { // 假设栈地址高于堆地址throw std::runtime_error("Object created on heap!");}}
    };
    
  • 局限性:不同平台内存布局不同,可能导致误判。

四、常见陷阱与解决方案

1. 继承与动态绑定
  • 问题:若基类析构函数为私有,派生类无法正确销毁。
  • 解决方案
    • 基类析构函数设为protected virtual,允许派生类重写。
    • 通过工厂函数返回基类指针,确保正确调用析构函数。
2. 智能指针的影响
  • 问题std::make_unique等函数在堆上创建对象,若类禁止堆分配,需显式禁用。
  • 解决方案
    class NoHeap {
    public:friend std::unique_ptr<NoHeap> std::make_unique<NoHeap>(); // 允许make_uniquevoid* operator new(size_t) = delete;
    };
    
    或通过私有构造函数强制使用工厂函数。
3. 异常处理
  • 问题:析构函数私有可能导致异常栈展开失败。
  • 解决方案:确保析构函数在异常处理路径中可访问(如设为protected并通过基类管理)。

五、作者建议与最佳实践

  1. 优先使用析构函数私有化:相比构造函数私有化,析构函数仅需处理一个函数,更简洁。
  2. 结合工厂函数:通过静态工厂方法封装对象创建逻辑,提升代码可读性和可维护性。
  3. 明确文档说明:在类注释中清晰标注内存分配限制,避免误用。
  4. 测试边界情况:如数组分配、继承层次、异常场景等,确保限制生效。

六、实际应用场景

  1. 强制堆分配
    • 多态类(如Shape基类及其派生类)需通过指针操作,避免切片问题。
    • 资源管理类(如std::thread)需延迟释放资源。
  2. 禁止堆分配
    • RAII类(如文件锁、数据库连接)需确保资源自动释放。
    • 嵌入式系统中内存受限,需避免动态分配。

七、总结

条款27通过控制内存分配位置,将对象生命周期管理纳入类型系统,减少了人为错误的可能性。其核心方法包括:

  • 强制堆分配:析构函数私有化 + 工厂函数。
  • 禁止堆分配:删除operator new
  • 处理继承与异常:合理使用protected成员和虚析构函数。

开发者应根据具体需求选择合适的方法,并注意实现中的陷阱(如数组分配、智能指针兼容性)。通过结合条款27的技术,可显著提升代码的健壮性和可维护性。

http://www.xdnf.cn/news/17716.html

相关文章:

  • powerbi本地报表发布到web,以得到分享链接
  • Day13 Vue工程化
  • SQL 语言分类
  • 人大BABEC地平线高效率具身导航!Aux-Think:探索视觉语言导航中数据高效的推理策略
  • @RequestMapping接收文件格式的形参(方法参数)
  • idea git commit特别慢,cpu100%
  • 13.深度学习——Minst手写数字识别
  • 嵌入式第二十六天(文件IO相关操作)
  • 基于PROFINET的西门子PLC通讯:S7-200与S7-1200在自动化仓储中的协同应用
  • NetworkManager配置热点
  • 6深度学习Pytorch-神经网络--过拟合欠拟合问题解决(Dropout、正则化、早停法、数据增强)、批量标准化
  • Qt树形控件QTreeWidget详解:构建可编辑的多级课程结构
  • 牛客疑难题(6)
  • 需求沟通会议如何组织
  • 呼吸道病原体检测需求激增,呼吸道快检试纸条诊断试剂生产厂家推荐,默克全链解决方案助IVD企业把握百亿风口
  • Java调用Vue前端页面生成PDF文件
  • JMeter并发测试与多进程测试
  • 开疆智能Ethernet转ModbusTCP网关连接发那科机器人与三菱PLC配置案例
  • Jenkins 实战指南-项目自动构建部署全流程通关
  • 快速了解自然语言处理
  • RIOT、RT-Thread 和 FreeRTOS 是三种主流的实时操作系统
  • 【debug 解决 记录】stm32 debug模式的时候可以运行,但是烧录没法执行
  • 鸿蒙下载图片保存到相册,截取某个组件保存到相册
  • 海康视觉相机驱动软件参数配置
  • 【C++】哈希
  • Unity:GUI笔记(一)——文本、按钮、多选框和单选框、输入框和拖动条、图片绘制和框绘制
  • 电商双 11 美妆数据分析:从数据清洗到市场洞察
  • Linux系统中mount指令的作用与必要性
  • 简单的双向循环链表实现与使用指南
  • Java数据库编程之【JDBC数据库例程】【自动生成报表】【六】