目录一.智能指针的使用及原理1.1智能指针的使用场景分析1.2RAII和智能指针的设计思路1.3C标准库智能指针的使用1.4删除器1.5完善shared_ptr模拟实现1.6shared_ptr与weak_ptr1.6.1shared_ptr的循环引用问题1.6.2weak_ptr总结四种智能指针的区别1.7C11和boost中智能指针的关系1.8内存泄漏1.8.1 什么是内存泄漏内存泄漏的危害1.8.2 如何检测内存泄漏了解1.8.3 如何避免内存泄漏一.智能指针的使用及原理1.1智能指针的使用场景分析对比项mallocnew所属C 语言库函数头文件cstdlibC 运算符语言内置不需要头文件内存来源堆只分配原始内存不会调用构造函数堆分配内存 调用构造函数完成对象初始化返回值void*需要强制类型转换返回对应类型指针不用强转失败行为分配失败返回NULL 空指针分配失败默认抛出std::bad_alloc异常释放方式free()delete/delete[]同时调用析构函数数组支持手动算总字节大小malloc(sizeof(T)*N)new T[N]配套delete[]重载不能重载可以全局 / 类内重载operator new自定义处理无支持nothrow版本new(nothrow) T失败返回 nullptr 不抛异常Qoperator 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是 C98 时设计出来的智能指针他的特点是拷贝时把被拷贝对象的资源的管理权转移给拷贝对象这是一个非常糟糕的设计因为他会到被拷贝对象悬空访问报错的问题C11 设计出新的智能指针后强烈建议不要使用auto_ptr。其他 C11 出来之前很多公司也是明令禁止使用这个智能指针的。unique_ptr是 C11 设计出来的智能指针他的名字翻译出来是唯一指针他的特点的不支持拷贝只支持移动。如果不需要拷贝的场景就非常建议使用他。shared_ptr是 C11 设计出来的智能指针他的名字翻译出来是共享指针他的特点是支持拷贝也支持移动。如果需要拷贝的场景就需要使用他了。底层是用引用计数的方式实现的。weak_ptr是 C11 设计出来的智能指针他的名字翻译出来是弱指针他完全不同于上面的智能指针他不支持 RAII也就意味着不能用它直接管理资源weak_ptr的产生本质是要解决shared_ptr的一个循环引用导致内存泄漏的问题。具体细节下面我们再细讲。auto_ptr指针我们发现程序错误退出为什么呢我们调试看一下拷贝赋值后原来的 ap1 资源被夺走内部指针置空继续使用 ap1 就崩溃这就是auto_ptr被废弃的根源。unique_ptr:移动后也会制空移动的本质就是调用移动构造实现资源交换所以如果不使用拷贝就用unique_ptr最方便shared_ptr:我们可以看到指向的是同一块地址然后共同管理这同一块资源模拟实现auto_ptr:namespace Jam { templateclass T class auto_ptr { public: auto_ptr(T* ptr) :_ptr(ptr) {} auto_ptr(auto_ptrT sp) :_ptr(sp._ptr) { sp._ptr nullptr; } // 赋值运算符重载 auto_ptrT operator(auto_ptrT 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_ptrDate u1(new Date(2026, 8, 20)); Jam::auto_ptrDate u2(u1); return 0; }模拟实现unique_ptr:namespace Jam { templateclass T class unique_ptr { public: unique_ptr(T* ptr) :_ptr(ptr) { } // unique_ptr不允许拷贝构造和运算符重载 unique_ptr(unique_ptrT up) delete; unique_ptrT operator(const unique_ptrT up) delete; // 可以移动拷贝构造 unique_ptr(unique_ptrT up) { _ptr up._ptr; up._ptr nullptr; } unique_ptrT operator(unique_ptrT 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_ptrDate up1(new Date); Jam::unique_ptrDate up2(new Date(2026,8,20)); Jam::unique_ptrDate up3(move(up1)); Jam::unique_ptrDate up4(move(up2)); up3 move(up4); return 0; }模拟实现shared_ptr:namespace Jam { templateclass T class shared_ptr { public: shared_ptr(T* ptr) :_ptr(ptr) ,_pcount(new int(1)) // 开辟一段空间来计算有多少个指针指向这个地址 { } shared_ptr(const shared_ptrT sp) { _ptr sp._ptr; _pcount sp._pcount; (*_pcount); } shared_ptrT operator(shared_ptrT 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_ptrDate sp1(new Date); Jam::shared_ptrDate sp2(new Date(2026, 8, 20)); Jam::shared_ptrDate sp3(sp1); Jam::shared_ptrDate 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 { templateclass T class shared_ptr { public: // 不传对象走这里 shared_ptr(T* ptr) :_ptr(ptr) ,_pcount(new int(1)) // 开辟一段空间来计算有多少个指针指向这个地址 { } // 传入lambda之类的对象走这个构造进行对[]删除 templateclass D shared_ptr(T* ptr, D del) :_ptr(ptr) ,_pcount(new int(1)) ,_del(del) { } shared_ptr(const shared_ptrT sp) { _ptr sp._ptr; _pcount sp._pcount; (*_pcount); } shared_ptrT operator(shared_ptrT 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函数指针仿函数 // 直接用包装器来包装不需要知道是什么类型 functionvoid(T*) _del [](T* ptr) {delete ptr; }; // 这里给缺省值是因为走第一个构造也要初始化_del,如果不给在析构的时候就出错 // 因为外部没传_ptr没有被释放 }; } int main() { Jam::shared_ptrDate sp1(new Date); Jam::shared_ptrDate sp2(new Date(2026, 8, 20)); Jam::shared_ptrDate sp3(sp1); Jam::shared_ptrDate sp4 sp2; auto delArrObj [](Date* ptr) {delete[] ptr; }; Jam::shared_ptrDate sp5(new Date[2], delArrObj); return 0; }1.6shared_ptr与weak_ptr1.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_ptrweak_ptr绑定到shared_ptr时不会增加它的引用计数_next和_prev不参与资源释放管理逻辑就成功打破了循环引用解决了这里的问题造成循环引用这时就需要用到weak_ptr了1.6.2weak_ptrweak_ptr 本身不拥有对象它是 shared_ptr 的 “观察者”不会增加引用计数。它不能直接解引用使用对象必须通过lock()转换成shared_ptr再用。 专门解决shared_ptr的循环引用内存泄漏问题。weak_ptr 能干什么、不能干什么✅可以绑定到一个shared_ptr管理的对象不增加 ref 计数判断对象是否还活着对象有没有已经被释放升级成shared_ptrlock()对象活着就得到 shared_ptr对象销毁返回空 shared_ptr❌不能不能直接*wp、wp-访问对象没有重载解引用不能管理资源不能做 RAII不会自动 delete 内存不能用unique_ptr初始化 weak_ptr只能配套shared_ptr总结四种智能指针的区别智能指针头文件所有权引用计数拷贝移动直接解引用* / -底层控制块主要用途核心注意点auto_ptrmemory独占所有权无❌拷贝会转移所有权拷贝后原对象置空支持✅可以无C98 老式独占智能指针C11 已经废弃不要用拷贝行为诡异不能放到容器彻底淘汰unique_ptrmemory独占所有权无❌禁止拷贝✅支持移动std::move✅可以无直接持有裸指针独占管理堆资源优先使用代替 auto_ptr数组支持unique_ptrT[]独占不能拷贝需要转移所有权必须 move不支持 weak_ptr 观察shared_ptrmemory共享所有权有引用计数✅支持拷贝 (计数 1)✅支持移动✅可以有控制块保存引用计数、删除器多个地方共享同一个对象生命周期多份持有者会产生循环引用内存泄漏不要用裸指针多次构造配合 make_shared 优先weak_ptrmemory无所有权仅观察者不改变计数✅拷贝✅支持移动❌不能直接* / -必须.lock()升级为 shared_ptr复用 shared_ptr 的控制块解决 shared_ptr 循环引用观察者模式、缓存只观察不持有对象只能绑定 shared_ptr不能配合 unique_ptrlock 后一定要判空多线程优先 lock 不要用 expired1.7C11和boost中智能指针的关系1.8内存泄漏1.8.1 什么是内存泄漏内存泄漏的危害1.8.2 如何检测内存泄漏了解Linux下内存泄漏检测Linux下几款C程序中的内存泄露检查工具_c内存泄露工具分析-CSDN博客window下检测windows下的内存泄露检测工具VLD使用_windows内存泄漏检测工具-CSDN博客1.8.3 如何避免内存泄漏