Jamzy Wang

life is a struggle,be willing to do,be happy to bear~~~

C++中的智能指针剖析

2012-06-25 21:49

原创声明:本作品采用知识共享署名-非商业性使用 3.0 版本许可协议进行许可,欢迎转载,演绎,但是必须保留本文的署名(包含链接),且不得用于商业目的。

智能指针

内存管理是C++中最让人头疼的大问题。不像JAVA那样拥有GC这一利器,C++将内存管理的部分权限交给了程序员,允许程序员在堆上申请内存使用内存,自然也需要程序员自己释放不再被使用的内存。在C++编程中,在堆上申请了内存却忘了释放或者写了 delete 语句内存却未被释放导致的内存泄露是最常见的内存管理错误。

  • 1) 用了 new 却忘了 delete
1
2
3
4
5
void function ()
{
    Object *ptr = new Object();
    ptr->DoSomething();
}
  • 2) 在调用 delete 之前程序发生异常(exception)导致程序无法执行到 delete 语句
1
2
3
4
5
6
void function ()
{
    Object *ptr = new Object();
    ptr->DoSomething();    //raise an exception
    delete ptr;
}

为了避免上述两种情形导致的内存泄露,一种安全的写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void function()
{
    Object *ptr = new Object();
    try
    {
        ...
    }
    catch (...)    //for any exception
    {
        delete ptr;     // clean  up
        throw;
    }
    delete ptr;     //clean up on normal end

}

上述写法中,程序捕捉 new 语句和 delete 语句之间的所有异常,并在异常处理块中删除已分配的内存。这种方法虽然可以避免内存的泄露,但是实在是过于繁杂。那么有没有一种既能防止内存泄露又使用简便的方法呢?答案是使用智能指针(smart pointer),使用了智能指针后上述代码就可以写成如下形式:

1
2
3
4
5
6
7
8
9
void function ()
{
    SomeSmartPtr<MyObject> ptr(new MyObject());
    ptr->DoSomething(); // Use the object in some way.

    // Destruction of the object happens, depending on the policy 
    // the smart pointer class uses.
    // Destruction would happen even if DoSomething() raises an exception
}

那么什么是智能指针呢?

A smart pointer is a class that wraps a “bare” C++ pointer, to manage the lifetime of the object being pointed to.

智能指针是存储指向动态分配(堆)对象指针的类, 用于生存期控制, 它能够确保自动正确的销毁动态分配的对象,防止内存泄露。

C++ 智能指针思路类似于在语言(如 C#)中创建对象的过程:创建对象后让系统负责在正确的时间将其删除。 不同之处在于,单独的垃圾回收器不在后台运行;按照标准 C++ 范围规则对内存进行管理,以使运行时环境更快速更有效。一种简单的智能指针的实现如下:

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
39
40
41
42
43
44
45
46
47
48
49
50
template <typename T>
class SmartPointer
{
public:
    SmartPointer(T* ptr){
        ref = ptr;
        ref_count = (unsigned*)malloc(sizeof(unsigned));
        *ref_count = 1;
    }

    SmartPointer(SmartPointer<T> &sptr){
        ref = sptr.ref;
        ref_count = sptr.ref_count;
        ++*ref_count;
    }

    SmartPointer<T>& operator=(SmartPointer<T> &sptr){
        if (this != &sptr) {
            if (--*ref_count == 0){
                clear();
                cout<<"operator= clear"<<endl;
            }

            ref = sptr.ref;
            ref_count = sptr.ref_count;
            ++*ref_count;
        }
        return *this;
    }

    ~SmartPointer(){
        if (--*ref_count == 0){
            clear();
            cout<<"destructor clear"<<endl;
        }
    }

    T getValue() { return *ref; }

private:
    void clear(){
        delete ref;
        free(ref_count);
        ref = NULL; // 避免它成为迷途指针
        ref_count = NULL;
    }

protected:
    T *ref;
    unsigned *ref_count;};

从上例中我们可以看到,智能指针是在堆栈上声明的类模板,并可通过使用指向某个堆分配的对象的原始指针进行初始化。 在初始化智能指针后,它将拥有原始的指针。 这意味着智能指针负责删除原始指针指定的内存。智能指针析构函数包括要删除的调用,并且由于在堆栈上声明了智能指针,当智能指针超出范围时将调用其析构函数,尽管堆栈上的某处将进一步引发异常。

通过使用熟悉的指针运算符(-> 和 *)访问封装指针,智能指针类将重载这些运算符以返回封装的原始指针。

有了上述smart pointer后,我们就可以通过如下形式使用:

1
2
3
4
5
void function()
{
    SmartPointer<int> ptr(new Object());
    ptr->DoSomething();
}

我们来分析一下上述函数的执行过程:

1) new Object()在堆上构造了一个Object对象并返回指向这个对象的指针p;

2) 指针p被传入到智能指针模板类的构造函数中,之后p所指的对象就由智能指针对象的ref数据成员负责;

3)ptr->DoSomething()实际执行的就是p->DoSomething();

4) 当程序执行到function的末尾时,因为ptr是在栈上分配的一个变量,所以它会被销毁, 又ptr是一个对象指针,销毁ptr会调用其析构函数,在析构函数中步骤1)分配的内存被释放。

智能指针的一种通用实现技术是使用引用计数(reference counting)。智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针。每次创建类的新对象时,初始化指针并将引用计数置为1;当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。

这时候可以回过来看上述智能指针模板类的析构函数中先判断 --*ref_count == 0,只有满足该条件时才会删除分配的内存代表的含义。

下面来看智能指针的拷贝问题。假设p为一个智能指针,那么语句 q = p 会发生什么呢?这种智能指针的拷贝问题有以下几种策略:

1) 创建新的拷贝。创建一个新的 p 所指向的对象的完全拷贝并用指针 q 指向这个新的拷贝

2) 转移对象的拥有权(ownership transfer)。指针p将对象的拥有权转给q

3) 引用计数(reference counting)。q = p 会将引用计数的值增加1。

4) 引用链(reference linking)。和引用计数类似,将指向同一个对象的智能指针构建一个循环链表。

可能有人会有疑问,为什么上述智能指针的析构函数一定会被执行呢?如果发生异常呢?这个涉及到了C++中对象的RAII机制,这个机制保证了对象的析构函数在 gets out of scopethrow an exception情况下都会被执行。

C++11中的智能指针

在现代 C++ 编程中,标准库包含智能指针,该指针用于确保程序不存在内存和资源泄漏且是异常安全的。智能指针是在 标头文件中的 std 命名空间中定义的。 它们对 RAII或“获取资源即初始化”编程惯用法至关重要。

RAII的主要目的是确保资源获取与对象初始化同时发生,从而能够创建该对象的所有资源并在某行代码中准备就绪。 实际上,RAII 的主要原则是为将任何堆分配资源(例如,动态分配内存或系统对象句柄)的所有权提供给其析构函数(包含用于删除或释放资源的代码以及任何相关清理代码的堆栈分配对象。)

C++ 标准库中支持的智能指针有如下几类:

1
2
3
4
unique_ptr(C++11) smart pointer with unique object ownership semantics
shared_ptr(C++11) smart pointer with shared object ownership semantics
weak_ptr(C++11)   weak reference to an object managed by std::shared_ptr
auto_ptr(until C++17) smart pointer with strict object ownership semantics
  • unique_ptr

unique_ptr 不共享它的指针。 它无法复制到其他 unique_ptr,无法通过值传递到函数,也无法用于需要副本的任何 STL 算法。 只能移动 unique_ptr。 这意味着,内存资源所有权将转移到另一 unique_ptr,并且原始 unique_ptr 不再拥有此资源。

下图演示了两个 unique_ptr 实例之间的所有权转换。

此处输入图片的描述(图源)

以下示例演示如何创建 unique_ptr 实例并在函数之间传递这些实例。

1
2
3
4
5
6
std::unique_ptr<int> p1(new int(5));
std::unique_ptr<int> p2 = p1; //Compile error.
std::unique_ptr<int> p3 = std::move(p1); //Transfers ownership. p3 now owns the memory and p1 is rendered invalid.

p3.reset(); //Deletes the memory.
p1.reset(); //Does nothing.
  • shared_ptr

采用引用计数的智能指针。 如果你想要将一个原始指针分配给多个所有者(例如,从容器返回了指针副本又想保留原始指针时),请使用该指针。 直至所有 shared_ptr 所有者超出了范围或放弃所有权,才会删除原始指针。 大小为两个指针;一个用于对象,另一个用于包含引用计数的共享控制块。

下图显示了指向一个内存位置的几个 shared_ptr 实例。

此处输入图片的描述(图源)

下面的例子显示了在新对象中声明和初始化一个 shared_ptr 的各种方式。

1
2
3
4
5
std::shared_ptr<int> p1(new int(5));
std::shared_ptr<int> p2 = p1; //Both now own the memory.

p1.reset(); //Memory still exists, due to p2.
p2.reset(); //Deletes the memory, since no one else owns the memory.
  • weak_ptr

结合 shared_ptr 使用的特例智能指针。 weak_ptr 提供对一个或多个 shared_ptr 实例拥有的对象的访问,但不参与引用计数。 如果你想要观察某个对象但不需要其保持活动状态,请使用该实例。

Ref

Comments