c++智能指针的超详细讲解

 更新时间:2022年06月27日 09:59:50   作者:bitcarmanlee  
c++程序设计中经常会用堆内存,程序员要自己管理内存的申请和释放,使用原始指针,容易造成堆内存泄漏(忘记释放),二次释放,使用智能指针能更好的管理堆内存,下面这篇文章主要给大家介绍了关于c++智能指针的相关资料,需要的朋友可以参考下

1.什么是智能指针

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

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

2.原始指针的问题

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

我们看个简单的例子

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

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

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

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

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

3.unique_ptr

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

创建unique_ptr:

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

void f1() {
    unique_ptr<int> p(new int(5));
    cout<<*p<<endl;
}

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

void f1() {
    unique_ptr<int> p(new int(5));
    cout<<*p<<endl;
    unique_ptr<int> p2(p);
    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方法即可。

void f1() {
    unique_ptr<int> p(new int(5));
    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对应的析构函数都能保证被调用,从而保证申请的动态资源能被释放掉。

4.shared_ptr

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

shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,删除所指向的堆内存。

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

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

void f2() {
    shared_ptr<int> p = make_shared<int>(1);
    shared_ptr<int> p2(p);
    shared_ptr<int> p3 = p;
}

以上写法都是可以的

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

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

5.shared_ptr使用需要注意的点

5.1 不能将一个原始指针初始化多个shared_ptr

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

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

5.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 对象也会被析构。

6.智能指针小结

我们该如何选择智能指针:

如果程序要使用多个指向同一个对象的指针,应选择 shared_ptr。这样的情况包括

1.有一个指针数组,并使用一些辅助指针来标示特定的元素,如最大的元素和最小的元素;

2.两个对象包含都指向第三个对象的指针;

3.STL 容器包含指针。很多 STL 算法都支持复制和赋值操作,这些操作可用于 shared_ptr,但不能用于 unique_ptr(编译器发出 warning)和 auto_ptr(行为不确定)。如果你的编译器没有提供 shared_ptr,可使用 Boost 库提供的 shared_ptr。

如果程序不需要多个指向同一个对象的指针,则可使用 unique_ptr。如果函数使用 new 分配内存,并返还指向该内存的指针,将其返回类型声明为 unique_ptr 是不错的选择。这样,所有权转让给接受返回值的 unique_ptr,而该智能指针将负责调用 delete。

参考文献

https://zhuanlan.zhihu.com/p/461837602

总结

到此这篇关于c++智能指针的文章就介绍到这了,更多相关c++智能指针内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • qt获取当前时间(QDateTime、QTime、QDate)

    qt获取当前时间(QDateTime、QTime、QDate)

    本文主要介绍了qt获取当前时间(QDateTime、QTime、QDate),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-04-04
  • c++中的基本IO类型详解

    c++中的基本IO类型详解

    IO的设备可以是文件、控制台、string。c++主要定义了三种IO类型,分别被包含在iostream、fstream、sstream头文件中,本文给大家分享c++中的基本IO类型,感兴趣的朋友跟随小编一起看看吧
    2021-07-07
  • C语言实现简单的三子棋项目

    C语言实现简单的三子棋项目

    这篇文章主要为大家详细介绍了C语言实现简单的三子棋项目,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-08-08
  • C++实现学生档案管理系统

    C++实现学生档案管理系统

    这篇文章主要为大家详细介绍了C++实现学生档案管理系统,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-05-05
  • C语言 深入理解动态规划之计数类DP

    C语言 深入理解动态规划之计数类DP

    动态规划可谓是大名鼎鼎,笔试面试中的高频考点,也是重点难点,动态规划类型题目灵活多变,难度系数也相对较高,往往我们做不好动态规划的题目就会与心仪的offer失之交臂,本篇文章我们就一起来研究一下动态规划的计数类DP
    2022-04-04
  • QT委托代理机制之Model View Delegate使用方法详解

    QT委托代理机制之Model View Delegate使用方法详解

    这篇文章主要介绍了QT委托代理机制之Model View Delegate的使用方法,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-08-08
  • 深入浅析C++的new和delete

    深入浅析C++的new和delete

    new和delete既是C++中的关键字也是一种特殊的运算符。这篇文章主要介绍了C++的new和delete详解,需要的朋友可以参考下
    2018-02-02
  • C语言基础双指针移除元素解法

    C语言基础双指针移除元素解法

    这篇文章介绍了C语言基础双指针移除元素的解法,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-12-12
  • 利用C语言实现n字棋游戏

    利用C语言实现n字棋游戏

    本文将利用C语言编写一个n字棋游戏,和井字棋一样,不过这个游戏你可以自定义棋盘的大小。文中的示例代码讲解详细,感兴趣的小伙伴可以尝试一下
    2022-05-05
  • C语言结构体指针的具体使用

    C语言结构体指针的具体使用

    结构体指针是一种非常有用的数据类型,它可以让我们更方便地操作结构体,本文主要介绍了C语言结构体指针的具体使用,非常具有实用价值,需要的朋友可以参考下
    2023-05-05

最新评论