news 2026/10/9 2:30:32

C++智能指针:原理和使用,多种指针的区别,以及内存泄漏

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++智能指针:原理和使用,多种指针的区别,以及内存泄漏

目录

一.智能指针的使用及原理

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智能指针的使用场景分析

对比项mallocnew
所属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[]等运算符,方便访问资源。

两者关系总结

  1. RAII 是通用原则,不限于内存RAII 可以管理:文件std::fstream、互斥锁std::lock_guard、socket;不仅仅管堆内存。
  2. 智能指针 = RAII 应用于堆内存管理智能指针是RAII 的一个子集、具体实现,专门用来管理new出来的堆内存资源。
  3. 分工
  • 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
  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不参与资源释放管理逻辑,就成功打破了循环引用,解决了这里的问题

造成循环引用,这时就需要用到weak_ptr了

1.6.2weak_ptr

weak_ptr 本身不拥有对象,它是 shared_ptr 的 “观察者”,不会增加引用计数。它不能直接解引用使用对象,必须通过lock()转换成shared_ptr再用。 专门解决shared_ptr的循环引用内存泄漏问题。

weak_ptr 能干什么、不能干什么

✅可以:

  1. 绑定到一个shared_ptr管理的对象,不增加 ref 计数
  2. 判断对象是否还活着(对象有没有已经被释放)
  3. 升级成shared_ptr:lock(),对象活着就得到 shared_ptr;对象销毁返回空 shared_ptr

❌不能:

  1. 不能直接*wp、wp->访问对象!没有重载解引用
  2. 不能管理资源,不能做 RAII,不会自动 delete 内存
  3. 不能用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博客

1.8.3 如何避免内存泄漏

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 2:29:42

SpringBoot+Vue大学生在线租房平台全栈实战解析

你有没有发现&#xff0c;最近两年“毕设级全栈项目”这个词出现频率特别高&#xff0c;其中基于SpringBootVue的大学生在线租房平台管理系统更是常客。名字虽然长&#xff0c;但它做的事情很清晰——用JavaMySQLMyBatis把后端接口撑起来&#xff0c;用Vue把前端页面渲染出来&a…

作者头像 李华
网站建设 2026/10/9 2:28:39

CMake FindLibLZMA 模块详解:在项目中集成 LZMA/XZ 压缩库

构建工具开发工具CLI 【免费下载链接】CMake Mirror of CMake upstream repository 项目地址&#xff1a; https://gitcode.com/gh_mirrors/cm/CMake 点击查看 免费下载 导读 本篇文章围绕 CMake 官方仓库中的 FindLibLZMA 查找模块展开&#xff0c;该模块用于在 CMake 构建系…

作者头像 李华
网站建设 2026/10/9 2:27:38

C# TCP粘包拆包 终极满分笔记

一、TCP核心本质&#xff08;必考概念&#xff09;TCP是面向字节流的协议&#xff0c;无消息边界。TCP只保证&#xff1a;数据可靠、有序、不重复。TCP不保证&#xff1a;应用层一次发送多少&#xff0c;接收层就一次读到多少。因此必然产生&#xff1a;粘包、拆包&#xff0c;…

作者头像 李华