当前位置: 首页 > news >正文

C++11之智能指针

1. 智能指针的使用场景分析

在C++当中没有垃圾回收器的概念,因为垃圾回收器是需要付出一些代价的,C++这个语言现在偏向的使用场景还是属于性能要求比较高的地方,而且没有像Java社区那样的优势,如果去设计垃圾回收器的话,反而会不好用,失去了一些优势。因为没有垃圾回收器,在C++中new/malloc1的时候,就需要自己释放:

在C++编程中,内存管理一直是一个重要的问题。传统的手动内存管理容易导致内存泄漏等问题,尤其是在复杂的程序中,当发生异常时,很容易忘记释放内存。逻辑原因是收尾都有new-delete,但是中间抛异常的等等原因。智能指针的出现,为这些问题提供了解决方案。

下面是一个简单的例子,展示了在发生异常时,如何使用智能指针来避免内存泄漏:

我们在Func当中,array1new成功,但是下一个array2new失败,发生抛异常,这时候就需要释放array1,如果两个都成功,如果在Divide发生抛异常,这时候就需要去delete上面new出来的两个数组.....这样就会导致整个代码很复杂,因此,我们可以使用智能指针来避免内存泄漏:

double Divide(int a, int b)
{// 当b == 0时抛出异常if (b == 0){throw "Divide by zero condition!";}else{return (double)a / (double)b;}
}void Func()
{// 这里可以看到如果发生除0错误抛出异常,另外下面的array和array2没有得到释放。// 所以这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再重新抛出去。// 但是如果array2new的时候抛异常呢,就还需要套一层捕获释放逻辑,这里更好解决方案// 是智能指针,否则代码太戳了int* array1 = new int[10];int* array2 = new int[10]; // 抛异常呢try{int len, time;cin >> len >> time;cout << Divide(len, time) << endl;}catch (...){cout << "delete []" << array1 << endl;cout << "delete []" << array2 << endl;delete[] array1;delete[] array2;throw; // 异常重新抛出,捕获到什么抛出什么}// ...cout << "delete []" << array1 << endl;delete[] array1;cout << "delete []" << array2 << endl;delete[] array2;
}int main()
{try{Func();}catch (const char* errmsg){cout << errmsg << endl;}catch (const exception& e){cout << e.what() << endl;}catch (...){cout << "未知异常" << endl;}return 0;
}

在这个例子中,如果在Func函数中发生异常,array1array2可能没有被正确释放,导致内存泄漏。使用智能指针可以简化这个问题。

2. RAII和智能指针的设计思路

智能指针第一个思想时使用RAII(Resource Acquisition Is Initialization)的思想,RAII就是资源获得立即初始化,是一种管理资源的类的设计思想。它的本质是利用对象生命周期来管理获取到的动态资源,避免资源泄漏。RAII在获取资源时把资源委托给一个对象,接着控制对资源的访问,资源在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。

也就是说:智能指针的第一个设计思路是将资源的生命周期绑定到对象的生命周期上。具体来说,智能指针通过将资源的指针封装在一个对象中,使得资源的管理与对象的生命周期紧密绑定。当智能指针对象超出作用域时,它的析构函数会自动被调用,从而确保资源被正确释放。

这种设计的核心思想是利用对象的生命周期来管理资源的生命周期,避免手动释放资源可能带来的问题,比如内存泄漏、重复释放等。即使程序抛出异常,智能指针也能通过栈展开机制保证资源被正确释放,因为每个栈帧中的智能指针对象在栈展开过程中都会被正常析构。

总结来说,智能指针的第一个设计思路是通过对象化资源管理,将资源的生命周期与对象的生命周期绑定,从而实现资源的自动释放和异常安全。

C++对锁的封装也是可以很好解决某些问题:死锁问题:

lock++i;func();unlock;

当两个线程要同时访问i时,因为++i不是原子的,所以需要加锁,但是加锁了,就需要解锁,不然另外一个线程没有访问该临界区的机会,但是如果在func处抛异常了,那么持有锁的线程就会一直持有锁,到try-catch去了,这就会造成死锁,因此,对lock进行RAII的方式封装后,就不需要再担心这种抛异常带来的死锁问题了。(将锁将给一个对象去管理) 

智能指针类除了满足RAII的设计思路,还方便资源的访问。智能指针类会重载operator*operator->operator[]等运算符,方便访问资源。

template<class T>
class SmartPtr
{
public:// RAIISmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){cout << "delete[] " << _ptr << endl;delete[] _ptr;}// 重载运算符,模拟指针的行为,方便访问资源T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T& operator[](size_t i){return _ptr[i];}
private:T* _ptr;
};void Func()
{// 这里使用RAII的智能指针类管理new出来的数组以后,程序简单多了SmartPtr<int> spl = new int[10];SmartPtr<int> sp2 = new int[10];for (size_t i = 0; i < 10; i++){spl[i] = sp2[i] = i;}int len, time;cin >> len >> time;cout << Divide(len, time) << endl;
}

3. C++标准库智能指针的使用

对于智能指针要进行拷贝的时候,不可能走深拷贝:因为智能指针的作用是代管资源(不像STL容器中,目的是构建出来自己单独的属于自己的资源),智能指针是模拟指针的行为的,对于指针,智能在可以对执行的资源进行有效管理(访问,修改......资源):在进行拷贝的时候,期望的是拷贝出来的这个智能指针是可以和拷贝前的智能指针一起共同管理资源的,所以不能走深拷贝。

但是进行浅拷贝的话,就会带来一个问题:析构多次的问题。

C++标准库中的智能指针都在<memory>这个头文件下,我们包含<memory>就可以使用了,智能指针有好几种,除了weak_ptr他们都符合RAII和像指针一样访问的行为,原理上而言主要是解决智能指针拷贝时的思路不同。(就是针对拷贝的析构多次问题的解决思路)

  • auto_ptr:是C++98时设计出来的智能指针,他的特点是拷贝时把被拷贝对象的资源的管理权转移给拷贝对象,这是一个非常糟糕的设计,因为他会导致被拷贝对象悬空,访问报错的问题。C++11设计出新的智能指针后,强烈建议不要使用auto_ptr。其他C++11出来之前很多公司也是明令禁止使用这个智能指针的。

所以auto_ptr,但是不算正真的解决,只是将管理权转交给来拷贝的对象,这就使之前的被拷贝对象悬空了,像野指针,访问会报错,这就很草率!!!

  • unique_ptr:是C++11设计出来的智能指针,他的名字翻译出来是唯一指针,他的特点是不支持拷贝,只支持移动。如果不需要拷贝的场景就非常建议使用他。

  • shared_ptr:是C++11设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是支持拷贝,也支持移动。如果需要拷贝的场景就需要使用他了。底层是用引用计数的方式实现的。

  • weak_ptr:是C++11设计出来的智能指针,他的名字翻译出来是弱指针,他完全不同于上面的智能指针,他不支持RAII,也就意味着不能用它直接管理资源,weak_ptr的产生本质是要解决shared_ptr的一个循环引用导致内存泄漏的问题。具体细节我们在后面会详细讲解


智能指针析构时默认是进行delete释放资源,这也就意味着如果不是new出来的资源(比如使用new[10]),交给智能指针管理,析构时就会崩溃。智能指针支持在构造时给一个删除器,所谓删除器本质就是一个可调用对象,这个可调用对象中实现你想要的释放资源的方式,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调用删除器去释放资源。因为new[]经常使用,所以为了简洁一点,unique_ptrshared_ptr特化了一份[]的版本,使用时unique_ptr<Date[]> up1(new Date[5]);shared_ptr<Date[]> sp1(new Date[5]);就可以管理new[]的资源。

int main()
{std::shared_ptr<Date> sp1(new Date[10]);//这样会崩溃std::shared_ptr<Date[]> sp1(new Date[10]);//unique_ptr也可以这么用!!!return 0;
}

原理是使用特化!

但是如果是fopen --- close 形式的,又该如何解决?

std::shared_ptr<FILE> sp2(fopen("Test.cpp", "r"));//这样会出粗

所以基于这些原因,有一个通用的解决概念 --- 删除器!

智能指针支持在构造时给一个删除器,所谓删除器本质就是一个可调用对象(函数指针,lambda表达式,仿函数,包装器...),这个可调用对象中实现你想要的释放资源的方式,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调用删除器去释放资源。

删除器就是看成是一个模板:

template <class U, class D> shared_ptr (U* p, D del);
class Fclose
{
public:void operator()(FILE* ptr){cout << "fclose:" << ptr << endl;fclose(ptr);}
};std::shared_ptr<FILE> sp2(fopen("Test.cpp", "r"), Fclose());

观察下面的总代码!最香的删除器方式就是使用lambda表达式!

但是 unique_ptr 确是在模板参数中传递


  • template <class T, class... Args> shared_ptr<T> make_shared(Args&&... args);shared_ptr除了支持用指向资源的指针构造,还支持make_shared用初始化资源对象的值直接构造。--- 从内存碎片的角度看,make_shared 通过 合并内存分配 减少了堆内存的碎片化,同时提升了性能和安全性。在大多数情况下,应优先使用 make_shared,除非有特殊需求(如自定义删除器)。

  • shared_ptrunique_ptr都支持了operator bool的类型转换,如果智能指针对象是一个空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断是否为空。

  • shared_ptrunique_ptr的构造函数都使用explicit修饰,防止普通指针隐式类型转换成智能指针对象。

int main()
{shared_ptr<Date> sp1(new Date(2024, 9, 11));shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);auto sp3 = make_shared<Date>(2024, 9, 11);shared_ptr<Date> sp4;// if (sp1.operator bool())if (sp1)cout << "sp1 is not nullptr" << endl;if (!sp4)cout << "sp1 is nullptr" << endl;// 报错shared_ptr<Date> sp5 = new Date(2024, 9, 11);unique_ptr<Date> sp6 = new Date(2024, 9, 11);return 0;
}

总的来说:C++标准库中的智能指针都在<memory>这个头文件下。主要有以下几种:

  • auto_ptr:C++98时设计出来的智能指针,拷贝时把被拷贝对象的资源的管理权转移给拷贝对象。这个设计导致被拷贝对象悬空,访问报错的问题,C++11后强烈建议不要使用。

  • unique_ptr:C++11设计出来的智能指针,不支持拷贝,只支持移动。适合不需要拷贝的场景。

  • shared_ptr:C++11设计出来的智能指针,支持拷贝和移动。底层用引用计数的方式实现。

  • weak_ptr:C++11设计出来的智能指针,用于解决shared_ptr的循环引用导致内存泄漏的问题。

我们尽量不要使用 auto_ptr,因为它的左值拷贝会导致悬空指针的问题。auto_ptr 的设计初衷是通过左值拷贝转移资源管理权,但这种方式容易引发问题,因为左值拷贝后,原对象会变成悬空状态,而悬空指针的访问会导致未定义行为。

相比之下,现代智能指针(如 unique_ptrshared_ptr)的设计思路更倾向于右值语义。右值语义的核心思想是:

  1. 只对右值转移资源:右值通常是临时对象或匿名对象,不会被其他代码访问,因此转移资源是安全的。

  2. move 是主动行为:通过 move 显式地将资源从一个对象转移到另一个对象,转移后原对象会变成悬空状态,但这种悬空状态是预期的,不会被意外访问。

这种设计思路避免了左值拷贝带来的悬空指针问题,同时保留了资源转移的灵活性。右值设计使得智能指针在移动构造和移动赋值时更加安全和高效。


3.1 总代码:

补充:lambda的底层,默认构造是被禁用的!!!

struct Date
{int __year;int __month;int __day;Date(int year = 1, int month = 1, int day = 1):__year(year),_month(month),_day(day){}~Date(){cout << "~Date()" << endl;}
};int main()
{auto_ptr<Date> api(new Date);// 拷贝时,管理权限转移,被拷贝对象api悬空auto_ptr<Date> ap2(api);// 空指针访问,api对象已经悬空//api->_year++;unique_ptr<Date> up1(new Date);// 不支持拷贝//unique_ptr<Date> up2(up1);// 支持移动,但是移动后up1也悬空,所以使用移动要谨慎unique_ptr<Date> up3(move(up1));shared_ptr<Date> sp1(new Date);// 支持拷贝shared_ptr<Date> sp2(sp1);shared_ptr<Date> sp3(sp2);cout << sp1.use_count() << endl;sp1->_year++;cout << sp1->_year << endl;cout << sp2->_year << endl;cout << sp3->_year << endl;// 支持移动,但是移动后sp1也悬空,所以使用移动要谨慎shared_ptr<Date> sp4(move(sp1));return 0;
}template<class T>
void DeleteArrayFunc(T* ptr)
{delete[] ptr;
}template<class T>
class DeleteArray
{
public:void operator(){T* ptr}{delete[] ptr;}
};class Fclose
{
public:void operator(){FILE* ptr}{cout << "fclose:" << ptr << endl;fclose(ptr);}
};int main()
{// 这样实现程序会崩溃// unique_ptr<Date> up1(new Date[10]);// shared_ptr<Date> sp1(new Date[10]);// 解决方案1// 因为new[]经常使用,所以unique_ptr和shared_ptr// 实现了一个转化版本,这个转化版本将时用的delete[]unique_ptr<Date[]> up1(new Date[5]);shared_ptr<Date[]> sp1(new Date[5]);// 解决方案2// 仿函数对象做删除器unique_ptr<Date, DeleteArray<Date>> up2(new Date[5], DeleteArray<Date>());shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());// 函数指针做删除器unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);// lambda表达式做删除器auto delArr0BJ = [](Date* ptr) {delete[] ptr; };unique_ptr<Date, decltype(delArr0BJ)> up4(new Date[5], delArr0BJ);shared_ptr<Date> sp4(new Date[5], delArr0BJ);// 实现其他资源管理的删除器shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose());shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {cout << "fclose:" << ptr << endl;fclose(ptr);});shared_ptr<Date> sp1(new Date(2024, 9, 11));shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);auto sp3 = make_shared<Date>(2024, 9, 11);shared_ptr<Date> sp4;if (sp1)cout << "sp1 is not nullptr" << endl;if (!sp4)cout << "sp1 is nullptr" << endl;// 报错// shared_ptr<Date> sp5 = new Date(2024, 9, 11);// unique_ptr<Date> sp6 = new Date(2024, 9, 11);return 0;
}

4. 智能指针的原理

下⾯我们模拟实现了auto_ptr和unique_ptr的核⼼功能,这两个智能指针的实现⽐较简单,⼤家了 解⼀下原理即可。auto_ptr的思路是拷⻉时转移资源管理权给被拷⻉对象,这种思路是不被认可 的,也不建议使⽤。unique_ptr的思路是不⽀持拷⻉。

⼤家重点要看看shared_ptr是如何设计的,尤其是引⽤计数的设计,主要这⾥⼀份资源就需要⼀个 引⽤计数,所以引⽤计数使⽤静态成员的⽅式是⽆法实现的,要使⽤堆上动态开辟的⽅式,构造智 能指针对象时来⼀份资源,就要new⼀个引⽤计数出来。多个shared_ptr指向资源时就++引⽤计 数,shared_ptr对象析构时就--引⽤计数,引⽤计数减到0时代表当前析构的shared_ptr是最后⼀ 个管理资源的对象,则析构资源。

4.1 auto_ptr和unique_ptr的实现

auto_ptr的思路是拷贝时转移资源管理权给被拷贝对象,这种思路不被认可,也不建议使用。unique_ptr的思路是不支持拷贝。

namespace rose
{template<class T>class auto_ptr{public:auto_ptr(T* ptr):_ptr(ptr){}auto_ptr(auto_ptr<T>& sp):_ptr(sp._ptr){// 管理权转移sp._ptr = nullptr;}auto_ptr<T>& operator=(auto_ptr<T>& ap){// 检测是否为自己给自己赋值if (this != &ap){// 释放当前对象中资源if (_ptr)delete _ptr;// 转移ap中资源到当前对象中_ptr = ap._ptr;ap._ptr = NULL;}return *this;}~auto_ptr(){if (_ptr){cout << "delete:" << _ptr << endl;delete _ptr;}}// 像指针一样使用T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};template<class T>class unique_ptr{public:explicit unique_ptr(T* ptr):_ptr(ptr){}~unique_ptr(){if (_ptr){cout << "delete:" << _ptr << endl;delete _ptr;}}// 像指针一样使用T& operator*(){return *_ptr;}T* operator->(){return _ptr;}unique_ptr(const unique_ptr<T>& sp) = delete;unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;unique_ptr(unique_ptr<T>&& sp):_ptr(sp._ptr){sp._ptr = nullptr;}unique_ptr<T>& operator=(unique_ptr<T>&& sp){delete _ptr;_ptr = sp._ptr;sp._ptr = nullptr;}private:T* _ptr;};}int main()
{rose::auto_ptr<Date> api(new Date);// 拷贝时,管理权限转移,被拷贝对象api_悬空rose::auto_ptr<Date> ap2(api);// 空指针访问,api对象已经悬空//api->_year++;rose::unique_ptr<Date> up1(new Date);// 不支持拷贝//unique_ptr<Date> up2(up1);// 支持移动,但是移动后up1也悬空,所以使用移动要谨慎rose::unique_ptr<Date> up3(move(up1));}

4.2 shared_ptr(重点在于拷贝构造和赋值重载)和weak_ptr的实现

shared_ptr的实现较为复杂,主要涉及引用计数的设计。多个shared_ptr指向资源时就增加引用计数,shared_ptr对象析构时就减少引用计数,引用计数减到0时释放资源。

针对引用计数,我们这么写:

int _count;//这样肯定是不行的

是不行的! 拷贝构造的时候加成2!然后进行拷贝给sp2,这时候sp2也就有了一个引用计数count=2!但是在析构的时候,sp2先进性析构,其count--为1了,sp1也要析构了,其count--为1,两个的count目前都是1,没有到0,造成都没有析构!!!--- 就很扯淡了!!!

所以说不能说是他们有个引用计数,而是他们公共有一个引用计数!

那我们使用一个静态的来解决???

因为在 C++ 中,静态成员(静态成员变量和静态成员函数)属于类本身,而不属于类的某个具体实例(对象)。这是静态成员最核心的特性!

静态成员变量在内存中只有一份拷贝,被该类的所有对象共享,不随对象的创建 / 销毁而分配 / 释放内存(生命周期与程序一致)

static int _count;//不行的!!!

但是这是不行的,这也是一个大坑:上面就说到了:引⽤计数的设计,主要这⾥⼀份资源就需要⼀个 引⽤计数,也就是引用计数属于这个类的所有的对象,如图:

本来有一个引用计数为2,sp1和sp2造成的,现在来了一个sp3,sp3不是拷贝于前两者的,执行的资源不一样!如果使用静态成员,这是所有类共享的,只有一份的资源,再++的话,sp3的引用计数就会变成3了,这是不符合我们的需求的!!!

所以引⽤计数使⽤静态成员的⽅式是⽆法实现的!我们期望的是一份资源就配个计数!

所以这里我们采用动态开辟的方式,我们可以通过指针来找到资源对应的计数,还有构造的时候,资源就会有了,那就表示来了一份资源,交给了对应的智能指针对象! 

namespace rose
{template<class T>class shared_ptr{public:shared_ptr(T* ptr = nullptr): _ptr(ptr), _pcount(new int(1)){}//支持定制删除器template<class D>//是这个构造函数的模板参数,不是整个类的模板参数 --- 类里面用不了,就是后面定义的 D_del 用不了,这不扯淡了嘛?// 写成整个类的就会造成和unique_ptr一样了 --- 不好用// 我们可以写成 function<void(T*)> _del -- 直接使用一个包装器就可以了shared_ptr(T* ptr, D del):_ptr(ptr), _pcount(new int(1)), _del(del){}//拷贝构造的时候,需要进行++//拷贝构造走浅拷贝shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount){(*_pcount)++;}//拷贝构造~shared_ptr(){if (--(*_pcount) == 0){// delete _ptr;_del(_ptr);delete _pcount;}}//赋值重载:(释放前者 --- 一定会释放吗???)shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr != sp._ptr)//如果不写这个就很坑!!!自己给自己赋值,当引用计数为1,就先被释放了,后面还"="就会造成野指针了//使用this != &sp 是不那么好的{if (--(*_pcount) == 0){delete _ptr;delete _pcount;//_ptr = nullptr;//_pcount = nullptr; --- 不需要}_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount);}return *this;}int use_count() const{return *(_pcount);}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;// int _count;//这样肯定是不行的// static int _count;//不行的!!!int* _pcount;// atomic<int>* _pcount;function<void(T*)> _del = [](T* ptr) {delete ptr; };};
}

完善的代码:

namespace rose
{template<class T>class shared_ptr{public:explicit shared_ptr(T* ptr = nullptr): _ptr(ptr), _pcount(new int(1)){}template<class D>shared_ptr(T* ptr, D del): _ptr(ptr), _pcount(new int(1)), _del(del){}shared_ptr(const shared_ptr<T>& sp): _ptr(sp._ptr), _pcount(sp._pcount), _del(sp._del){++(*_pcount);}void release(){if (--(*_pcount) == 0){// 最后一个管理的对象,释放资源_del(_ptr);delete _pcount;_ptr = nullptr;_pcount = nullptr;}}shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr != sp._ptr){release();_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount);_del = sp._del;}return *this;}~shared_ptr(){release();}T* get() const{return _ptr;}int use_count() const{return *_pcount;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;int* _pcount;//atomic<int>* _pcount;function<void(T*)> _del = [](T* ptr) {delete ptr; };};template<class T>class weak_ptr{public:weak_ptr(){}weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){}weak_ptr<T>& operator=(const shared_ptr<T>& sp){_ptr = sp.get();return *this;}private:T* _ptr = nullptr;};
}int main()
{rose::shared_ptr<Date> sp1(new Date);// 支持拷贝rose::shared_ptr<Date> sp2(sp1);rose::shared_ptr<Date> sp3(sp2);cout << sp1.use_count() << endl;sp1->_year++;cout << sp1->_year << endl;cout << sp2->_year << endl;cout << sp3->_year << endl;return 0;
}
}

5. shared_ptr和weak_ptr

5.1 shared_ptr循环引用问题 --- 重点

shared_ptr大多数情况下管理资源非常合适,支持RAII,也支持拷贝。但是在循环引用的场景下会导致资源没得到释放 内存泄漏,所以我们要认识循环引用的场景和资源没释放的原因,并且学会使用weak_ptr解决这种问题。

n1的_next指向n2的资源,那么n2的引用计数就会变成2,next和n2共同管理这个资源,n2的——prev指针同理:

如下图所述场景,n1和n2析构后,管理两个节点的引用计数减到1,然后就没有然后了。

分析:

  1. 右边的节点什么时候释放呢,左边节点中的_next管着呢,_next析构后,右边的节点就释放了。
  2. _next什么时候析构呢,_next是左边节点的的成员,左边节点释放,_next就析构了。
  3. 左边节点什么时候释放呢,左边节点由右边节点中的_prev管着呢,_prev析构后,左边的节点就释放了。
  4. _prev什么时候析构呢,_prev是右边节点的成员,右边节点释放,_prev就析构了。

至此逻辑上成功形成回旋镖似的循环引用,谁都不会释放就形成了循环引用,导致内存泄漏。

把ListNode结构体中的_next和_prev改成weak_ptr,weak_ptr绑定到shared_ptr时不会增加它的引用计数,_next和_prev不参与资源释放管理逻辑,就成功打破了循环引用,解决了这里的问题。

struct ListNode
{int _data;std::shared_ptr<ListNode> _next;std::shared_ptr<ListNode> _prev;// 这里改成weak_ptr,当n1->_next = n2;绑定shared_ptr时// 不增加n2的引用计数,不参与资源释放的管理,就不会形成循环引用了/*std::weak_ptr<ListNode> __next;std::weak_ptr<ListNode> __prev;*/~ListNode(){cout << "~ListNode()" << endl;}
};int main()
{// 循环引用 -- 内存泄露std::shared_ptr<ListNode> n1(new ListNode);std::shared_ptr<ListNode> n2(new ListNode);cout << n1.use_count() << endl;cout << n2.use_count() << endl;//互相指向 --- 一个智能指针是没有办法直接给给原生指针的 --- 改成智能指针n1->_next = n2;n2->_prev = n1;cout << n1.use_count() << endl;cout << n2.use_count() << endl;// weak_ptr不支持管理资源,不支持RAII// weak_ptr是专门绑定shared_ptr,不增加他的引用计数,作为一些场景的辅助管理//std::weak_ptr<ListNode> wp(new ListNode);return 0;
}

5.2 weak_ptr

weak_ptr不支持RAII,也不支持访问资源,所以我们看文档发现weak_ptr构造时不支持绑定到资源,只支持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引用计数,那么就可以解决上述的循环引用问题。

weak_ptr也没有重载operator*和operator->等,因为他不参与资源管理,那么如果他绑定的shared_ptr已经释放了资源,那么他去访问资源就是很危险的。weak_ptr支持expired检查指向的资源是否过期,use_count也可获取shared_ptr的引用计数,weak_ptr想访问资源时,可以调用lock返回一个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是一个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的。

int main()
{std::shared_ptr<string> spl(new string("111111"));std::shared_ptr<string> sp2(spl);std::weak_ptr<string> wp = spl;cout << wp.expired() << endl;cout << wp.use_count() << endl;// spl和sp2都指向其他资源,则weak_ptr就过期了spl = make_shared<string>("222222");cout << wp.expired() << endl;cout << wp.use_count() << endl;sp2 = make_shared<string>("333333");cout << wp.expired() << endl;cout << wp.use_count() << endl;wp = spl;auto sp3 = wp.lock();// 自命为王cout << wp.expired() << endl;cout << wp.use_count() << endl;*sp3 += "###";cout << *spl << endl;return 0;
}

6. shared_ptr的线程安全问题

shared_ptr的引用计数对象在堆上,如果多个shared_ptr对象在多个线程中进行拷贝析构时会访问修改引用计数,存在线程安全问题。需要加锁或者原子操作保证线程安全。--- atomic<int*> _pcount; 

struct AA
{int _a1 = 0;int _a2 = 0;~AA(){cout << "~AA()" << endl;}
};int main()
{bit::shared_ptr<AA> p(new AA);const size_t n = 1000000;mutex mtx;auto func = [&](){for (size_t i = 0; i < n; ++i){// 这里智能指针拷贝会++计数bit::shared_ptr<AA> copy(p);{unique_lock<mutex> lk(mtx);copy->_a1++;copy->_a2++;}}};thread t1(func);thread t2(func);t1.join();t2.join();cout << p->_a1 << endl;cout << p->_a2 << endl;cout << p.use_count() << endl;return 0;
}

7. C++11和boost中智能指针的关系

Boost库是为C++语言标准库提供扩展的一些C++程序库的总称。Boost社区建立的初衷之一就是为C++的标准化工作提供可供参考的实现,Boost社区的发起人Dawes本人就是C++标准委员会的成员之一。在Boost库的开发中,Boost社区也在这个方向上取得了丰硕的成果,C++11及之后的新语法和库有很多都是从Boost中来的。

  • C++ 98:产生了第一个智能指针auto_ptr

  • C++ boost:给出了更实用的scoped_ptrscoped_arrayshared_ptrshared_array以及weak_ptr等。

  • C++ TR1:引入了shared_ptr等,不过需要注意的是TR1并不是标准版。

  • C++ 11:引入了unique_ptrshared_ptrweak_ptr。需要注意的是,unique_ptr对应Boost中的scoped_ptr,并且这些智能指针的实现原理是参考Boost中的实现的。

8. 内存泄漏

8.1 什么是内存泄漏,内存泄漏的危害

什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存,一般是忘记释放或者发生异常释放程序未能执行导致的。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

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

int main()
{char* ptr = new char[1024 * 1024 * 1024];cout << (void*)ptr << endl;return 0;
}

8.2 如何检测内存泄漏

  • Linux下内存泄漏检测:使用Valgrind等工具。

  • Windows下使用第三方工具:如VLD(Visual Leak Detector)。

8.3 如何避免内存泄漏

  • 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。

  • 尽量使用智能指针来管理资源,如果自己场景比较特殊,采用RAII思想自己造个轮子管理。

  • 定期使用内存泄漏工具检测,尤其是每次项目快上线前,不过有些工具不够靠谱,或者是收费。

总结一下:内存泄漏非常常见,解决方案分为两种:

  1. 事前预防型:如智能指针等。
  2. 事后查错型:如泄漏检测工具。
http://www.lryc.cn/news/612338.html

相关文章:

  • harmonyOS学习 - rcp请求
  • 文字转语音tts
  • 鹧鸪云:光伏电站的“智慧中枢”,精准调控逆变器
  • OpenCV校准双目相机并测量距离
  • 10.MTK充电之mt6358-gauge驱动
  • Linux发行版分类与Centos替代品
  • 媒体资产管理系统和OCR文字识别的结合
  • 笔试——Day30
  • 简单介绍cgroups以及在K8s中的应用
  • 小程序中,给一段富文本字符串文案特殊内容加样式监听点击事件
  • 无人机遥控器舵量技术解析
  • cad c#二次开发 图层封装 获取当前层
  • 无人机遥控器波特率技术解析
  • 基于AI的自动驾驶汽车(AI-AV)网络安全威胁缓解框架
  • 开疆智能ModbusTCP转Profinet网关连接EPSON机器人配置案例
  • Docker国内可用镜像(2025.08.06测试)
  • 深入理解数据库连接池(Connection Pool):原理、优势与常见实现
  • wordpress网站的“管理员邮箱地址”有什么用?
  • Linux86 sheel流程控制前瞻4 判断vsftpd服务启动,如果启动,打印端口号,进程id
  • 系统运维之LiveCD详解
  • 【图像处理基石】浅谈3D城市生成中的数据融合技术
  • 【图像处理基石】什么是数字高程模型?如何使用数字高程模型?
  • dify之推送飞书群消息工作流
  • 飞书对接E签宝完整方案
  • 《动手学深度学习》读书笔记—9.7序列到序列学习
  • CPP网络编程-异步sever
  • 内部类详解:Java中的嵌套艺术
  • MATLAB深度学习之数据集-数据库构建方法详解
  • 202506 电子学会青少年等级考试机器人三级实际操作真题
  • KVazaar:开源H.265/HEVC编码器技术深度解析