C++ - 智能指针

发布时间 2023-10-13 10:11:54作者: [BORUTO]

1. 什么是智能指针

从比较简单的层面来看,智能指针是RAII(Resource Acquisition Is Initialization,资源获取即初始化)机制对普通指针进行的一层封装。这样使得智能指针的行为动作像一个指针,本质上却是一个对象,这样可以方便管理一个对象的生命周期。

在C++中,智能指针一共定义了4种: auto_ptr、unique_ptr、shared_ptr 和 weak_ptr。其中 auto_ptr 在 C++11已被摒弃,在C++17中已经移除不可用。

C++11 中提供了三种智能指针,使用这些智能指针时需要引用头文件 #include <memory>

 

2. 原始指针的问题

原始指针的问题大家都懂,就是如果忘记删除,或者删除的情况没有考虑清楚,容易造成悬挂指针(dangling pointer)或者说野指针(wild pointer)。

我们看个简单的例子

objtype *p = new objtype();
p -> func();
delete p;

上面的代码结构是我们经常看到的。里面的问题主要有以下两点:

1.代码的最后,忘记执行delete p的操作。

2.第一点其实还好,比较容易发现也比较容易解决。比较麻烦的是,如果func()中有异常,delete p语句执行不到,这就很难办。有的同学说可以在func中进行删除操作,理论上是可以这么做,实际操作起来,会非常麻烦也非常复杂。

此时,智能指针就可以方便我们控制指针对象的生命周期。在智能指针中,一个对象什么情况下被析构或被删除,是由指针本身决定的,并不需要用户进行手动管理,是不是瞬间觉得幸福感提升了一大截,有点幸福来得太突然的意思,终于不用我自己手动删除指针了。

 

3. shared_ptr(共享的智能指针)

我们提到的智能指针,很大程度上就是指的shared_ptr,shared_ptr也在实际应用中广泛使用。它的原理是使用引用计数实现对同一块内存的多个引用。在最后一个引用被释放时,指向的内存才释放,这也是和 unique_ptr 最大的区别。当对象的所有权需要共享(share)时,share_ptr可以进行赋值拷贝。 shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,删除所指向的堆内存。

 

3.1 shared_ptr智能指针的创建

shared_ptr<T> 类模板中,提供了多种实用的构造函数,这里给读者列举了几个常用的构造函数(以构建指向 int 类型数据的智能指针为例)。

1) 通过如下 2 种方式,可以构造出 shared_ptr<T> 类型的空智能指针:

std::shared_ptr<int> p1;             //不传入任何实参
std::shared_ptr<int> p2(nullptr);    //传入空指针 nullptr

注意,空的 shared_ptr 指针,其初始引用计数为 0,而不是 1。

2) 在构建 shared_ptr 智能指针,也可以明确其指向。例如:

std::shared_ptr<int> p3(new int(10));

由此,我们就成功构建了一个 shared_ptr 智能指针,其指向一块存有 10 这个 int 类型数据的堆内存空间。

同时,C++11 标准中还提供了 std::make_shared<T> 模板函数,其可以用于初始化 shared_ptr 智能指针,例如:

std::shared_ptr<int> p3 = std::make_shared<int>(10);

以上 2 种方式创建的 p3 是完全相同。

3) 除此之外,shared_ptr<T> 模板还提供有相应的拷贝构造函数和移动构造函数,例如:

//调用拷贝构造函数
std::shared_ptr<int> p4(p3);//或者 std::shared_ptr<int> p4 = p3;
//调用移动构造函数
std::shared_ptr<int> p5(std::move(p4)); //或者 std::shared_ptr<int> p5 = std::move(p4);

有关拷贝构造函数,读者可阅读《C++拷贝构造函数》一节做系统了解;有关移动构造函数,读者可阅读《C++移动构造函数》做详细了解;有关 move() 函数的功能和用法,读者可阅读《C++11 move()》一节。

如上所示,p3 和 p4 都是 shared_ptr 类型的智能指针,因此可以用 p3 来初始化 p4,由于 p3 是左值,因此会调用拷贝构造函数。需要注意的是,如果 p3 为空智能指针,则 p4 也为空智能指针,其引用计数初始值为 0;反之,则表明 p4 和 p3 指向同一块堆内存,同时该堆空间的引用计数会加 1。

而对于 std::move(p4) 来说,该函数会强制将 p4 转换成对应的右值,因此初始化 p5 调用的是移动构造函数。另外和调用拷贝构造函数不同,用 std::move(p4) 初始化 p5,会使得 p5 拥有了 p4 的堆内存,而 p4 则变成了空智能指针。

 

std::shared_ptr<int> p4 = new int(1)

注意: 上面这种写法是错误的,因为右边得到的是一个原始指针,前面我们讲过shared_ptr本质是一个对象,将一个指针赋值给一个对象是不行的。

 

shared_ptr<int> p = make_shared<int>(1);
int *p2 = p.get();
cout<<*p2<<endl;

上面的写法,可以获取shared_ptr的原始指针。

 

3.2 shared_ptr需要注意的点

(1) 不能将一个原始指针初始化多个shared_ptr

void func2() 
{
    int *p0 = new int(1);
    shared_ptr<int> p1(p0);
    shared_ptr<int> p2(p0);
    cout<<*p1<<endl;
}

上面代码就会报错。原因也很简单,因为p1,p2都要进行析构删除,这样会造成原始指针p0被删除两次,自然要报错。

 

(2) 循环引用问题

shared_ptr最大的坑就是循环引用。引用网络上的一个例子:

struct Father
{
    shared_ptr<Son> son_;
};
​
struct Son
{
    shared_ptr<Father> father_;
};
​
int main()
{
    auto father = make_shared<Father>();
    auto son = make_shared<Son>();
​
•   father->son_ = son;
•   son->father_ = father;
​
•   return 0;
}

该部分代码会有内存泄漏问题。原因是 1.main 函数退出之前,Father 和 Son 对象的引用计数都是 2。 2.son 指针销毁,这时 Son 对象的引用计数是 1。 3.father 指针销毁,这时 Father 对象的引用计数是 1。 4.由于 Father 对象和 Son 对象的引用计数都是 1,这两个对象都不会被销毁,从而发生内存泄露。

为避免循环引用导致的内存泄露,就需要使用 weak_ptr。weak_ptr 并不拥有其指向的对象,也就是说,让 weak_ptr 指向 shared_ptr 所指向对象,对象的引用计数并不会增加。 使用 weak_ptr 就能解决前面提到的循环引用的问题,方法很简单,只要让 Son 或者 Father 包含的 shared_ptr 改成 weak_ptr 就可以了。

struct Father
{
    shared_ptr<Son> son_;
};
​
struct Son
{
    weak_ptr<Father> father_;
};
​
int main()
{
    auto father = make_shared<Father>();
    auto son = make_shared<Son>();
​
    father->son_ = son;
    son->father_ = father;
​
    return 0;
}
​

1.main 函数退出前,Son 对象的引用计数是 2,而 Father 的引用计数是 1。

2.son 指针销毁,Son 对象的引用计数变成 1。

3.father 指针销毁,Father 对象的引用计数变成 0,导致 Father 对象析构,Father 对象的析构会导致它包含的 son_ 指针被销毁,这时 Son 对象的引用计数变成 0,所以 Son 对象也会被析构。

 

3.3 shared_ptr<T>模板类提供的成员方法

为了方便用户使用 shared_ptr 智能指针,shared_ptr<T> 模板类还提供有一些实用的成员方法,它们各自的功能如表 1 所示。

成员方法名 功 能
operator=() 重载赋值号,使得同一类型的 shared_ptr 智能指针可以相互赋值。
operator*() 重载 * 号,获取当前 shared_ptr 智能指针对象指向的数据。
operator->() 重载 -> 号,当智能指针指向的数据类型为自定义的结构体时,通过 -> 运算符可以获取其内部的指定成员。
swap() 交换 2 个相同类型 shared_ptr 智能指针的内容。
reset() 当函数没有实参时,该函数会使当前 shared_ptr 所指堆内存的引用计数减 1,同时将当前对象重置为一个空指针;当为函数传递一个新申请的堆内存时,则调用该函数的 shared_ptr 对象会获得该存储空间的所有权,并且引用计数的初始值为 1。
get() 获得 shared_ptr 对象内部包含的普通指针。
use_count() 返回同当前 shared_ptr 对象(包括它)指向相同的所有 shared_ptr 对象的数量。
unique() 判断当前 shared_ptr 对象指向的堆内存,是否不再有其它 shared_ptr 对象再指向它。
operator bool() 判断当前 shared_ptr 对象是否为空智能指针,如果是空指针,返回 false;反之,返回 true。

除此之外,C++11 标准还支持同一类型的 shared_ptr 对象,或者 shared_ptr 和 nullptr 之间,进行 ==,!=,<,<=,>,>= 运算。

下面程序给大家演示了 shared_ptr 智能指针的基本用法,以及该模板类提供了一些成员方法的用法:

#include <iostream>
#include <memory>
using namespace std;
​
int main()
{
    //构建 2 个智能指针
    std::shared_ptr<int> p1(new int(10));
    std::shared_ptr<int> p2(p1);
    //输出 p2 指向的数据
    cout << *p2 << endl;
    p1.reset();//引用计数减 1,p1为空指针
    if (p1) {
        cout << "p1 不为空" << endl;
    }
    else {
        cout << "p1 为空" << endl;
    }
    //以上操作,并不会影响 p2
    cout << *p2 << endl;
    //判断当前和 p2 同指向的智能指针有多少个
    cout << p2.use_count() << endl;
    return 0;
}

 

 

4. unique_ptr(独占的智能指针)

unique_ptr是独享被管理对象指针所有权(owership)的智能指针。unique_ptr对象封装一个原始指针,并负责其生命周期。当该对象被销毁时,会在其析构函数中删除关联的原始指针。

 

4.1 unique_ptr智能指针的创建

unique_ptr<int> p(new int(5));
cout<<*p<<endl;

上面的代码就创建了一个unique_ptr。需要注意的是,unique_ptr没有复制构造函数,不支持普通的拷贝和赋值操作。因为unique_ptr独享被管理对象指针所有权,当p2, p3失去p的所有权时会释放对应资源,此时会执行两次delete p的操作。

unique_ptr<int> p(new int(5));
cout << *p << endl;
unique_ptr<int> p2(p); //会报错,unique_ptr没有复制构造函数,不支持普通的拷贝和赋值操作。
unique_ptr<int> p3 = p;//会报错

对于p2,p3对应的行,IDE会提示报错

无法引用 函数 "std::1::unique_ptr<_Tp, _Dp>::unique_ptr(const std::1::unique_ptr<int, std::1::default_delete<int>> &) [其中 _Tp=int, _Dp=std::1::default_delete<int>]" (已隐式声明) -- 它是已删除的函数

unique_ptr虽然不支持普通的拷贝和赋值操作,但却可以将所有权进行转移,使用std::move方法即可。

 unique_ptr<int> p(new int(50));
 unique_ptr<int> p2 = std::move(p);
 //error,此时p指针为空: cout<<*p<<endl; 
 cout << *p2 << endl;

unique最常见的使用场景,就是替代原始指针,为动态申请的资源提供异常安全保证。

objtype *p = new objtype();
p -> func();
delete p

前面我们分析了这部分代码的问题,如果我们修改一下

unique_ptr<objtype> p(new objtype());
p -> func();
delete p

此时我们只要unique_ptr创建成功,unique_ptr对应的析构函数都能保证被调用,从而保证申请的动态资源能被释放掉。

 

 

5. weak_ptr(弱引用的智能指针)

weak_ptr是一种不控制所指向对象生存期的智能指针,它指向一个由shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr,不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也还是会被释放。它是一种弱引用。

 

5.1 weak_ptr智能指针的创建

当我们创建一个weak_ptr时,要用一个shared_ptr来初始化它:

#include <iostream>
#include <memory>
using namespace std;

int main() 
{
    shared_ptr<int> sp(new int);

    weak_ptr<int> wp1;
    weak_ptr<int> wp2(wp1);
    weak_ptr<int> wp3(sp);
    weak_ptr<int> wp4;
    wp4 = sp;
    weak_ptr<int> wp5;
    wp5 = wp3;
    
    return 0;
}
  • weak_ptr<int> wp1; 构造了一个空 weak_ptr 对象
  • weak_ptr<int> wp2(wp1); 通过一个空 weak_ptr 对象构造了另一个空 weak_ptr 对象
  • weak_ptr<int> wp3(sp); 通过一个 shared_ptr 对象构造了一个可用的 weak_ptr 实例对象
  • wp4 = sp; 通过一个 shared_ptr 对象构造了一个可用的 weak_ptr 实例对象(这是一个隐式类型转换)
  • wp5 = wp3; 通过一个 weak_ptr 对象构造了一个可用的 weak_ptr 实例对象

 

由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock。此函数检查weak_ptr指向的对象是否存在。如果存在,lock返回一个指向共享对象的shared_ptr。与任何其他shared_ptr类似,只要此shared_ptr存在,它所指向的底层对象也就会一直存在。

void func3()
{
    shared_ptr<int> p1(new int(666));
    weak_ptr<int> wp(p1);
    cout << wp.use_count() << endl;//1
    if (shared_ptr<int> sp = wp.lock())
    {
        (*sp)++;
        cout << "sp = " << *sp << endl;
        cout << sp.use_count() << endl;//2
        cout << p1.use_count() << endl;//2
    }
}