C++智能指针详解
Swift Lv6

了解Objective-C/Swift的程序员应该知道引用计数的概念。引用计数这种计数是为了防止内存泄露而产生的。 基本想法是对于动态分配的对象,进行引用计数,每当增加一次对同一个对象的引用,那么引用对象的引用计数就会增加一次, 每删除一次引用,引用计数就会减一,当一个对象的引用计数减为零时,就自动删除指向的堆内存。

在传统C++中,程序员需要手动释放资源,经常忘记去释放资源而导致泄露。通常的做法是对于一个对象而言,我们在构造函数的时候申请空间,而在析构函数(在离开作用域时调用)的时候释放空间, 也就是我们常说的 RAII资源获取即初始化技术。

传统C++里我们使用newdelete去申请和释放资源,两者必须配对写,new会返回一个裸指针,即Object *这种形式。而C++11引入了智能指针的概念,使用了引用计数的想法,让程序员不再需要关心手动释放内存。 这些智能指针包括:

  • shared_ptr
  • unique_ptr
  • weak_ptr

使用它们需要包含头文件memory,下面进行详细介绍。

shared_ptr

shared_ptr是一种智能指针,它能够记录多少个shared_ptr共同指向一个对象,从而消除显式的调用delete,当引用计数变为零的时候就会将对象自动删除。

  • make_shared用来消除显式的使用new,它会分配创建传入参数中的对象,并返回这个对象类型的shared_ptr指针。
  • shared_ptr可以通过get()方法来获取原始指针,通过reset()来减少一个引用计数, 并通过use_count()来查看一个对象的引用计数。

下面是关于 shared_ptr 的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <memory>

using namespace std;

int main()
{
auto p = make_shared<int>(10);
(*p)++;
cout << *p << endl;

auto p1 = p;
auto p2 = p1;
cout << "p count: " << p.use_count() << endl; // 3
cout << "p1 count: " << p1.use_count() << endl; // 3
cout << "p2 count: " << p2.use_count() << endl; // 3
cout << "---------------" << endl;

p1.reset();
cout << "p count: " << p.use_count() << endl; // 2
cout << "p1 count: " << p1.use_count() << endl; // 0
cout << "p2 count: " << p2.use_count() << endl; // 2
cout << "---------------" << endl;

p.reset();
cout << "p count: " << p.use_count() << endl; // 0
cout << "p1 count: " << p1.use_count() << endl; // 0
cout << "p2 count: " << p2.use_count() << endl; // 1
return 0;
}

特点

  • 占用内存高:因为除了要管理一个裸指针外,还要维护一个引用计数器。
  • 原子操作性能低:虑到线程安全问题,引用计数的增减必须是原子操作。而原子操作一般情况下都比非原子操作慢。

使用场景

  • 通常使用在共享权不明的场景,有可能多个对象同时管理同一个内存。
  • 对象的延迟销毁,当一个对象的析构非常耗时,甚至影响到了关键线程的速度。可以使用 BlockingQueue<shared_ptr<void>>将对象转移到另外一个线程中释放,从而解放关键线程(陈硕-《Linux多线程服务器端编程》)。

unique_ptr

unique_ptr是一种独占的智能指针,它禁止其他智能指针与其共享同一个对象,从而保证代码的安全。unique_ptr只支持移动,不支持赋值:

1
2
3
unique_ptr<int> pointer = make_unique<int>(10);
unique_ptr<int> pointer2 = pointer; // 非法
unique_ptr<int> pointer3 = move(pointer); // 合法

下面是关于 unique_ptr 的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
#include <memory>

struct Foo {
Foo() { cout << "construct Foo" << endl; }
~Foo() { cout << "delete Foo" << endl; }
void foo(int i) { cout << "point" << i << " is not null. Here is Foo:foo" << endl; }
};

void f(const Foo& foo, int i) {
cout << "point" << i << " call outer function" << endl;
}

int main()
{
unique_ptr<Foo> p1(make_unique<Foo>());
// p1 不空, 输出
if (p1)
{
p1->foo(1);
}
{
unique_ptr<Foo> p2(move(p1));
// p2 不空, 输出
f(*p2, 2);
// p2 不空, 输出
if (p2) p2->foo(2);
// p1 为空, 无输出
if (p1) p1->foo(1);
p1 = move(p2);
// p2 为空, 无输出
if (p2) p2->foo(2);
cout << "p2 被销毁" << endl;
}
// p1 不空, 输出
if (p1) p1->foo(1);
return 0;
}

特点

  • unique_ptr在默认情况下和裸指针的大小是一样的。所以内存上没有任何的额外消耗,性能是最优的。

使用场景

  • 忘记delete:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    class Widget
    {
    public:
    Widget() {}
    ~Widget() {}
    void do_something()
    {
    cout << "Here is Widget!" << endl;
    }
    };

    class Box
    {
    public:
    Box(): w(new Widget())
    {}
    ~Box()
    {
    /*delete w;*/
    }
    void call_widget()
    {
    w->do_something();
    }
    private:
    Widget* w;
    };
    如果因为一些原因,w必须建立在堆上。如果用裸指针管理w,那么需要在析构函数中 delete w,但程序员容易漏写该语句,造成内存泄漏。

如果按照unique_ptr的写法,不用在析构函数手动delete属性。当对象析构时,属性w将会自动释放内存。

  • 异常安全
    假如我们在一段代码中,需要创建一个对象,处理一些事情后返回,返回之前将对象销毁,如下所示:
    1
    2
    3
    4
    5
    6
    void process()
    {
    Widget* w = new Widget();
    w->do_something(); // 可能会发生异常
    delete w;
    }
    在正常流程下,我们会在函数末尾delete创建的对象w,正常调用析构函数,释放内存。

但是如果w->do_something()发生了异常,无法执行到delete w。此时就会发生内存泄漏。
我们当然可以使用try…catch捕捉异常,在 catch里面执行delet,但是这样代码上并不美观,也容易漏写。

如果我们用unique_ptr,那么这个问题就迎刃而解了。无论代码怎么抛异常,在unique_ptr离开函数作用域的时候,内存就将会自动释放。

weak_ptr

看如下代码;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct A;
struct B;

struct A {
shared_ptr<B> pointer;
~A() {
cout << "A 被销毁" << endl;
}
};
struct B {
shared_ptr<A> pointer;
~B() {
cout << "B 被销毁" << endl;
}
};
int main() {
auto a = make_shared<A>();
auto b = make_shared<B>();
a->pointer = b;
b->pointer = a;
}

运行结果是 A, B 都不会被销毁,这是因为 a,b 内部的 pointer 同时又引用了 a,b,这使得 a,b 的引用计数均变为了 2,而离开作用域时,a,b 智能指针被析构,却只能造成这块区域的引用计数减一,这样就导致了 a,b 对象指向的内存区域引用计数不为零,而外部已经没有办法找到这块区域了,也就造成了内存泄露,如图所示:

解决这个问题的办法就是使用弱引用指针weak_ptr,它是一种弱引用(相比较而言shared_ptr就是一种强引用)。弱引用不会引起引用计数增加,当换用弱引用时候,最终的释放流程如图下图所示:

在上图中,最后一步只剩下 B,而 B 并没有任何智能指针引用它,因此这块内存资源也会被释放。

weak_ptr没有*运算符和->运算符,所以不能够对资源进行操作,它可以用于检查shared_ptr是否存在,其expired()方法能在资源未被释放时,会返回false,否则返回true;除此之外,它也可以用于获取指向原始对象的shared_ptr指针,其lock()方法在原始对象未被释放时,返回一个指向原始对象的shared_ptr 指针,进而访问原始对象的资源,否则返回nullptr

总结

在日常使用中,unique_ptr使用频率最高,weak_ptr最低,需要避免循环引用的情况。当然,还是要根据具体的业务场景和性能要求来选择哪种指针。


参考

Powered by Hexo & Theme Keep
Unique Visitor Page View