C++ 动态内存

概述

在 C++ 中,内存是通过 new 表达式分配,通过 delete 表达式释放的。标准库还定义了一个 allocator 类来分配动态内存块(allocator 可以实现内存分配和对象构造的分离)。

内存的正确释放是非常容易出错的地方: 要么内存永远不会被释放(内存泄漏),要么在仍有指针引用它时就被释放了(内存的二次释放问题)。新的标准库定义了智能指针类型—— shared_ptr、unique_ptr 和 weak_ptr,可令动态内存管理更为安全。

操作系统提供的资源除了内存,还有文件描述符、互斥锁、图形界面中的字型和笔刷、数据库连接以及网络 socket。不论哪种资源,当程序不再使用它时,必须将它还给系统。

尝试在任何运用情况下都确保以上所言,是件困难的事,尤其当考虑到异常、函数内多重回传路径、程序维护员改动软件却没能充分理解随之而来的冲击,态势就更明显了:资源管理的特殊手段还不很充分够用。

标准库提供的智能指针类型

指针类型特征特征操作
shared_ptr引用计数管理资源获得原始指针 get()
unique_ptr某一时刻都只能有一个 unique_ptr 指向给定对象(auto_ptr 的替代品)release()、reset();不能直接拷贝或赋值
weak_ptr不控制所指向的对象生命周期,指向一个由 shared_ptr 管理的对象不能使用 weak_ptr 直接访问对象,而必须调用 lock() 方法

shared_ptr 不直接支持管理动态数组,若要使用,需要提供自己定义的删除器

  • 与 unique_ptr 不同,shared_ptr 不直接支持管理动态数组。如果希望使用 shared_ptr 管理一个动态数组,必须提供自己定义的删除器:

    shared_ptr<int> sp(new int[10], [](int *p) {  delete[] p; });   // 为了使用 shared_ptr 管理动态数组,必须提供一个删除器
    sp.reset(); // 使用指定的删除器: lambda 表达式释放数组,它使用 delete[]
    

C++ 动态内存的实践建议

  1. 以对象管理资源

    • 获得资源后立刻放进资源管理对象内。资源取得时机便是初始化时机(RAII)。

    • 资源管理对象运用析构函数确保资源被释放。不论控制流如何离开区块,一旦对象被销毁,其析构函数自然就会被自动调用,于是资源被释放。

    • 在以对象管理资源的情况下,如何解决二次释放的问题: 引用计数型指针。标准库提供了 shared_ptr、unique_ptr 和 weak_ptr 三种智能指针类型。

      一个类使用动态生存期的资源,可能是出于以下需求之一:

      • 程序不知道自己需要使用多少对象
      • 程序不知道所需对象的准确类型
      • 程序需要在多个对象间共享数据
  2. 在资源管理类中小心 copying 行为

    • 复制 RAII 对象必须一并复制它所管理的资源,以资源的 copying 行为决定 RAII 对象的 copying 行为
    • 普遍而常见的 RAII class copying 行为是: 抑制(或者禁止) copying、施行引用计数法(reference counting)
  3. 在资源管理类中提供对原始资源的访问

    • APIs 往往要求访问原始资源 (raw resource)(比如为了与 C 语言留下的接口兼容), 所以每隔 RAII class 应该提供一个 “取得其所管理之资源”的办法
    • 对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换比较安全,但隐式转换对客户比较方便
  4. 使用智能指针时应该坚持的基本规范

    • 不使用相同的内置指针值初始化(或 release)多个智能指针
    • 不 delete get() (shared_ptr 提供的返回原始指针的方法)返回的指针
    • 不使用 get() 返回的指针初始化或 reset 另一个智能指针
    • 如果使用智能指针管理的资源不是 new 分配的内存,记住传递给它一个删除器
  5. 成对使用 new 和 delete 时要采取相同形式

    • 如果在 new 表达式中使用 [],必须在相应的 delete 表达式中也使用 [],如果在 new 表达式中不使用 [],一定不要在相应的 delete 表达式中使用 []
    • 当使用 new 时(也就是通过 new 动态生成一个对象),有两件事发生,第一,内存会被分配出来,第二,针对此内存会有一个(或更多)构造函数被调用。当使用 delete,也有两件事发生:针对此内存会有一个(或更多) 析构函数被调用,然后内存才被释放。delete 的最大问题在于: 即将被删除的内存之内究竟由多少对象? (这个问题主要由 deletedelete [] 以区别)
  6. 以独立语句将 new 出来的对象置入智能指针

    • 以独立语句将 newed 对象存储于 (置入)智能指针内。如果不这样,一旦异常被抛出,有可能导致难以察觉的资源泄漏

      processWidget(new Widget, priority());
      

      对上面这条语句,在调用 processWidget 之前,编译器必须创建代码,做以下三件事:

      • 调用 priority
      • 执行 “new Widget”
      • 调用 shared_ptr 的构造函数

      但是编译器最终执行这三条语句的顺序是不确定的,也许是下面的这种顺序:

      1. 执行 “new Widget”
      2. 调用 priority
      3. 调用 shared_ptr 的构造函数

      如果在执行到第二条语句时发生了异常,则可能会造成在调用 processWidget 的过程中产生内存泄漏(并没有达到我们预期的用 shared_ptr 防卫内存泄漏的目的)。

malloc 和 delete 的区别

  1. C 语言其实是不支持动态内存分配的,是通过 malloc 库函数来实现的,可能一些硬件不支持 malloc;而 C++ 中 new 是一个关键字,不管是在任意编译器上,任意硬件上,都是能够进行动态内存分配的,这是本质区别。
  2. malloc 是基于字节来进行动态内存分配的, new 则是基于对象类型来进行动态内存分配。因此, malloc 需要自己指定申请的内存字节大小,而 new 可以不需要。
  3. malloc 不具备内存初始化的特性,而 new 默认会调用对象的构造函数初始化分配的内存

如何定义一个只能在堆上(栈上)生成对象的类

参考链接: huihut/interview

只能在堆上

  • 方法

    将析构函数设置为私有

  • 原因

    C++ 是静态绑定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能在栈上创建对象。

  • 实践建议

    此时 delete 也不能使用,建议将对象的构造和析构都封装成单独的函数,类似单例模式。

只能在栈上

  • 方法

    将 new 和 delete 重载为私有

  • 原因

    在堆上生成对象,使用 new 关键词操作,其过程分为两阶段:第一阶段,使用 new 在堆上寻找可用内存,分配给对象;第二阶段,调用构造函数生成对象。将 new 操作设置为私有,那么第一阶段就无法完成,就不能够在堆上生成对象。

参考资料

  • 《C++ Primer(第五版)》[美] Stanley B.Lippman 著,王刚,杨巨峰 译
  • 《Effective C++ (中文版 第三版)》[美] Scott Meyers 著,侯捷 译
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值