智能指针和垃圾回收

堆内存管理:智能指针与垃圾回收

显式内存管理

  • 野指针
  • 重复释放
  • 内存泄漏

C++11 的智能指针

  • unique_ptr
  • shared_ptr
  • weak_ptr
#include <iostream>
#include <memory>

int main()
{
    std::unique_ptr<int> up1(new int(11));
//    unique_ptr<int> up2 = up1; // error, 不能复制

    std::cout << *up1 << std::endl; // 11

    std::unique_ptr<int> up3 = std::move(up1); // up3 是数据唯一的 unique_ptr 智能指针

    std::cout << *up3 << std::endl; // 11
    std::cout << *up1 << std::endl; // error, 但偶尔可能输出正确的值,但是不安全

    up3.reset(); // 显式释放内存
    up1.reset(); // 不会导致运行时错误

    std::cout << *up3 << std::endl; // 运行时错误

    std::shared_ptr<int> sp1(new int(22));
    std::shared_ptr<int> sp2 = sp1;

    std::cout << *sp1 << std::endl; // 22
    std::cout << *sp2 << std::endl; // 22

    sp1.reset();
    std::cout << *sp2 << std::endl; // 22

    return 0;
}
  • weak_ptr 可以指向 shared_ptr 指针指向的内存对象,却并不拥有该内存。而使用 weak_ptr 的 lock 成员,可以返回其指向内存的一个 shared_ptr 对象,且在所指向内存已经失效时,返回空值。
#include <iostream>
#include <memory>

void check(std::weak_ptr<int> &wp)
{
    std::shared_ptr<int> sp = wp.lock();
    if (sp != nullptr) {
        std::cout << "Still " << *sp << std::endl;
    } else {
        std::cout << "Pointer is invalid." << std::endl;
    }
}

int main()
{
    std::shared_ptr<int> sp1(new int(22));
    std::shared_ptr<int> sp2 = sp1;
    std::weak_ptr<int> wp = sp1;

    std::cout << *sp1 << std::endl; // 22
    std::cout << *sp2 << std::endl; // 22

    check(wp); // Still 22

    sp1.reset();
    std::cout << *sp2 << std::endl; // 22
    check(wp); // Still 22

    sp2.reset();
    check(wp); // Pointer is invalid.

    return 0;
}

智能指针虽然好,但是还是要显式使用。

垃圾回收的分类

Garbage Collection

Language Support
C++ 部分
Java 支持
Python 支持
C 不支持
C# 支持
Ruby 支持
PHP 支持
Perl 支持
Hashkell 支持
Pascal 不支持

垃圾回收主要分为两类:

  • 基于引用计数(reference counting grabage collector)的垃圾回收器

    引用计数必须存在的问题: retain cycle

  • 基于跟踪处理(tracing grabage collector)的垃圾回收器

    • 标记——清除(Mark——Sweep)

      算法分为两个过程:首先将程序中正在使用的对象视为“根对象”,从根对象开始查找它们所引用的堆空间,并在这些堆空间上做标记。当标记结束后,所有被标记的对象就是可达对象(Reachable Object)或活对象(Live Object),而没有被标记的对象被认为是垃圾,在第二步的清扫(Sweep)阶段会被回收掉。

      特点式活的对象不会被移动,但是其存在大量的内存碎片问题。

    • 标记——整理(Mark——Compact)

      这种算法标记的方法和标记——清除方法一样,但是标记完后,不再便利所有对象清扫垃圾了,而是将活的对象向“左”靠齐,这就解决了碎片问题。

    • 标记——拷贝(Mark——Copy)

      将堆空间分为两部分:From和To。刚开始系统从 From 堆空间里分配内内存,当 From 分配满的时候系统就开始垃圾回收:从From堆空间里找出所有活的对象,拷贝到To的堆空间里。这样一来,From的堆空间里就全剩下垃圾了。而对象拷贝到To里之后,在To里式排列紧凑的。接下来需要将From和To交换角色,接着从新的From里开始分配。

      该算法堆的利用率只有一半,而且也需要移动活的对象。此外,从某种意义上讲,该算法其实是标记——整理算法的一种实现而已。

C++ 与垃圾回收

  • Boehm 已经支持 标记——清除 方法的垃圾回收,但是可移植性不好
  • 为了解决GC中安全性和可移植性的问题,在2007年,HP 的 Hans-J.Boehm 和 Symantec 的 Mike Spertus 共同向 C++ 委员会提交了一个关于 C++ 中垃圾回收的提案。该提案通过添加 gc_forbidden, gc_relaxed, gc_required, gc_safe, gc_strict 等关键字来支持 C++ 语言中的垃圾回收。由于给特定过于复杂,并且存在问题,后来被标准删除了。所以 Boehm 和 Spertus 对初稿进行了简化,仅仅保留了支持GC的最基本部分,即通过语言的约束,来保证安全的GC。这也是我们看到的C++11标准中的“最小垃圾回收的支持”的历史由来。
  • 要保证安全的GC,首先必须知道C/C++语言中什么样的行为可能导致GC出现不安全的情况。简单地说,不安全源自于C/C++语言对指针的“放纵”,即允许过分灵活的使用。
int main()
{
    int *p = new int;
    p += 10; // 移动指针,可能导致 GC
    p -= 10; // 回收原来指向的内存
    *p = 10; // 再次使用原本相同的指针可能无效
}
int main()
{
    int *p = new int;
    int *q = (int *)(reinterpret_cast<long long>(p) ^ 2012); // q 隐藏了 p

    // 做了一些其他工作,垃圾回收器可能已经回收了 p 指向对象
    q = (int *)(reinterpret_cast<long long>(q) ^ 2012); // 这里 q == p
    *q = 10;
    /*
     * 先将 p 隐藏,然后恢复。
     * 在隐藏 p 后,很有可能出发 GC,导致恢复 p 后仍为非法。
    */
}

C++11与最小垃圾回收支持

C++11 新标准为了做到最小的垃圾回收的支持,首先对“安全”的指针进行了定义,或者使用C++11中的术语说,安全派生(safely derived)的指针。安全派生的指针指向由 new 分配的对象或其子对象的指针。安全派生指针的操作包括:

  • 在解引用基础上的引用: &*p
  • 定义明确的指针操作:p + 1
  • 定义明确的指针转换:static_cast<void *>(p)
  • 指针和整型之间的 reinterpret_cast: reinterpret_cast<intptr_t>(p).

通过 get_pointer_safety 函数查询编译器是否支持 “安全派生” 特性。

pointer_safety get_pointer_safety() noexcept
// 返回值:
- pointer_safety::strict —— 编译器支持最小垃圾回收及安全派生指针等相关概念
- pointer_safety::relax —— 编译器不支持
— pointer_safety::preferred —— 编译器不支持

如果程序中出现了指针不安全使用的状况,C++11 运行程序员通过一些API来通知GC不得回收该内存。

void declare_reachable(void *p);
template <class T> T* undeclare_rechable(T *p) noexcept;

declare_reachable 显式的通知 GC 某一个对象被认为是可达的,即使它所有指针都对回收器不可见。

undeclare_rechable 则可以取消这种可达声明。

#include <iostream>
#include <memory>

int main()
{
    int *p = new int;
    std::declare_reachable(p); // 在 p 被 不安全派生 前声明,这样 GC 不会回收

    int *q = (int *)((long long)p ^ 2012);

    // 解除可达声明
    q = undeclare_reachable<int>((int *)((long long)q ^ 2012));
    *q = 10;

    return 0;
}
  • declare_no_pointers
  • undeclare_no_pointers

告诉编译器该内存区域不存在有效的指针。

void declare_no_pointers(char *p, size_t n) noexcept;
void undeclare_no_pointers(char *p, size_t n) noexcept;

垃圾回收的兼容性

GC 必然会破坏向后兼容。因此,我们必须限制指针的使用或者使用 declare_reachable/undeclare_reachable, declare_no_pointers/undeclare_no_pointers 来让一些不安全的指针使用免于垃圾回收的检查。因此想让老代码毫不费力地使用垃圾回收,现实情况对大多数代码不太可能。

此外, C++11 标准对指针垃圾回收的支持仅限于 new 操作符分配的内存;
malloc 分配的内存会被总认为是 可达的,即无论何时垃圾回收器都不予回收。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容