这次肯定可以看懂C++智能指针,适合新手小白,C++内功修炼

Posted CodeBowl

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了这次肯定可以看懂C++智能指针,适合新手小白,C++内功修炼相关的知识,希望对你有一定的参考价值。

智能指针的本质

类。
智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。

为什么要使用智能指针?

c++的内存管理是让很多人头疼的事,当我们写一个new语句时,一般就会立即把delete语句直接也写了,但是我们不能避免程序还未执行到delete时就跳转了或者在函数中没有执行到最后的delete语句就返回了,如果我们不在每一个可能跳转或者返回的语句前释放资源,就会造成内存泄露。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。

智能指针的原理

智能指针是利用RAII或者“获取资源即初始化”思想来实现的。RAII的主要原则是为将任何堆分配资源的所有权提供给其析构函数包含用于删除或释放资源的代码以及任何相关清理代码的堆栈分配对象。也就是,利用C++对象生命周期的概念来控制程序的资源,确保资源能得到及时释放。
当然上述思想仅是基础,在此之上还需要添加诸多实现才能得到我们的智能指针。其中一种比较通用的技术是使用引用计数,将一个计数器与类指向的对象相关联,引用技术跟踪该类有多少个对象的指针指向同一对象。
比如常见的智能指针shared_ptr,多个指针共享一个引用计数器,其引用规则如下:
• 当一个shared_ptr被赋值或者拷贝构造给其他shared_ptr之时,共享引用计数器自增1。
• 当一个shared_ptr被析构或者被用于管理其他裸指针之时,共享引用计数器自减1。
• 当引用计数器为0时,也就是这是管理该指针的最后一个shared_ptr,此时会释放该指针指向的资源。
在RAII的基础上建立合适的引用计数规则,确保智能指针所指向的对象,总能得到正确的释放。

智能指针的类型

智能指针,类型上可以分为两类:
• 独占型:如std::unique_ptr,一份资源,仅能由一个std::unique_ptr对象管理;
• 共享型:如std::shared_ptr,一份资源,可以由多个std::shared_ptr对象共同管理,当没有std::shared_ptr对象指向这份的资源,资源才会被释放,即基于引用技术原理。
细分: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11支持,并且第一个已经被c++11弃用。

auto_ptr

这个指针,现在已经被C++11标准所抛弃,是C++03的失败品。
auto_ptr的出现,主要是为了解决“被异常抛出时发生资源泄漏”的问题。即如果我们让资源在局部对象构造时分配,在局部对象 析构时释放。这样即使在函数执行过程时发生异常退出,也会因为异常能保证局部对象被析构从而保证资源被释放。auto_ptr就是基于这个理念而设计。

特点

  • 1.析构函数会删除它所拥有的对象,并且释放内存;auto_ptr析构的时候肯定会删除他所拥有的那个对象,所有我们就要注意了,一个萝卜一个坑,两个auto_ptr不能同时拥有同一个对象。像这样:
int* p = new int(0);
auto_ptr ap1(p);
auto_ptr ap2(p);

因为ap1与ap2都认为指针p是归它管的,在析构时都试图删除p, 两次删除同一个对象的行为在C++标准中是未定义的。所以我们必须防止这样使用auto_ptr.

  • 2.auto_ptr对指针完全占有,也就是说一个”裸“指针不能同时被两个以上的auto_ptr所拥有。 因此在拷贝构造与赋值时:

    • (1). 对智能指针进行赋值时,如ptest2 =ptest,ptest2会接管ptest原来的内存管理权,ptest会变为空指针,如果ptest2原来不为空,则它会释放原来的资源,基于这个原因,应该避免把auto_ptr放到容器中,因为算法对容器操作时,很难避免STL内部对容器实现了赋值传递操作,这样会使容器中很多元素被置为NULL。

缺陷

auto_ptr采用copy语义来转移指针资源,转移指针资源的所有权的同时将原指针置为NULL,这跟通常理解的copy行为是不一致的(不会修改原数据),而这样的行为在有些场合下不是我们希望看到的。

unique_ptr

unique_ptr是取代auto_ptr的产物,也是它的升级版。
unique_ptr 是一个独享所有权的智能指针,它提供了严格意义上的所有权,包括:

  1. 拥有它指向的对象
  2. 无法进行复制构造,无法进行复制赋值操作。即无法使两个unique_ptr指向同一个对象。但是可以进行移动构造和移动赋值操作
  3. 保存指向某个对象的指针,当它本身被删除释放的时候,会使用给定的删除器释放它指向的对象

功能

unique_ptr
可以实现如下功能:

  1. 为动态申请的内存提供异常安全
  2. 讲动态申请的内存所有权传递给某函数
  3. 从某个函数返回动态申请内存的所有权
  4. 在容器中保存指针

实验

#include<iostream>
#include<Windows.h>
#include<memory>
using namespace std;
class Test
{
public:
    Test(string s)
    {
        str = s;
        cout << "Test creat\\n";
    }
    ~Test()
    {
        cout << "Test delete:" << str << endl;
    }
    string& getStr()
    {
        return str;
    }
    void setStr(string s)
    {
        str = s;
    }
    void print()
    {
        cout << str << endl;
    }
private:
    string str;
};
unique_ptr<Test> fun()
{
    return unique_ptr<Test>(new Test("789"));
}
int main()
{
    unique_ptr<Test> ptest(new Test("123"));
    unique_ptr<Test> ptest2(new Test("456"));
    ptest->print();
    ptest2 = std::move(ptest);//不能直接ptest2 = ptest
    if (ptest == NULL)
        cout << "ptest = NULL\\n";
    Test* p = ptest2.release();
    p->print();
    ptest.reset(p);
    ptest->print();
    ptest2 = fun(); //这里可以用=,因为使用了移动构造函数    
    ptest2->print();
    return 0;
}

在这里插入图片描述

unique_ptr 和 auto_ptr用法很相似,不过不能使用两个智能指针赋值操作,应该使用std::move; 而且它可以直接用if(ptest == NULL)来判断是否空指针;release、get、reset等用法也和auto_ptr一致,使用函数的返回值赋值时,可以直接使用=, 这里使用c++11 的移动语义特性。另外注意的是当把它当做参数传递给函数时(使用值传递,应用传递时不用这样),传实参时也要使用std::move,比如foo(std::move(ptest))。它还增加了一个成员函数swap用于交换两个智能指针的值。

share_ptr

share_ptr是共享型智能指针。
资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。
除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。

成员函数

use_count 返回引用计数的个数
unique 返回是否是独占所有权( use_count 为 1)
swap 交换两个 shared_ptr 对象(即交换所拥有的对象)
reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少
get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.
缺陷
share_ptr虽然已经很好用了,但是有一点share_ptr智能指针还是有内存泄露的情况,当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。

#include <iostream>  
using namespace std;  
#include <memory>  
  
class B;  
  
class A  
{  
public:  
    shared_ptr<B> ptrA_B;  
public:  
    A()  
    {  
        cout << "调用class A的默认构造函数" << endl;  
    }  
    ~A()  
    {  
        cout << "调用class A的析构函数" << endl;  
    }  
};  
  
class B  
{  
public:  
    shared_ptr<A> ptrB_A;  
public:  
    B()  
    {  
        cout << "调用class B的默认构造函数" << endl;  
    }  
    ~B()  
    {  
        cout << "调用class B的析构函数" << endl;  
    }  
};  
  
int main()  
{  
    shared_ptr<B> ptrB = make_shared<B>();  
    shared_ptr<A> ptrA = make_shared<A>();  
    ptrA->ptrA_B = ptrB;  
    ptrB->ptrB_A = ptrA;  
}  

在堆区中构建的class A和class B对象未析构,内存未被释放,这就造成了内存泄漏。
在这里插入图片描述

class A中包含着指向堆区构建的class B对象的引用计数指针,而class B中也包含着指向堆区构建的class A对象的引用计数指针,这样就构成了下面一个环形结构:
在这里插入图片描述

这样的话,对于class A对象来说,每当class A对象生命结束时,指向在堆区构建的class B对象的shared_ptr指针就会调用class B的析构函数结束class B对象的声明,但此时class B对象中也包含有指向“在堆区构架的class A对象”的shared_ptr指针,也得执行对class A对象的析构,如此往复进入了死循环。

实例

int main()
{
    string *s1 = new string("s1");
    shared_ptr<string> ps1(s1);
    shared_ptr<string> ps2;
    ps2 = ps1;
    cout << ps1.use_count()<<endl;  //2
    cout<<ps2.use_count()<<endl;    //2
    cout << ps1.unique()<<endl; //0
    string *s3 = new string("s3");
    shared_ptr<string> ps3(s3);
    cout << (ps1.get()) << endl;    //033AEB48
    cout << ps3.get() << endl;  //033B2C50
    swap(ps1, ps3); //交换所拥有的对象
    cout << (ps1.get())<<endl;  //033B2C50
    cout << ps3.get() << endl;  //033AEB48
    cout << ps1.use_count()<<endl;  //1
    cout << ps2.use_count() << endl;    //2
    ps2 = ps1;
    cout << ps1.use_count()<<endl;  //2
    cout << ps2.use_count() << endl;    //2
    ps1.reset();    //放弃ps1的拥有权,引用计数的减少
    cout << ps1.use_count()<<endl;  //0
    cout << ps2.use_count()<<endl;  //1
}

weak_ptr

weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

weak_ptr是什么

weak_ptr 是为了配合 shared_ptr 而引入的一种智能指针,它指向一个由 shared_ptr 管理的对象而不影响所指对象的生命周期,也就是将一个 weak_ptr 绑定到一个 shared_ptr 不会改变 shared_ptr 的引用计数。
不论是否有 weak_ptr 指向,一旦最后一个指向对象的 shared_ptr 被销毁,对象就会被释放。
从这个角度看,weak_ptr更像是shared_ptr的一个助手而不是智能指针。

weak_ptr如何使用

创建
创建一个 weak_ptr 指针,有以下 3 种方式:

  1. 可以创建一个空 weak_ptr 指针,例如:
 `std::weak_ptr<int> wp1;`
  1. 凭借已有的 weak_ptr 指针,可以创建一个新的 weak_ptr 指针,
    例如:
std::weak_ptr<int> wp2 (wp1);

若 wp1 为空指针,则 wp2 也为空指针;反之,如果 wp1 指向某一 shared_ptr 指针拥有的堆内存,则 wp2 也指向该块存储空间(可以访问,但无所有权)。

  1. weak_ptr 指针更常用于指向某一 shared_ptr 指针拥有的堆内存,因为在构建 weak_ptr
    指针对象时,可以利用已有的 shared_ptr 指针为其初始化。例如:
1. std::shared_ptr<int> sp (new int);
2. std::weak_ptr<int> wp3 (sp);

由此,wp3 指针和 sp 指针有相同的指针。再次强调,weak_ptr 类型指针不会导致堆内存空间的引用计数增加或减少。

weak_ptr模板类提供的成员方法

和 shared_ptr、unique_ptr 相比,weak_ptr 模板类提供的成员方法不多,表 1 罗列了常用的成员方法及各自的功能。
表 1 weak_ptr指针可调用的成员方法
在这里插入图片描述

再次强调,weak_ptr 模板类没有重载 * 和 -> 运算符,因此 weak_ptr 类型指针只能访问某一 shared_ptr 指针指向的堆内存空间,无法对其进行修改。

参考资料

【1】https://www.cnblogs.com/WindSun/p/11444429.html
【2】https://blog.csdn.net/weixin_45590473/article/details/113057545
【3】C++11 weak_ptr智能指针(一看即懂)http://c.biancheng.net/view/7918.html

小白进阶
大学生到工程师的打怪升级之路
持续更新

以上是关于这次肯定可以看懂C++智能指针,适合新手小白,C++内功修炼的主要内容,如果未能解决你的问题,请参考以下文章

从C语言到C++你必须学会的---动态内存和智能指针

从C语言到C++你必须学会的---动态内存和智能指针

智能优化算法——粒子群优化算法(PSO)(小白也能看懂)

小白学习C++ 教程七在C++指针声明和指针相关概念

自学C++

自学C++