C++智能指针核心知识梳理

一、为啥要引入智能指针

解决以下问题

  • 悬空指针被使用。有些内存资源已经被释放,但指向它的指针并没有改变指向,并且后续还在使用;

  • 二次释放:有些内存资源已经被释放,后期又试图再释放一次(重复释放同一块内存会导致程序运行崩溃);

  • 堆内存泄露(忘记释放):没有及时释放不再使用的内存资源造成内存泄漏,程序占用的内存资源越来越多;程序发生异常时内存泄露。

二、智能指针分类

2.1、[已废弃] auto_ptr

2.1.1、使用方法

auto_ptr通过类封装一层,使用析构来delete堆区数据,用法如下

#include <iostream>

using namespace std;

class Test {
public:
    Test() {
        std::cout << "Test构造" << std::endl;
    }

    Test(Test &t) {
        std::cout << " Test拷贝构造" << std::endl;
    }

    ~Test() {
        std::cout << "~Test析构" << this << std::endl;
    }
};

int main() {
    auto_ptr<int> auto_p(new int(10));

    auto_ptr<Test> auto_p1(new Test());
    auto_ptr<Test> auto_p2(new Test());
    auto_p1 = auto_p2;
}

运行结果

image-20220127202810611
2.1.2、废弃原因

如下代码,对datas赋值给p,p = datas[1];导致datas中数据datas[1]为空,程序崩溃

#include <iostream>
#include <vector>

using namespace std;

class Test {
public:
    int num = -1;

    Test(int num) {
        this->num = num;
        cout << "Test构造" << endl;
    }

    Test(Test &t) {
        cout << " Test拷贝构造" << endl;
    }

    ~Test() {
        cout << "~Test析构" << this << endl;
    }
};

int main() {

    auto_ptr<Test> datas[3] =
            {
                    auto_ptr<Test>(new Test(0)),
                    auto_ptr<Test>(new Test(1)),
                    auto_ptr<Test>(new Test(2))
            };
    cout << "datas" << sizeof(datas) / sizeof(auto_ptr<Test>) << endl;
    auto_ptr<Test> p;
    p = datas[1]; //datas[1]将所有权转给p,此时datas[1]不再指向"data2"字符串而变成空指针。
    for (int i = 0; i < 3; i++) {
        cout << datas[i]->num << endl; //i=1时,程序崩溃,使用shared_ptr、unique_ptr可以避免程序本刊问题
    }
    cout << "------------*****************-------------" << endl;
    cout << p->num << endl;
}

运行结果,报错,在datas[1]的时候报错

image-20220127202810611

auto_ptr智能指针在C++11中已经废弃。被废弃的原因是:
它可能导致对同一块堆空间进行多次delete。当两个智能指针都指向同一个堆空间时,每个智能指针都会delete一下这个堆空间,这会导致未定义行为。

image-20220127202810611
2.1.3、模拟实现一个auto_ptr

简单实现一个,新建auto_ptr.cpp

1、声明泛型指针保存外部数据

2、class析构时释放泛型指针

3、重写取值操作符*给外部调用

4、重写赋值操作符=,将数据赋值到智能指针类的变量_M_ptr上。

//代理类
template<typename T>
class auto_ptr {
private:
    //外部的数据
    T *_M_ptr;
public:
    explicit auto_ptr(T *__p = 0) throw(): _M_ptr(__p) {}
//    ~auto_ptr() {
//        count--;
//       if(count < 1){
//           delete _M_ptr;
//       }
//    }

    ~auto_ptr() {
        //能不能判断一下这个数据是否一个回收?
        delete _M_ptr;
    }

    T *operator*() {
        return _M_ptr;
    }

    auto_ptr<T> *operator=(auto_ptr<T> &tp) {
        _M_ptr = tp._M_ptr;
        return this;
    }

    auto_ptr<T> operator=(T &tp) {
        _M_ptr = tp;
        return this;
    }
};

使用把std改成自己的包,运行

//using namespace std;
#include "auto_ptr.cpp"
image-20220127202810611

这里对析构实现比较粗糙,所以报错,且官方对auto_ptr后期进行了一定的优化,但是不影响结论:

这里的缺陷在于存在同一块堆空间进行多次释放会产生问题,因为auto_ptr的所有权独有,所以防止两个auto_ptr对象指向同一块内存。这样会导致程序潜在的内存崩溃,这也是摒弃auto_ptr的原因

2.2、独享智能指针 unique_ptr

unique_ptr(一种强引用)
正如它的名字,独占 是它最大的特点。
特性就是内存唯一
当你需要一个独占资源所有权(访问权+生命控制权)的指针,且不允许任何外界访问,请使用std::unique_ptr

2.2.1、使用方法

注意这里的new int(123)与std::make_unique(123)没有区别,但是官方建议使用std::make_unique,原因是官方不想要在你的代码看到有任何new

int main()
{
    //初始化方式1
    std::unique_ptr<int> up1(new int(123));
    //初始化方式2
    std::unique_ptr<int> up2;
    up2.reset(new int(123));
    //初始化方式3 (-std=c++14) 更安全
    std::unique_ptr<int> up3 = std::make_unique<int>(123);
}
2.2.2、move剪切

unique_str没有赋值=,只有剪切move

下面示例说明剪切后原数据就没有了

int main()
{
    std::unique_ptr<int> up1(std::make_unique<int>(123));
    std::unique_ptr<int> up2(std::move(up1));  //通过移动实现了复制操作
    std::cout << ((up1.get() == nullptr) ? "up1 is NULL" : "up1 is not NULL") << std::endl;

    std::unique_ptr<int> up3;
    up3 = std::move(up2);    //通过移动实现了复制操作
    std::cout << ((up2.get() == nullptr) ? "up2 is NULL" : "up2 is not NULL") << std::endl;

    return 0;
}
//up1 is NULL
//up2 is NULL

2.3、强引用智能指针 shared_ptr

一种强引用指针
多个shared_ptr指向同一处资源,当所有shared_ptr都全部释放时,该处资源才释放。
(有某个对象的所有权(访问权,生命控制权) 即是 强引用,所以shared_ptr是一种强引用型指针)
当你需要一个共享资源所有权(访问权+生命控制权)的指针,请使用std::shared_ptr

2.3.1、使用方法

创建智能指针时必须提供额外的信息,指针可以指向的类型
std::unique_ptr 对其持有的资源具有独占性,而 std::shared_ptr 持有的资源可以在多个 std::shared_ptr 之间共享,每多一个 std::shared_ptr 对资源的引用,资源引用计数将增加 1,每一个指向该资源的 std::shared_ptr 对象析构时,资源引用计数减 1,最后一个 std::shared_ptr 对象析构时,发现资源计数为 0,将释放其持有的资源。
多个线程之间,递增和减少资源的引用计数是安全的。(注意:这不意味着多个线程同时操作 std::shared_ptr 引用的对象是安全的)。std::shared_ptr 提供了一个 use_count() 方法来获取当前持有资源的引用计数。除了上面描述的,std::shared_ptr 用法和 std::unique_ptr 基本相同。

int main()
{
    //初始化方式1
    std::shared_ptr<int> sp1(new int(123));

    //初始化方式2
    std::shared_ptr<int> sp2;
    sp2.reset(new int(123));

    //初始化方式3
    std::shared_ptr<int> sp3;
    sp3 = std::make_shared<int>(123);   //make_shared 去初始化

    return 0;
}
2.3.2、优点

优点是存在引用计数

下面示例,初始化后引用计数为1,添加引用后计算为2,reset释放后引用计数为1,在大括号作用域中添加变为2,作用域结束又恢复为1,最后打印还是1

class A {
public:
    A() {
        std::cout << "A constructor" << std::endl;
    }

    ~A() {
        std::cout << "~A destructor" << std::endl;
    }
};

int main() {
    {
        //初始化方式1
        std::shared_ptr<A> sp1(new A());

        std::cout << "use count: " << sp1.use_count() << std::endl;

        //初始化方式2
        std::shared_ptr<A> sp2(sp1);
        std::cout << "use count: " << sp1.use_count() << std::endl;

        //主动释放SP2的所有引用计数!
        sp2.reset();
        std::cout << "use count: " << sp1.use_count() << std::endl;

        {
            std::shared_ptr<A> sp3 = sp1;
            std::cout << "use count: " << sp1.use_count() << std::endl;
        }

        std::cout << "use count: " << sp1.use_count() << std::endl;
    }
    return 0;
}
//A constructor
//use count: 1
//use count: 2
//use count: 1
//use count: 2
//use count: 1
//~A destructor
2.3.3、缺点

share_ptr不能引用同一个地址,会报错

int main() {
    string s = "p";
    shared_ptr<string> p = make_shared<string>(s);
    /*不要这样做*/
    shared_ptr<string> p1 = make_shared<string>(s);
    cout<<*p<<endl;
    cout<<*p1<<endl;

    auto *p0 = new std::string("hello");
    std::shared_ptr<std::string> p2(p0);
    /*不要这样做*/
    std::shared_ptr<std::string> p3(p0);
    cout<<*p2<<endl;
    cout<<*p3<<endl;
    return 0;
}
image-20220127202810611

2.4、弱引用智能指针 weak_ptr

一种弱引用指针
weak_ptr是为了辅助shared_ptr的存在,它只提供了对管理对象的一个访问手段,同时也可以实时动态地知道指向的对象是否存活。
(只有某个对象的访问权,而没有它的生命控制权 即是 弱引用,所以weak_ptr是一种弱引用型指针)
当你需要一个能访问资源,但不控制其生命周期的指针,请使用std::weak_ptr

2.4.1、shared_ptr缺点

在使用shared_ptr智能指针时,假设设计一个二叉树,并在其中包含一个指向左右子节点的指针,并使改节点的左右节点构成循环引用

image-20220127202810611

代码实现如下

#include <iostream>
#include <memory>

using namespace std;

class Node {
    int value;
public:
    shared_ptr<Node> leftPtr;
    shared_ptr<Node> rightPtr;
    //如果给每个节点添加一个父节点时,则导致share_ptr内测泄漏。
    shared_ptr<Node> parentPtr;
    //使用弱引用进行处理,交叉引用不递增计数器
//    weak_ptr<Node> parentPtr;

    Node(int val) : value(val) {
        cout << "构造" << endl;
    }

    ~Node() {
        cout << "析构" << endl;
    }
};

int main() {
    shared_ptr<Node> ptr = make_shared<Node>(4);
    cout << "ptr添加时,ptr引用计数:" << ptr.use_count() << endl;
    ptr->leftPtr = std::make_shared<Node>(2);
    cout << "ptr->leftPtr添加时,ptr引用计数:" << ptr.use_count() << endl;
    ptr->leftPtr->parentPtr = ptr;
    cout << "ptr->leftPtr->parentPtr添加时,ptr引用计数:" << ptr.use_count() << endl;
    ptr->rightPtr = std::make_shared<Node>(5);
    cout << "ptr->rightPtr添加时,ptr引用计数:" << ptr.use_count() << endl;
    ptr->rightPtr->parentPtr = ptr;
    cout << "ptr->rightPtr->parentPtr添加时,ptr引用计数:" << ptr.use_count() << endl;

    cout << "ptr->leftPtr引用计数:" << ptr->leftPtr.use_count() << endl;
    cout << "ptr->rightPtr引用计数:" << ptr->rightPtr.use_count() << endl;
    return 0;
}

打印

构造
ptr添加时,ptr引用计数:1
构造
ptr->leftPtr添加时,ptr引用计数:1
ptr->leftPtr->parentPtr添加时,ptr引用计数:2
构造
ptr->rightPtr添加时,ptr引用计数:2
ptr->rightPtr->parentPtr添加时,ptr引用计数:3
ptr->leftPtr引用计数:1
ptr->rightPtr引用计数:1

这里出现

只有构造没有析构,导致内存泄漏。

2.4.2、shared_ptr缺点

上述parentPtr换成弱引用

//如果给每个节点添加一个父节点时,则导致share_ptr内测泄漏。
//    shared_ptr<Node> parentPtr;
//使用弱引用进行处理,交叉引用不递增计数器
weak_ptr<Node> parentPtr;

再次打印有析构,正常了:

构造
ptr添加时,ptr引用计数:1
构造
ptr->leftPtr添加时,ptr引用计数:1
ptr->leftPtr->parentPtr添加时,ptr引用计数:1
构造
ptr->rightPtr添加时,ptr引用计数:1
ptr->rightPtr->parentPtr添加时,ptr引用计数:1
ptr->leftPtr引用计数:1
ptr->rightPtr引用计数:1
析构
析构
析构

三、多线程下的智能指针

3.1、自动处理指针悬空

如下面示例,hander01多线程延迟执行,p被提前释放,造成指针悬空,那么智能指针可以解决这个问题

#include <iostream>
#include <memory>
#include <thread>

using namespace std;

class A {
private:
    int number;
public:
    A() { cout << "A()" << endl; }

    ~A() { cout << "~A()" << endl; }

    void funA() {
        cout << "A is good method!" << number << endl;
    }
};

void hander01(A *p) {
    std::this_thread::sleep_for(std::chrono::seconds(2));
    p->funA();
}

int main() {
    A *p = new A();
    thread t1(hander01, p);
    delete p;
    t1.join();
}

整体运行无报错

A()
~A()
A is good method!-1304362960

3.2、同步锁

多个东西对同一个指针进行赋值,智能指针内部会有风险。取数据无所谓,写数据会有风险。

在多线程内部使用共享的智能指针的时候需要减少对智能指针的修改,或者修改的时候加上锁同步,防止出现智能指针内部的不同步行为

//加锁解决当前同步写的问题
mut.lock();

mut.unlock();

示例如下:

备注:指针上会存在先赋值再计数器加一没法同步执行的问题,这里代码执行上结果与逾期不符,有点奇怪,后面再来深究

#include <memory>
#include <thread>
#include <iostream>

using namespace std;

class PTR{

public:
    PTR()
    {
        std::cout << "PTR "  << std::endl;
    }
    ~PTR()
    {
        std::cout << "~PTR "  << std::endl;
    }
private:

};

std::shared_ptr<PTR> ptr = make_shared<PTR>();

mutex mut;

void ThreadFunc(int num) {
    //加锁解决当前同步写的问题
    mut.lock();

    //内部有风险,取数据无所谓,如果是写数据
    shared_ptr<PTR> innerPtr = ptr;
    std::cout  << ptr.use_count() << std::endl;

    mut.unlock();
}

int main() {

    std::thread t1(ThreadFunc, 1);
    std::thread t2(ThreadFunc, 2);
    std::thread t3(ThreadFunc, 3);
    std::thread t4(ThreadFunc, 4);
    std::thread t5(ThreadFunc, 5);
    t1.join();

    system("pause");

    return 0;
}

示例部分原理

image-20220127202810611
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流星雨在线

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值