【C++进阶篇】智能指针
C++内存管理终极指南:智能指针从入门到源码剖析
- 一. 智能指针
- 1.1 auto_ptr
- 1.2 unique_ptr
- 1.3 shared_ptr
- 1.4 make_shared
- 二. 原理
- 三. shared_ptr循环引用问题
- 三. 线程安全问题
- 四. 内存泄漏
- 4.1 什么是内存泄漏
- 4.2 危害
- 4.3 避免内存泄漏
- 五. 最后
一. 智能指针
智能指针通过RAII(Resource Acquisition Is Initialization)机制,将内存管理封装为类生命周期行为,其核心价值体现在:
- 自动内存回收:通过析构函数自动释放资源,避免忘记delete导致的内存泄漏
- 异常安全性:在异常抛出时仍能保证资源释放
- 所有权语义:明确资源归属关系,减少悬垂指针风险
1.1 auto_ptr
特点:拷贝时将被拷贝对象的资源转移给拷贝对象,会导致被拷贝对象悬空问题,访问会崩溃。**建议:**坚决不要使用该智能指针。
1.2 unique_ptr
特点:见名知意,不支持拷贝,只支持移动。
使用场景:
- 当某种特定场景不需要拷贝,强烈建议使用它。
int main()
{unique_ptr<int> sp(new int[10]);//unique_ptr<int> sp1 = sp;int* fp = sp.get();//对该指针进行操作for (size_t i = 0; i < 10; i++){fp[i] = i + 1;}for (size_t i = 0; i < 10; i++){cout << fp[i] << " ";}cout << endl;cout << "sp交换前: ";cout << "sp _ptr:" << sp.get() << endl;unique_ptr<int> sp1;sp1.swap(sp);cout << "sp交换后: ";cout << "sp _ptr:" << sp.get() << endl;cout << "sp1 _ptr:" << sp1.get() << endl;sp1.release();//将指针置空cout << "调用release()后: ";cout << "sp1 _ptr:" << sp1.get() << endl;return 0;
}
- 输出结果:
从结果可以看出调用swap后资源的管理权转移给调用者对象,自己置空。
1.3 shared_ptr
特点:支持拷贝也支持移动。底层是使用计数方式来看看是哪个对象来释放和清理资源。
- 使用场景:
当某种场景需要拷贝时,推荐使用它。
struct Date
{int _year;int _month;int _day;Date(int year = 1, int month = 1, int day = 1):_year(year), _month(month), _day(day){cout << "_year = " << _year << " _month = " << _month << " _day = " << _day << endl;}~Date(){cout << "~Date()" << endl;}
};int main()
{shared_ptr<Date> sp(new Date(2025, 6, 9));cout << sp.use_count()<<endl;shared_ptr<Date> sp1 = sp;//赋值cout << sp.use_count() << endl;//引用计数cout << sp.get() << endl;//获取原生指针cout << sp1.get() << endl;cout << sp.operator->() << endl;return 0;
}
- 输出结果:
从结果可以看出创建出的新对象与拷贝对象指向的资源一致。
sp->_year;
上面语句访问是允许的。因为智能指针里面重载operator ->() 同时返回的是管理对象的类的对象。
上述语句等价于:shared_ptr通过重载operator->,使得sp->_day的语法等价于**(sp)._day*。
下面再看其它问题:
shared_ptr<Date> sp1(new Date[10]);
程序会崩溃!!!
输出结果:
- 原因分析:
- shared_ptr默认使用delete释放内存(针对单个对象)但new Date[ ],必须使用delete [ ] 释放资源。错误类型匹配会导致未定义行为。
这里的T是Date实例化时,而构造时Date[ ],导致析构时资源不匹配。
解决办法:
使用自定义(推荐):说白了就是让智能指针管理资源的对象类型与构造时的对象数据类型一致。就可以解决了。
shared_ptr<Date[]> sp1(new Date[10]);
输出结果:
从结果可以看出构造时的10个Date对象都被正确清理了。
其它的方法可以使用仿函数对象,lambda表达式,函数指针对象等构造时将删除器传递给智能指针整个类,删除器在类内部初始化了,因为这个类内部包含指向对象指针,然后释放和清理资源。
// lambda表达式做删除器
auto delArrOBJ = [](Date* ptr) {delete[] ptr; };
unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);
shared_ptr<Date> sp4(new Date[5], delArrOBJ);
// 函数指针做删除器
template<class T>
void DeleteArrayFunc(T* ptr)
{delete[] ptr;
}
unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);
1.4 make_shared
std::make_shared 是 C++11 引入的工厂函数,用于高效、安全地创建 shared_ptr 智能指针。
基本用法:
#include <memory>// 创建 shared_ptr<int>
auto sp1 = std::make_shared<int>(42);// 创建 shared_ptr<Date>
class Date { /* ... */ };
auto sp2 = std::make_shared<Date>(2025, 6, 9);
性能优化:一次内存分配
传统方式:
shared_ptr sp(new Date(2025,6,9)); // 两次内存分配
- 第一次分配:为 Date 对象分配内存
- 第二次分配:为引用计数控制块分配内存
make_shared 方式:
auto sp = std::make_shared(2025,6,9); // 一次内存分配
- 底层原理:内存分配策略
- 调用 ::operator new(sizeof(T) + sizeof(ControlBlock))
- 将对象和控制块放置在同一块连续内存中
二. 原理
auto_ptr 转移资源,思路不被认可,而unique_ptr 不支持拷贝,思路较简单,下面重点看看shared_ptr 设计思路 及 原理。
- 思路:
主要这⾥⼀份资源就需要⼀个引⽤计数,所以引⽤计数才⽤静态成员的⽅式是⽆法实现的,要使⽤堆上动态开辟的⽅式,构造智能指针对象时来⼀份资源,就要new⼀个引⽤计数出来。多个shared_ptr指向资源时就++引⽤计数,shared_ptr对象析构时就–引⽤计数,引⽤计数减到0时代表当前析构的shared_ptr是最后⼀个管理资源的对象,则析构资源。
- 问题:为啥shared_ptr计数需要再堆上开空间,静态方式行不行???
不行,因为需要特定的实例对象指向资源时,计数器才+1,因为需要一个资源共享一个计数器。
假如使用静态方式:
static int static_counter = 0; // 错误!所有对象共享同一个计数器
导致虽然是类对象实例,但未指向该资源,导致计数器逻辑错误。
模拟实现shrared_ptr智能指针:
namespace W
{template<class T>class shared_ptr{public:explicit shared_ptr(T* ptr = nullptr):_ptr(ptr), _pcount(new int(1)){}template<class D>explicit shared_ptr(T* ptr = nullptr,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){//delete _ptr;_del(_ptr);delete _pcount;_ptr = nullptr;_pcount = nullptr;}}shared_ptr<T> operator=(const shared_ptr<T>& sp){if (this != &sp){release();_ptr = sp._ptr;_pcout = sp._pcount;++(*_pcount);}return *this;}~shared_ptr(){release();}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* get()const{return _ptr;}int use_count()const{return *_pcount;}operator bool(){return _ptr != nullptr;}private:T* _ptr;int* _pcount;std::function<void(T*)> _del = [](T* ptr) {delete ptr; };//默认删除器};
}
三. shared_ptr循环引用问题
循环引用导致资源未被释放,从而导致内存泄漏,使用weak_ptr可以解决该问题,减少引用个数。
- 下面以一个场景来看看循环引用导致内存泄漏场景:
当n1和n2析构时,计数器分别减1,节点中的指针分别指向对方,导致每个资源引用计数器增加1。
n1节点中的next指针指向n2,n2节点中的prev指针指向n1,next什么时候析构呢,等着n2的prev指针不再指向是就析构了,n2的prev指针什么时候析构呢,等着n1的next不在指向时,就析构了。又回到原始问题,这就导致内存泄漏。
使用 weak_ptr 可以解决问题。
std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;
下面详细介绍一下weak_ptr原理
- 功能:
weak_ptr是C++11引入的智能指针,主要用于解决shared_ptr的循环引用问题,并提供对共享资源的非拥有式观察。
- 非拥有式观察
weak_ptr不增加对象的引用计数,仅观察由shared_ptr管理的资源。它通过共享控制块(Control Block)跟踪对象状态,但不会影响对象生命周期。
- 解决循环引用
当两个对象通过shared_ptr互相引用时,引用计数无法归零,导致内存泄漏。将其中一个引用改为weak_ptr可打破循环。例如:
class B;
class A {
public:std::shared_ptr<B> b_ptr;
};
class B {
public:std::weak_ptr<A> a_ptr; // 使用weak_ptr打破循环
};
- expired():快速检查对象是否存活(无需创建shared_ptr)。
总结:
weak_ptr通过非拥有式观察机制,有效解决了shared_ptr的循环引用问题,并支持缓存、观察者模式等场景。理解其控制块共享、引用计数管理及安全访问方法,能帮助开发者编写更健壮的C++代码。
三. 线程安全问题
如果多个线程在堆上同时进行对该计数器进行操作,就会导致线程安全问题。
解决办法:
- 加互斥锁。
- 将计数器不设置为int* 类型,而设置为atomic*。
atmoic<int>* _pcount;
四. 内存泄漏
4.1 什么是内存泄漏
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使⽤的内存,⼀般是忘记释
放或者发⽣异常释放程序未能执⾏导致的。内存泄漏并不是指内存在物理上的消失,⽽是应⽤程序分
配某段内存后,因为设计错误,失去了对该段内存的控制,因⽽造成了内存的浪费。
4.2 危害
普通程序运⾏⼀会就结束了出现内存泄漏问题也不⼤,进程正常结束,⻚表的映射
关系解除,物理内存也可以释放。⻓期运⾏的程序出现内存泄漏,影响很⼤,如操作系统、后台服
务、⻓时间运⾏的客⼾端等等,不断出现内存泄漏会导致可⽤内存不断变少,各种功能响应越来越
慢,最终卡死。
4.3 避免内存泄漏
⼯程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理
想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下⼀条智能指针来管理
才有保证。
- 尽量使⽤智能指针来管理资源,如果⾃⼰场景⽐较特殊,采⽤RAII思想⾃⼰造个轮⼦管理。
- 定期使⽤内存泄漏⼯具检测,尤其是每次项⽬快上线前,不过有些⼯具不够靠谱,或者是收费。
- 总结⼀下:内存泄漏⾮常常⻅,解决⽅案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测⼯具。
五. 最后
本文深入探讨了C++智能指针(auto_ptr、unique_ptr、shared_ptr、weak_ptr)的原理与应用。智能指针通过RAII机制实现自动内存管理,提升代码健壮性。unique_ptr独占资源,shared_ptr共享资源并通过引用计数管理生命周期,weak_ptr则提供非拥有式观察以解决循环引用问题。文章还介绍了make_shared的高效内存分配策略,并强调了线程安全与内存泄漏防范的重要性,是C++开发者掌握现代内存管理的实用指南。关于C++全内容到此结束了,下面将进入Linux网络篇的学习征程。