【c++】智能指针

智能指针

智能指针

在c++中,我们需要注意对内存资源的管理,若在程序中发生内存泄漏,这是一个非常可怕的事情。刚开始可能无事发生,但是随着内存资源的泄漏,最后就会导致程序崩溃。

但是内存资源也是一件麻烦事,所以c++提供了智能指针,将申请到的内存对象交给智能指针对象来管理。

RAII和智能指针的设计思路

RAII是ResourceAcquisition Is Initialization,即资源申请立即初始化。利用对象的生命周期来管理内存资源、网络连接、互斥锁等。RAII在获取到资源后,立即将资源交付给对象,在对象生命周期内,资源始终有效,当对象析构时,资源释放,这样就避免了资源泄漏问题

cpp 复制代码
#include<iostream>
#include<memory>
{
	shared_ptr<string> sp1=make_shared<string>("111");
	unique_ptr<string> sp1=make_shared<string>("111");
	return 0;
}

什么是内存泄漏

内存泄漏指因为疏忽或错误 造成程序未能释放已经不再使⽤的内存 ,⼀般是忘记释 放或者发⽣异常释放程序未能执⾏导致的。

内存泄漏并不是指内存在物理上的消失,⽽是应⽤程序分 配某段内存后,因为设计错误,失去了对该段内存的控制,因⽽造成了内存的浪费。

内存泄漏的危害

内存泄漏的危害:普通程序运⾏⼀会就结束了出现内存泄漏问题也不⼤,进程正常结束,⻚表的映射 关系解除,物理内存也可以释放。⻓期运⾏的程序出现内存泄漏,影响很⼤,如操作系统、后台服 务、⻓时间运⾏的客⼾端等等,不断出现内存泄漏会导致可⽤内存不断变少,各种功能响应越来越 慢,最终卡死。

内存泄漏场景

下面以抛异常举例

cpp 复制代码
void  David(int x, int b)
{
    if (b == 0)
        throw ("b不能为0");
}


void func()
{
    int* arr1 = new int[10];
    int* arr2 = new int[10];

    try
    {
 			//若这里抛出除0异常,且当前作用域的catch不匹配
 			//程序会跳到main函数中,下面catch中的delete不会被执行
 			//资源不会被释放,造成资源泄漏
        int len = 0, time = 0;
        cin >> len >> time;
        David(len, time);
    }
    catch(const char*errmsg)
    {
    //每次都要对异常进行捕捉,然后释放资源,再抛出异常
        delete[] arr1;
        delete[] arr2;
        throw;
    }

}

int main()
{
    try
    {
        func();
    }
    catch (const char* errmsg)
    {
        cout << errmsg << endl;
    }
    catch (const exception& errmsg)
    {
        cout << errmsg.what() << endl;
    }
    catch (...)
    {
        cout << "未知错误" << endl;
    }
    return 0;
}

C++标准库智能指针的使⽤

C++标准库中的智能指针都在<memory>头⽂件下⾯,我们包含就可以是使⽤了, 智能指针有好⼏种,除了weak_ptr他们都符合RAII和像指针⼀样访问的⾏为,原理上而言主要是解决智能指针拷贝时的思路不同。

auto_ptr

auto_ptr是在c++98提出的,这是一个非常糟糕的设计,因为在拷贝时,会发生资源管理权转移的问题,造成智能指针对象悬空,访问报错的问题。

cpp 复制代码
auto_ptr<int>ap1(new int(1));

ap1在拷贝构造ap2时,资源的管理权被转移到了ap2,ap1悬空。

所以对于auto_ptr,尽量不要用

unique_ptr

是C++11设计出来的智能指针,唯一指针,不支持拷贝,只支持移动。如果不需要拷⻉的场景就⾮常建议使⽤他。

它也会发生资源管理权的转移,但与auto_ptr不同的是,unique_ptr是否对资源管理权的转移,是由程序员决定的,而auto_ptr发生资源管理权转移时,程序员自己可能都不知道。

cpp 复制代码
int main()
{
	unique_ptr<Date>up1(new Date);
	unique_ptr<Date>up2(move(up1));
	return 0;
}


unique_ptr只支持移动构造,不支持拷贝构造,所以当发生资源管理权转移时,必定是程序员对unique_ptr对象move了。资源管理权转移变的可控了

shared_ptr

是C++11设计出来的智能指针,共享指针,支持拷贝构造也支持移动构造,需要进行拷贝的场景就要使用它。底层是⽤引⽤计数的⽅式实现的。

  • 它允许多个shared_ptr指针指向同一块资源,底层用一个"计数器"来记录。
cpp 复制代码
class  Date
{
public:
	   Date(int year = 1, int month = 1, int day = 1)
	           :_year(year),
	           _month(day),
	           _day(day)
        {
        }
private:
        int _year;
        int _month;
        int _day;
};

int main()
{
    shared_ptr<Date>sp1(new Date);
    shared_ptr<Date>sp2(sp1);
    return 0;
}

use_count()接口返回有多少shared_ptr在管理该资源

shared_ptr支持new type[]

cpp 复制代码
shared_ptr<Date[]>sp1(new Date[]);

说到这个,就要提到另一个东西"定制删除器"

cpp 复制代码
template <class U, class D> shared_ptr (U* p, D del);

删除器可以是:

  • lambda
  • 仿函数
  • 函数指针
cpp 复制代码
//传lambda
 shared_ptr<Date>sp2(new Date[10], [](Date* ptr) {delete[]ptr; });
 //传入放函数
 shared_ptr<FILE>sp3(fopen("...","r"),Fclose());
 //传函数指针
 shared_ptr<Date>sp3(new Date[10],Del<Date>);
shared_ptr循环引用问题

我们知道,shared_ptr在底层,会维护一个计数器,来记录有多个shared_ptr在管理该资源,这个设计避免了因为一个对象析构,释放了资源导致其他管理该资源的对象悬空。但同时页引出了一个新问题"循环引用"

cpp 复制代码
struct ListNode
{
	shared_ptr<ListNode>*_next;
	shared_ptr<ListNode>*_prev;

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

int main()
{
    shared_ptr<ListNode>sp1(new ListNode);
    shared_ptr<ListNode>sp2(new ListNode);

    sp1->_next = sp2;
    sp2->_prev = sp1;

    return 0;
}
  • n1与n2是双向链表中的节点。sp1管理着n1节点,sp2管理着n2节点,此时让n1的_next指针指向sp2管理的资源,也就是n2节点,再让n2的_prev指针指向sp1管理的资源,也就是n1。
  • 运行程序,当程序结束时,n1与n2因该被析构,终端会打印 "~ListNode()" ,但是结果是并没有打印。


这是因为n1与n2节点的_next、_prev是智能指针,这两个节点互相指向时,shared_ptr底层对应的计数器++,即使sp1与sp2析构了,计数器--,但由于_next、_prev的原因,资源对应的计数器始终不会减为0,导致资源无法正常释放,内存泄漏。这是为什么?

如果n1节点想要释放,就需要n2节点_prev对象析构,而_prev对象析构就需要n2节点释放,而n2节点释放,就需要n1节点的_next对象析构,而_next对象析构,就需要n1节点释放,此时,形成循环,导致n1与n2节点都无法释放

为了解决循环引用问题,c++提供了weak_ptr

weak_ptr和shared_ptr

weak_ptr其实并不是智能指针。它本质就是为了解决循环引用的问题而提出的。

  • 当weak_ptr指向share_ptr管理的资源时,并不会增加引用计数。
  • 但是,引用计数还被一个count维护,当weak_ptr指向shared_ptr管理的资源时,count++。
    像上面提到的循环引用问题,为了解决该问题,我们需要将ListNode的代码该动一下
cpp 复制代码
struct ListNode
{
    //shared_ptr< ListNode> _prev;
    //shared_ptr< ListNode> _next;

    weak_ptr<ListNode>_prev;
    weak_ptr<ListNode>_next;


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

sp1管理n1节点,sp2管理n2节点,即使n1与n2节点互相指向,但是引用计数也不会++,因为ListNode的两个指针是weak_ptr,不会增加引用计数,只会增加count。

当sp1与sp2析构时,引用计数--,变为0,weak_ptr检测到计数器变为0,说明该资源过期了,就不会指向该资源了,count--,若此时count减为0,说明没有weak_ptr指向该资源了,就将资源释放了,反之就不释放。

shared_ptr线程安全问题

shared_ptr本身是线程安全的,但是指向的对象不是线程安全的,当多线程场景时,需要对临界资源进行加锁

cpp 复制代码
struct AA
{
        int _a1 = 0;
        int _a2 = 0;

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

};


int main()
{
        shared_ptr<AA>p1(new AA);

        mutex _mutex;

        auto func = [&]() {
                for (int i = 0; i < 10000; i++)
                {
                        shared_ptr<AA>copy(p1);
                        {
                                unique_lock<mutex>lx(_mutex);
                                copy->_a1++;
                                copy->_a2++;
                        }
                }
        };

        thread t1(func);
        thread t2(func);

        t1.join();
        t2.join();

        cout << p1->_a1 << endl;
        cout << p1->_a2 << endl;

        return 0;
}
相关推荐
杜子不疼.1 小时前
【C++ AI 大模型接入 SDK】 - LLMProvider 抽象基类与策略模式
开发语言·c++·策略模式
BirdenT4 小时前
20260519紫题训练
c++·算法
C+++Python12 小时前
C++ 进阶学习完整指南
java·c++·学习
sparEE12 小时前
c++值类别、右值引用和移动语义
开发语言·c++
jrrz082813 小时前
Apollo MPC Controller
c++·自动驾驶·apollo·mpc·横向控制·lateral control
小王C语言15 小时前
【线程概念与控制】:线程封装
jvm·c++·算法
学习,学习,在学习15 小时前
Qt工控仪器程序框架设计详解(工控多仪器控制版本)
开发语言·c++·qt
信竞星球_少儿编程题库15 小时前
2026年全国信息素养大赛算法应用主题赛 丝路新城 C++ 模拟卷(三)
开发语言·c++
Zhang~Ling15 小时前
深入解析C++list:从0到1实现一个完整的链表类
c++·链表·list