目录
一.智能指针的使用及原理
1.1智能指针的使用场景分析
1.2RAII和智能指针的设计思路
1.3C++标准库智能指针的使用
1.4删除器
1.5完善shared_ptr模拟实现
1.6shared_ptr与weak_ptr
1.6.1shared_ptr的循环引用问题
1.6.2weak_ptr
总结四种智能指针的区别:
1.7C++11和boost中智能指针的关系
1.8内存泄漏
1.8.1 什么是内存泄漏,内存泄漏的危害
1.8.2 如何检测内存泄漏(了解)
1.8.3 如何避免内存泄漏
一.智能指针的使用及原理
1.1智能指针的使用场景分析
| 对比项 | malloc | new |
|---|---|---|
| 所属 | C 语言库函数,头文件<cstdlib> | C++ 运算符,语言内置,不需要头文件 |
| 内存来源 | 堆,只分配原始内存,不会调用构造函数 | 堆,分配内存 +调用构造函数完成对象初始化 |
| 返回值 | void*,需要强制类型转换 | 返回对应类型指针,不用强转 |
| 失败行为 | 分配失败返回NULL 空指针 | 分配失败默认抛出std::bad_alloc异常 |
| 释放方式 | free() | delete/delete[],同时调用析构函数 |
| 数组支持 | 手动算总字节大小malloc(sizeof(T)*N) | new T[N],配套delete[] |
| 重载 | 不能重载 | 可以全局 / 类内重载operator new |
| 自定义处理 | 无 | 支持nothrow版本:new(nothrow) T失败返回 nullptr 不抛异常 |
Q:
operator new和new的区别?
new运算符:完整动作:分配内存 + 调用构造函数operator new:只是函数,等价 malloc,只申请内存,不会调用构造函数。
1.2RAII和智能指针的设计思路
- RAII 是 Resource Acquisition Is Initialization(资源获取即初始化)的缩写,他是一种管理资源的类的设计思想,本质是一种利用对象生命周期来管理获取到的动态资源,避免资源泄漏,这里的资源可以是内存、文件指针、网络连接、互斥锁等等。RAII 在获取资源时把资源委托给一个对象,接着控制对资源的访问,资源在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题。
- 智能指针类除了满足 RAII 的设计思路,还要方便资源的访问,所以智能指针类还会想迭代器类一样,重载
operator*/operator->/operator[]等运算符,方便访问资源。
两者关系总结
- RAII 是通用原则,不限于内存RAII 可以管理:文件
std::fstream、互斥锁std::lock_guard、socket;不仅仅管堆内存。 - 智能指针 = RAII 应用于堆内存管理智能指针是RAII 的一个子集、具体实现,专门用来管理
new出来的堆内存资源。 - 分工
- RAII:思想,规定「构造拿资源,析构放资源」
- 智能指针:封装好的类,帮你做内存的获取 / 自动释放,不用手写 delete。
智能指针本质就是一个栈上对象,把堆资源托管给栈对象;栈对象生命周期结束,析构自动释放堆内存,这就是 RAII。
下面是大致的智能指针模拟,后面我们会具体讲解各种指针的优缺点:
1.3C++标准库智能指针的使用
- C++ 标准库中的智能指针都在
<memory>这个头文件下面,我们包含<memory>就可以是使用了,智能指针有好几种,除了weak_ptr他们都符合 RAII 和像指针一样访问的行为,原理上而言主要是解决智能指针拷贝时的思路不同。 auto_ptr是 C++98 时设计出来的智能指针,他的特点是拷贝时把被拷贝对象的资源的管理权转移给拷贝对象,这是一个非常糟糕的设计,因为他会到被拷贝对象悬空,访问报错的问题,C++11 设计出新的智能指针后,强烈建议不要使用auto_ptr。其他 C++11 出来之前很多公司也是明令禁止使用这个智能指针的。unique_ptr是 C++11 设计出来的智能指针,他的名字翻译出来是唯一指针,他的特点的不支持拷贝,只支持移动。如果不需要拷贝的场景就非常建议使用他。shared_ptr是 C++11 设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是支持拷贝,也支持移动。如果需要拷贝的场景就需要使用他了。底层是用引用计数的方式实现的。weak_ptr是 C++11 设计出来的智能指针,他的名字翻译出来是弱指针,他完全不同于上面的智能指针,他不支持 RAII,也就意味着不能用它直接管理资源,weak_ptr的产生本质是要解决shared_ptr的一个循环引用导致内存泄漏的问题。具体细节下面我们再细讲。
auto_ptr指针:
我们发现程序错误退出,为什么呢?我们调试看一下
拷贝赋值后,原来的 ap1 资源被夺走,内部指针置空,继续使用 ap1 就崩溃,这就是auto_ptr被废弃的根源。
unique_ptr:
移动后也会制空,移动的本质就是调用移动构造,实现资源交换
所以如果不使用拷贝就用unique_ptr最方便
shared_ptr:
我们可以看到指向的是同一块地址,然后共同管理这同一块资源
模拟实现auto_ptr:
namespace Jam { 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>& up) { if (this != &up) { // 释放当前的对象接受被赋值的对象 if (_ptr) delete _ptr; _ptr = up._ptr; up._ptr = nullptr; } return *this; } ~auto_ptr() { if (_ptr) { delete _ptr; cout << "delete auto_ptr" << endl; } } T* operator->() { return _ptr; } T& operator*() { return *_ptr; } private: T* _ptr; }; } int main() { Jam::auto_ptr<Date> u1(new Date(2026, 8, 20)); Jam::auto_ptr<Date> u2(u1); return 0; }模拟实现unique_ptr:
namespace Jam { template<class T> class unique_ptr { public: unique_ptr(T* ptr) :_ptr(ptr) { } // unique_ptr不允许拷贝构造和运算符重载 unique_ptr(unique_ptr<T>& up) = delete; unique_ptr<T> operator=(const unique_ptr<T>& up) = delete; // 可以移动拷贝构造 unique_ptr(unique_ptr<T>&& up) { _ptr = up._ptr; up._ptr = nullptr; } unique_ptr<T>& operator=(unique_ptr<T>&& up) { if (this != &up) { if (_ptr) delete _ptr; _ptr = up._ptr; up._ptr = nullptr; } return *this; } ~unique_ptr() { if (_ptr) delete _ptr; } T* operator->() { return _ptr; } T& operator*() { return *_ptr; } private: T* _ptr; }; } int main() { Jam::unique_ptr<Date> up1(new Date); Jam::unique_ptr<Date> up2(new Date(2026,8,20)); Jam::unique_ptr<Date> up3(move(up1)); Jam::unique_ptr<Date> up4(move(up2)); up3 = move(up4); return 0; }模拟实现shared_ptr:
namespace Jam { template<class T> class shared_ptr { public: shared_ptr(T* ptr) :_ptr(ptr) ,_pcount(new int(1)) // 开辟一段空间来计算有多少个指针指向这个地址 { } shared_ptr(const shared_ptr<T>& sp) { _ptr = sp._ptr; _pcount = sp._pcount; ++(*_pcount); } shared_ptr<T>& operator=(shared_ptr<T>& sp) { if (this != &sp) { if (--(*_pcount) == 0) { delete _ptr; _ptr = sp._ptr; _pcount = sp._pcount; } ++(*_pcount); } return *this; } ~shared_ptr() { if (--(*_pcount) == 0) { delete _ptr; delete _pcount; _ptr = nullptr; _pcount = nullptr; } } T* operator->() { return _ptr; } T& operator*() { return &_ptr; } private: T* _ptr; int* _pcount; //用来计数,防止多次析构 }; } int main() { Jam::shared_ptr<Date> sp1(new Date); Jam::shared_ptr<Date> sp2(new Date(2026, 8, 20)); Jam::shared_ptr<Date> sp3(sp1); Jam::shared_ptr<Date> sp4 = sp2; return 0; }注:我们可以发现这里new的都是一个对象,然后用的是delete,但是如果是一次开辟多个对象呢,这时再用delete就会出错,同时如果资源不是new出来的,那么就无法释放,所以我们还需要对unique_ptr和shared_ptr进行改进。
1.4删除器
删除器:就是智能指针析构时,用来释放资源的「可调用对象」(仿函数 /lambda/ 函数)。
为什么要有删除器?
默认情况只能处理new / new[]出来的内存。 但是很多资源不是 new 出来:
malloc()分配内存,要用free()释放fopen()打开文件,要用fclose()关闭- Windows 句柄
CreateFile,要用CloseHandle()关闭
这种资源不能直接
delete,所以允许你传入自定义删除器,告诉智能指针 “该怎么释放这个资源”
- 写在模板尖括号
<>内:填类型→ 不加() - 写在函数 / 构造函数括号
()内:填对象→ 加(),构造临时对象
注:通常是对new[]进行删除,直接用库中的特化版本即可
1.5完善shared_ptr模拟实现
namespace Jam { template<class T> class shared_ptr { public: // 不传对象走这里 shared_ptr(T* ptr) :_ptr(ptr) ,_pcount(new int(1)) // 开辟一段空间来计算有多少个指针指向这个地址 { } // 传入lambda之类的对象走这个构造,进行对[]删除 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; ++(*_pcount); } shared_ptr<T>& operator=(shared_ptr<T>& sp) { if (this != &sp) { if (--(*_pcount) == 0) { delete _ptr; _ptr = sp._ptr; _pcount = sp._pcount; } ++(*_pcount); } return *this; } ~shared_ptr() { if (--(*_pcount) == 0) { _del(_ptr); delete _pcount; // 下面写不写都可以 _ptr = nullptr; _pcount = nullptr; } } T* operator->() { return _ptr; } T& operator*() { return &_ptr; } private: T* _ptr; int* _pcount; //用来计数,防止多次析构 // 因为不知道外部传过来的对象是什么类型,可以是lambda,函数指针,仿函数 // 直接用包装器来包装,不需要知道是什么类型 function<void(T*)> _del = [](T* ptr) {delete ptr; }; // 这里给缺省值是因为,走第一个构造也要初始化_del,如果不给在析构的时候就出错 // 因为外部没传,_ptr没有被释放 }; } int main() { Jam::shared_ptr<Date> sp1(new Date); Jam::shared_ptr<Date> sp2(new Date(2026, 8, 20)); Jam::shared_ptr<Date> sp3(sp1); Jam::shared_ptr<Date> sp4 = sp2; auto delArrObj = [](Date* ptr) {delete[] ptr; }; Jam::shared_ptr<Date> sp5(new Date[2], delArrObj); return 0; }1.6shared_ptr与weak_ptr
1.6.1shared_ptr的循环引用问题
- shared_ptr大多数情况下管理资源非常合适,支持RAII,也支持拷贝。但是在循环引用的场景下会导致资源没得到释放内存泄漏,所以我们要认识循环引用的场景和资源没释放的原因,并且学会使用weak_ptr解决这种问题。
- 如下图所述场景,n1和n2析构后,管理两个节点的引用计数减到1
- 右边的节点什么时候释放呢,左边节点中的_next管着呢,_next析构后,右边的节点就释放了。
- _next什么时候析构呢,_next是左边节点的成员,左边节点释放,_next就析构了。
- 左边节点什么时候释放呢,左边节点由右边节点中的_prev管着呢,_prev析构后,左边的节点就释放了。
- _prev什么时候析构呢,_prev是右边节点的成员,右边节点释放,_prev就析构了。
- 至此逻辑上成功形成回旋镖似的循环引用,谁都不会释放就形成了循环引用,导致内存泄漏
- 把ListNode结构体中的_next和_prev改成weak_ptr,weak_ptr绑定到shared_ptr时不会增加它的引用计数,_next和_prev不参与资源释放管理逻辑,就成功打破了循环引用,解决了这里的问题
造成循环引用,这时就需要用到weak_ptr了
1.6.2weak_ptr
weak_ptr 本身不拥有对象,它是 shared_ptr 的 “观察者”,不会增加引用计数。它不能直接解引用使用对象,必须通过
lock()转换成shared_ptr再用。 专门解决shared_ptr的循环引用内存泄漏问题。
weak_ptr 能干什么、不能干什么
✅可以:
- 绑定到一个
shared_ptr管理的对象,不增加 ref 计数 - 判断对象是否还活着(对象有没有已经被释放)
- 升级成
shared_ptr:lock(),对象活着就得到 shared_ptr;对象销毁返回空 shared_ptr
❌不能:
- 不能直接
*wp、wp->访问对象!没有重载解引用 - 不能管理资源,不能做 RAII,不会自动 delete 内存
- 不能用
unique_ptr初始化 weak_ptr,只能配套shared_ptr
总结四种智能指针的区别:
| 智能指针 | 头文件 | 所有权 | 引用计数 | 拷贝 | 移动 | 直接解引用* / -> | 底层控制块 | 主要用途 | 核心注意点 |
|---|---|---|---|---|---|---|---|---|---|
| auto_ptr | <memory> | 独占所有权 | 无 | ❌拷贝会转移所有权(拷贝后原对象置空) | 支持 | ✅可以 | 无 | C++98 老式独占智能指针,C++11 已经废弃不要用 | 拷贝行为诡异,不能放到容器,彻底淘汰 |
| unique_ptr | <memory> | 独占所有权 | 无 | ❌禁止拷贝 | ✅支持移动std::move | ✅可以 | 无,直接持有裸指针 | 独占管理堆资源,优先使用,代替 auto_ptr,数组支持unique_ptr<T[]> | 独占,不能拷贝;需要转移所有权必须 move;不支持 weak_ptr 观察 |
| shared_ptr | <memory> | 共享所有权 | 有(引用计数) | ✅支持拷贝 (计数 + 1) | ✅支持移动 | ✅可以 | 有控制块(保存引用计数、删除器) | 多个地方共享同一个对象生命周期;多份持有者 | 会产生循环引用内存泄漏;不要用裸指针多次构造;配合 make_shared 优先 |
| weak_ptr | <memory> | 无所有权,仅观察者 | 不改变计数 | ✅拷贝 | ✅支持移动 | ❌不能直接* / ->,必须.lock()升级为 shared_ptr | 复用 shared_ptr 的控制块 | 解决 shared_ptr 循环引用;观察者模式、缓存,只观察不持有对象 | 只能绑定 shared_ptr;不能配合 unique_ptr;lock 后一定要判空;多线程优先 lock 不要用 expired |
1.7C++11和boost中智能指针的关系
1.8内存泄漏
1.8.1 什么是内存泄漏,内存泄漏的危害
1.8.2 如何检测内存泄漏(了解)
Linux下内存泄漏检测:Linux下几款C++程序中的内存泄露检查工具_c++内存泄露工具分析-CSDN博客
window下检测:windows下的内存泄露检测工具VLD使用_windows内存泄漏检测工具-CSDN博客