C++智能指针unique_ptr详解

在一个大型程序中,指向动态分配内存的指针可能会在程序的各个部分使用。在这种情况下,确定哪些内存不再需要,或者程序的哪个部分应该负责删除指针就变得比较困难。

程序可能会因此出现悬挂指针,也就是说,指针已经被删除了,但其内存仍然在使用中;还可能出现内存泄漏,也就是说,即使已经不再需要内存了,但指针仍然未被删除。另外还有双重删除的问题,当程序的某 部分要删除一个已经被删除的指针时,即可出现这种情况。如果被删除的内存已经进行了重新分配,则双重删除会对程序造成破坏。

C++ 11 引入了智能指针的概念来解决该问题。智能指针是一个可以像指针一样工作的对象,但是当它不再被使用时,可以自动删除动态分配的内存。

C++11 提供了 3 种智能指针类型,它们分别由 unique_ptr 类、shared_ptr 类和 weak_ptr 类定义,所以又分别称它们为独占指针、共享指针和弱指针。

智能指针背后的核心概念是动态分配内存的所有权。智能指针被称为可以拥有或管理它所指向的对象。当需要让单个指针拥有动态分配的对象时,可以使用独占指针。对象的所有权可以从一个独占指针转移到另一个指针,其转移方式为:对象始终只能有一个指针作为其所有者。当独占指针离开其作用域或将要拥有不同的对象时,它会自动释放自己所管理的对象。

共享指针将记录有多少个指针共同享有某个对象的所有权。当有更多指针被设置为指向该对象时,引用计数随之增加;当指针和对象分离时,则引用计数也相应减少。当引用计数降低至0时,该对象被删除。

unique_ ptr、shared_ ptr 和 weak_ ptr 类是在 memory 头文件中定义的,所以需要在使用它们的程序中包含以下语句:

#include <memory>

本节我们先来讨论 unique_ ptr。

智能指针实际上是一个对象,在对象的外面包围了一个拥有该对象的普通指针。这个包围的常规指针称为裸指针

智能指针类可以通过它所指向的对象类型设置形参。例如,unique_ptr<int> 就是一个指向 int 的指针;而 unique_ptr<double> 就是一个指向 double 的指针。以下代码显示了如何创建独占指针:

unique_ptr<int> uptr1(new int);
unique_ptr<double> uptr2(new double);

当然,也可以先定义一个未初始化的指针,然后再给它赋值:

unique_ptr<int> uptr3;
uptr3 = unique_ptr<int> (new int);

为了避免内存泄漏,通过智能指针管理的对象应该没有其他的引用指向它们。换句话说,指向动态分配存储的指针应该立即传递给智能指针构造函数,而不能先将它赋值给指针变量。

例如,应该避免按以下方式编写代码:

int *p = new int;
unique_ptr<int> uptr(p);

智能指针不支持指针的算术运算,所以下面的语句将导致编译器错误:

uptr1 ++;
uptr1 = uptr1 + 2;

但是,智能指针通过运算符重载支持常用指针运算符 *->。以下代码将解引用一个独占指针,以给动态分配内存位置赋值,递增该位置的值,然后打印结果:

unique_ptr<int> uptr(new int);
*uptr = 12;
*uptr = *uptr + 1;
cout << *uptr << endl;

不能使用其他 unique_ptr 对象的值来初始化一个 unique_ptr。同样,也不能将一个 unique_ptr 对象赋值给另外一个。这是因为,这样的操作将导致两个独占指针共享相同对象的所有权,所以,以下语句都将出现编译时错误:

unique_ptr<int> uptr1(new int);
unique_ptr<int> uptr2 = uptr1; // 非法初始化
unique_ptr<int> uptr3; // 正确
uptr3 = uptr1; // 非法赋值

C++ 提供了一个 move() 库函数,可用于将对象的所有权从一个独占指针转移到另外一个独占指针:

unique_ptr<int> uptr1(new int);
*uptr1 = 15;
unique_ptr<int> uptr3; // 正确
uptr3 = move (uptr1) ; // 将所有权从 uptr1 转移到 uptr3
cout << *uptr3 << endl; // 打印 15

假设存在以下转移语句:

U = move(V);

那么,当执行该语句时,会发生两件事情。首先,当前 U 所拥有的任何对象都将被删除;其次,指针 V 放弃了原有的对象所有权,被置为空,而 U 则获得转移的所有权,继续控制之前由 V 所拥有的对象。

不能直接通过值给函数传递一个智能指针,因为通过值传递将导致复制真正的形参。如果要让函数通过值接收一个独占指针,则在调用函数时,必须对真正的形参使用 move() 函数:
//函数使用通过值传递的形参
void fun(unique_ptr<int> uptrParam)
{
    cout << *uptrParam << endl;
}

int main()
{
    unique_ptr<int> uptr(new int);
    *uptr = 10;
    fun (move (uptr)); // 在调用中使用 move
}
以上代码将打印来自于函数 fun() 中的 10。

当然,如果通过引用传递的方式,那就不必对真正的形参使用 move() 函数了。示例代码如下:
//函数使用通过引用传递的值
void fun(unique_ptr<int>& uptrParam)
{
    cout << *uptrParam << endl;
}

int main()
{
    unique_ptr<int> uptr(new int);
    *uptr1 = 15;
    fun (uptr1) ; //在调用中无须使用move
}
以上代码在执行时将打印数字 15。

有趣的是,可以从函数中返回一个独占指针,这是因为在遇到返回 unique_ptr 对象的函数时,编译器会自动应用 move() 操作以返回其值。来看以下代码:
//返回指向动态分配资源的独占指针
unique_ptr<int> makeResource()
{
    unique_ptr<int> uptrResult(new int);
    *uptrResult = 55;
    return uptrResult;
}

int main()
{
    unique_ptr<int> uptr;
    uptr = makeResource () ; // 自动移动
    cout << *uptr << endl;
}
该程序的输出结果为 55。

永远不要试图去动态分配一个智能指针,相反,应该像声明函数的局部变量那样去声明智能指针。当 unique_ptr 将要离开作用域时,它管理的对象也将被删除。如果要删除智能指针管理的对象,但同时又保留智能指针在作用域中,则可以将其值设置为 nullptr,或者调用其 reset() 成员函数,示例如下:

uptr = nullptr;
uptr.reset();

从 C++14 开始,有一个库函数 make_unique<T>() 可用于创建 unique_ptr 对象。该函数分配一个类型为 T 的对象,然后返回一个拥有该对象的独占指针。例如,来看下面的代码:

unique_ptr<int> uptr(new int);

现在可以弃用上面的代码,而改为使用以下代码:

unique_ptr<int> uptr = make_unique<int>();

指向数组的独占指针

按上述方式创建的独占指针将对指向已删除的被管理对象的包围指针调用 delete,但是,如果该包围指针指向的是一个对象数组的话,那么这种操作就是不正确的。要确保调用 delete[] 来处理被解除分配的对象数组,则应该在对象类型后面包含一对空的方括号 []。

例如,要使用指向动态分配 5 个整数数组的独占指针,需编写以下语句:

unique_ptr<int[]> uptr(new int[5]);

前面介绍过,智能指针 uptr 可以像一个指向int的普通指针那样使用;前面还介绍过,可以对指针使用数组符号,因此,可以如以下方式编写一个程序,在像 up[k] 这样的数组中存储整数的平方值:
int main()
{
    //指向数组的独占指针
    uriique_ptr<int [ ] > up (new int [5]);
    //设置数组元素为整数的平方值
    for (int k = 0; k < 5; k++)
    {
        up[k] = (k + l)*(k + 1);
    }
    //打印数组元素
    for (int k = 0; k < 5; k++)
    {
        cout << up[k] <<" ";
    }
    cout << endl;
}
以上代码的输出结果将是 "1 4 9 16 25"

当用于创建指向 T 类型对象数组的独占指针时,make_unique<T []>() 将釆用整数形参作为数组的大小:

unique_ptr<int[]> up = make_unique<int[]>(5);

unique_ptr 类的成员函数

unique_ptr 类有一些非常有用的实例成员函数,如表 1 所示。

表 1 unique_ptr成员函数
成员函数 描 述
reset() 销毁由该智能指针管理的任何可能存在的对象。该智能指针被置为空
reset(T* ptr) 销毁由该智能指针当前管理的任何可能存在的对象。该智能指针继续控制由裸指针 ptr 指向的对象
get() 返回该智能指针管理的由裸指针指向的对象。如果某个指针需要传递给函数,但是 该函数并不知道该如何操作智能指针,则 get() 函数非常有用