前置说明
智能指针基于RAII(资源获取即初始化)机制 ,自动管理堆内存,核心解决裸指针的内存泄漏问题:把堆内存的生命周期绑定到栈上的智能指针对象,当智能指针对象超出作用域时,析构函数会自动释放绑定的堆内存,无需手动
delete。C++ 中的裸指针(
int* p = new int;)需要手动管理内存:
- 忘记
delete会导致内存泄漏;- 多次
delete会导致未定义行为;- 异常场景下(比如
delete前抛出异常),内存也无法释放。
C++ 标准库在<memory>头文件中提供了三种核心智能指针:unique_ptr、shared_ptr、weak_ptr,其中auto_ptr已被废弃(设计缺陷)。
unique_ptr是独占式智能指针(轻量、高效),shared_ptr是共享式(引用计数),weak_ptr辅助shared_ptr解决循环引用;- 使用智能指针的核心原则:优先选
unique_ptr,避免裸指针与智能指针混用,警惕shared_ptr的循环引用。
智能指针的使用原则:
- 优先使用
unique_ptr(高效、无额外开销),仅在需要共享所有权时使用shared_ptr;- 避免用同一个裸指针创建多个智能指针(会导致重复释放);
- 不要手动
delete智能指针管理的裸指针(智能指针析构时会再次delete);shared_ptr的循环引用必须用weak_ptr解决;- 优先使用
make_unique/make_shared创建智能指针(异常安全、更高效)。
1. std::unique_ptr(独占式智能指针)
特性与使用场景
- 独占所有权 :同一时间只有一个
unique_ptr指向资源,拷贝构造 / 赋值被禁用,仅支持移动(std::move);- 轻量级:无额外内存开销(仅封装裸指针 + 删除器),效率接近裸指针;
- 适用场景 :函数返回值、容器元素(如
std::vector<unique_ptr<Foo>>)、独占资源的管理(如文件句柄、网络连接)。核心签名(类模板声明)
cpp// 基础版本(针对单个对象) template <class T, class Deleter = std::default_delete<T>> class unique_ptr; // 数组特化版本(针对动态数组 new T[]) template <class T, class Deleter> class unique_ptr<T[], Deleter>;
部分 含义 template <class T, class Deleter = std::default_delete<T>>模板参数:- T:智能指针指向的对象类型 (如int、std::string、自定义类);-Deleter:删除器类型 (负责释放资源的函数 / 仿函数),默认是std::default_delete<T>(调用delete释放单个对象);unique_ptr<T[], Deleter>数组特化版本:专门处理 new T[]分配的动态数组,默认删除器会调用delete[],而非普通版本的delete;关键成员函数签名(常用)
cpp// 1. 移动构造(独占所有权,仅支持移动,禁止拷贝) template <class U, class E> unique_ptr(unique_ptr<U, E>&& u) noexcept; // 2. 重置(释放当前资源,接管新资源) void reset(pointer p = pointer()) noexcept; // 3. 释放所有权(返回裸指针,智能指针不再管理) pointer release() noexcept; // 4. C++14 辅助创建函数(更安全,避免裸指针) template <class T, class... Args> unique_ptr<T> make_unique(Args&&... args); // 数组版本的make_unique template <class T> unique_ptr<T> make_unique(size_t n);示例代码
cpp#include <iostream> #include <memory> class MyClass { public: MyClass(int val) : value(val) { std::cout << "MyClass 构造:" << value << std::endl; } ~MyClass() { std::cout << "MyClass 析构:" << value << std::endl; } void show() { std::cout << "值:" << value << std::endl; } private: int value; }; int main() { // 1. 创建shared_ptr(推荐用make_shared,更高效) std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(10); std::cout << "引用计数:" << ptr1.use_count() << std::endl; // 输出:1 // 2. 拷贝,引用计数+1 std::shared_ptr<MyClass> ptr2 = ptr1; std::cout << "引用计数:" << ptr1.use_count() << std::endl; // 输出:2 std::cout << "引用计数:" << ptr2.use_count() << std::endl; // 输出:2 // 3. 多个指针共享资源 ptr1->show(); // 输出:值:10 ptr2->show(); // 输出:值:10 // 4. 重置指针,引用计数-1 ptr1.reset(); std::cout << "引用计数:" << ptr2.use_count() << std::endl; // 输出:1 // 5. 管理数组(C++17+支持make_shared数组,C++11/14需手动new) std::shared_ptr<int[]> arr_ptr(new int[5]{1,2,3,4,5}); arr_ptr[1] = 200; std::cout << arr_ptr[1] << std::endl; // 输出:200 return 0; // ptr2析构,引用计数变为0,资源释放 }
2. std::shared_ptr(共享式智能指针)
特性与使用场景
- 共享所有权 :多个
shared_ptr指向同一资源,每新增一个shared_ptr指向该资源,引用计数 + 1;每销毁一个shared_ptr,引用计数 - 1;引用计数为 0 时自动释放资源;- 线程安全:引用计数的增减是原子操作(但访问 / 修改指向的对象需手动加锁);
- 适用场景:需要多个对象共享同一资源的场景(如多线程访问同一数据、对象树的交叉引用)。
核心签名(类模板声明)
cpptemplate <class T> class shared_ptr;关键辅助函数 / 转换函数签名
cpp// 1. 高效创建(一次内存分配:对象+控制块,比直接new更优) template <class T, class... Args> shared_ptr<T> make_shared(Args&&... args); // 2. 类型转换(对应普通指针的cast,保证引用计数正确) // static_cast 等价版本 template <class T, class U> shared_ptr<T> static_pointer_cast(const shared_ptr<U>& r) noexcept; // dynamic_cast 等价版本(运行时类型检查) template <class T, class U> shared_ptr<T> dynamic_pointer_cast(const shared_ptr<U>& r) noexcept; // const_cast 等价版本 template <class T, class U> shared_ptr<T> const_pointer_cast(const shared_ptr<U>& r) noexcept;核心成员函数签名
cpp// 1. 拷贝构造(增加引用计数) template <class U> shared_ptr(const shared_ptr<U>& r) noexcept; // 2. 赋值重载(引用计数增减) template <class U> shared_ptr& operator=(const shared_ptr<U>& r) noexcept; // 3. 获取引用计数 long use_count() const noexcept; // 4. 检查是否是唯一持有者 bool unique() const noexcept; // 5. 重置(释放当前引用,引用计数-1) void reset() noexcept;
部分 含义 template <class T>仅需指定指向的对象类型 T,控制块(存储引用计数、删除器等)是内部隐式创建的;make_shared<Args&&... args>完美转发参数给 T的构造函数,在堆上创建T对象并绑定到shared_ptr;static/dynamic/const_pointer_cast避免直接对 shared_ptr的裸指针做 cast(会导致多个控制块),保证类型转换后引用计数统一;示例代码
cpp#include <iostream> #include <memory> class MyClass { public: MyClass(int val) : value(val) { std::cout << "MyClass 构造:" << value << std::endl; } ~MyClass() { std::cout << "MyClass 析构:" << value << std::endl; } void show() { std::cout << "值:" << value << std::endl; } private: int value; }; int main() { // 1. 创建shared_ptr(推荐用make_shared,更高效) std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(10); std::cout << "引用计数:" << ptr1.use_count() << std::endl; // 输出:1 // 2. 拷贝,引用计数+1 std::shared_ptr<MyClass> ptr2 = ptr1; std::cout << "引用计数:" << ptr1.use_count() << std::endl; // 输出:2 std::cout << "引用计数:" << ptr2.use_count() << std::endl; // 输出:2 // 3. 多个指针共享资源 ptr1->show(); // 输出:值:10 ptr2->show(); // 输出:值:10 // 4. 重置指针,引用计数-1 ptr1.reset(); std::cout << "引用计数:" << ptr2.use_count() << std::endl; // 输出:1 // 5. 管理数组(C++17+支持make_shared数组,C++11/14需手动new) std::shared_ptr<int[]> arr_ptr(new int[5]{1,2,3,4,5}); arr_ptr[1] = 200; std::cout << arr_ptr[1] << std::endl; // 输出:200 return 0; // ptr2析构,引用计数变为0,资源释放 }关键问题:循环引用
shared_ptr的最大陷阱是循环引用 :两个shared_ptr互相指向对方,导致引用计数永远无法变为 0,最终内存泄漏。循环引用示例(错误):
cpp#include <iostream> #include <memory> class B; // 前向声明 class A { public: std::shared_ptr<B> b_ptr; ~A() { std::cout << "A 析构" << std::endl; } }; class B { public: std::shared_ptr<A> a_ptr; ~B() { std::cout << "B 析构" << std::endl; } }; int main() { std::shared_ptr<A> a = std::make_shared<A>(); std::shared_ptr<B> b = std::make_shared<B>(); a->b_ptr = b; // A引用B b->a_ptr = a; // B引用A // 循环引用:a和b的引用计数都是2,析构时各减1,变为1,永远不会释放 return 0; // 不会输出"A 析构"和"B 析构",内存泄漏 }
3. std::weak_ptr(弱引用智能指针)
特性与使用场景
- 弱引用 :不拥有资源,不增加
shared_ptr的引用计数,不影响资源释放;- 解决循环引用 :
shared_ptr循环引用会导致引用计数无法归零,weak_ptr可打破循环;- 不能直接解引用 :必须通过
lock()转为shared_ptr后才能访问资源。签名(类模板声明)
cpptemplate <class T> class weak_ptr;成员函数签名
cpp// 1. 从shared_ptr构造(不增加引用计数) template <class U> weak_ptr(const shared_ptr<U>& r) noexcept; // 2. 锁定为shared_ptr(安全访问资源) shared_ptr<T> lock() const noexcept; // 3. 检查资源是否已释放(过期) bool expired() const noexcept; // 4. 获取对应的shared_ptr引用计数(仅参考,可能瞬时变化) long use_count() const noexcept; // 5. 重置(清空弱引用) void reset() noexcept;
部分 含义 template <class T>指向的对象类型与 shared_ptr一致;lock()核心函数:返回一个 shared_ptr(若资源未释放则引用计数 + 1,否则返回空shared_ptr),是访问弱引用资源的唯一安全方式;expired()等价于 use_count() == 0,但更高效(无需获取精确的引用计数值);解决循环引用的示例(正确):
cpp#include <iostream> #include <memory> class B; class A { public: std::weak_ptr<B> b_ptr; // 改为weak_ptr ~A() { std::cout << "A 析构" << std::endl; } }; class B { public: std::weak_ptr<A> a_ptr; // 改为weak_ptr ~B() { std::cout << "B 析构" << std::endl; } }; int main() { std::shared_ptr<A> a = std::make_shared<A>(); std::shared_ptr<B> b = std::make_shared<B>(); a->b_ptr = b; // weak_ptr不增加引用计数 b->a_ptr = a; // weak_ptr不增加引用计数 // 检查资源是否存在 if (auto temp = a->b_ptr.lock()) { // lock()返回shared_ptr,若资源存在则有效 std::cout << "B资源存在" << std::endl; } return 0; // a和b析构,引用计数变为0,资源释放(输出"A 析构"和"B 析构") }关键说明
weak_ptr不能直接访问资源(没有->和*运算符),必须通过lock()获取shared_ptr后才能访问;expired()方法可以判断weak_ptr指向的资源是否已释放(返回true表示已释放);weak_ptr的大小和shared_ptr相同(因为要存储引用计数的指针)。
内存泄漏案例以及改良方案
内存泄漏的核心本质是:堆内存被分配后,失去了对它的所有引用,导致程序无法再释放这块内存,直到程序退出(系统会回收,但长期运行的程序如服务器会持续占用内存)。
常见的内存泄漏场景
1. 最基础:忘记释放手动分配的内存
这是新手最易犯的错误 ------ 用
new/malloc分配堆内存后,未调用delete/free释放,尤其是在分支、循环等复杂逻辑中更容易遗漏。示例代码(错误):
cpp#include <iostream> using namespace std; void func() { // 分配堆内存 int* p = new int(10); string name = "test"; // 分支逻辑导致忘记释放 if (name == "test") { cout << "分支返回,遗漏delete" << endl; return; // 直接返回,p指向的内存永远无法释放 } // 只有走else才会释放(本例不会执行) delete p; } int main() { func(); // 执行后内存泄漏(4字节int) return 0; }避免方法:
- 优先使用智能指针(
unique_ptr/shared_ptr)替代裸指针;- 若必须用裸指针,遵循 "分配即规划释放" 原则,在分配内存时就确定释放的位置。
修复后的完整代码
cpp#include <iostream> #include <memory> // 必须包含智能指针的头文件 using namespace std; void func() { // 用unique_ptr替代裸指针,make_unique是创建unique_ptr的推荐方式 unique_ptr<int> p = make_unique<int>(10); string name = "test"; if (name == "test") { cout << "分支返回,智能指针自动释放内存" << endl; return; // 即使提前返回,p也会析构并释放内存 } // 无需手动delete!智能指针超出作用域时会自动释放 // 原来的delete p 可以完全删除 } int main() { func(); // 执行后无内存泄漏 return 0; }2. 异常导致的内存泄漏
new分配内存后,delete执行前抛出异常,导致delete语句无法执行,进而泄漏内存。这是比 "忘记释放" 更隐蔽的问题。示例代码(错误):
cpp#include <iostream> #include <stdexcept> using namespace std; void riskyFunc() { throw runtime_error("突发异常"); // 抛出异常 } void func() { int* p = new int(20); // 分配内存 riskyFunc(); // 抛出异常,后续代码全部跳过 delete p; // 永远执行不到,内存泄漏 } int main() { try { func(); } catch (const exception& e) { cout << "捕获异常:" << e.what() << endl; } return 0; }避免方法:
- 核心方案:使用智能指针(RAII 机制),即使抛出异常,智能指针对象析构时仍会自动释放内存;
- 兜底方案:用
try-catch包裹,但代码冗余且易遗漏,不如智能指针可靠。修复后的代码:
cpp#include <iostream> #include <stdexcept> #include <memory> // 智能指针头文件 using namespace std; void riskyFunc() { throw runtime_error("突发异常"); } void func() { unique_ptr<int> p = make_unique<int>(20); // 智能指针 riskyFunc(); // 抛异常也不影响,p析构时自动释放 } int main() { try { func(); } catch (const exception& e) { cout << "捕获异常:" << e.what() << endl; } return 0; // 无内存泄漏 }3. shared_ptr 的循环引用(进阶陷阱)
这是使用智能指针时的高频错误 ------ 两个或多个
shared_ptr互相持有对方的引用,导致引用计数永远无法归 0,内存无法释放。示例代码(错误):
cpp#include <iostream> #include <memory> using namespace std; class B; // 前向声明 class A { public: shared_ptr<B> b_ptr; // A持有B的shared_ptr ~A() { cout << "A 析构" << endl; } // 不会执行 }; class B { public: shared_ptr<A> a_ptr; // B持有A的shared_ptr ~B() { cout << "B 析构" << endl; } // 不会执行 }; int main() { shared_ptr<A> a = make_shared<A>(); shared_ptr<B> b = make_shared<B>(); a->b_ptr = b; // 循环引用开始 b->a_ptr = a; // a和b的引用计数都是2,析构时各减1变为1,永远不会释放 return 0; // 无析构输出,内存泄漏 }避免方法:
- 将循环引用中的一方或双方的
shared_ptr替换为weak_ptr(弱引用,不增加引用计数);- 修复后的代码可参考上一轮讲解智能指针时的
weak_ptr示例。4. 容器存储裸指针未清理
vector/list/map等容器存储裸指针时,清空容器(如clear())仅会删除指针本身(容器内的元素),但不会释放指针指向的堆内存。示例代码(错误):
cpp#include <iostream> #include <vector> using namespace std; int main() { vector<int*> vec; // 向容器添加堆内存指针 vec.push_back(new int(1)); vec.push_back(new int(2)); vec.push_back(new int(3)); vec.clear(); // 仅清空容器,3个int的堆内存未释放,泄漏 return 0; }避免方法:
- 容器中存储智能指针(如
vector<unique_ptr<int>>),清空时自动释放内存;- 若必须存裸指针,清空容器前遍历 delete 每个元素。
修复后的代码:
cpp#include <iostream> #include <vector> #include <memory> using namespace std; int main() { vector<unique_ptr<int>> vec; vec.push_back(make_unique<int>(1)); vec.push_back(make_unique<int>(2)); vec.push_back(make_unique<int>(3)); vec.clear(); // 自动释放所有堆内存,无泄漏 return 0; }5. 动态数组释放错误(delete vs delete [])
用
new[]分配的数组,若误用delete(而非delete[])释放:
- 对于类对象数组:仅调用第一个元素的析构函数,其余元素的析构函数不执行,导致内存泄漏;
- 对于内置类型数组(int/char 等):看似无泄漏,但属于 "未定义行为",可能引发其他问题。
示例代码(错误):
cpp#include <iostream> using namespace std; class MyClass { public: MyClass() { cout << "MyClass 构造" << endl; } ~MyClass() { cout << "MyClass 析构" << endl; } }; int main() { // 分配对象数组 MyClass* arr = new MyClass[3]; // 输出3次构造 delete arr; // 错误!仅调用第一个对象的析构,后2个泄漏 // 正确写法:delete[] arr; return 0; }避免方法:
- 严格遵循 "
new配delete,new[]配delete[]" 的规则;- 优先使用
vector或unique_ptr<T[]>管理动态数组(无需手动释放)。6. 全局 / 静态指针的内存泄漏
全局或静态指针指向堆内存时,若程序结束前未释放:
- 虽然程序退出后操作系统会回收内存,但长期运行的程序(如服务器、后台服务)会持续占用内存,最终导致内存耗尽;
- 不符合 "资源用完即释放" 的编程规范。
示例代码(错误):
cpp#include <iostream> using namespace std; // 全局指针 int* g_ptr = new int(100); int main() { // 程序运行期间未释放g_ptr,直到退出才被系统回收 cout << *g_ptr << endl; // 遗漏:delete g_ptr; return 0; }避免方法:
- 用全局智能指针(如
static unique_ptr<int> g_ptr = make_unique<int>(100));- 在程序退出前(如 main 结束前)显式释放全局 / 静态裸指针。
7. 第三方库资源未释放
使用第三方库的 API 分配资源(如自定义句柄、内存、句柄)时,未调用库提供的 "释放函数",导致泄漏(这类泄漏常被忽略)。
示例场景(伪代码):
cpp// 第三方库API示例 void* create_obj(); // 分配资源,返回指针 void destroy_obj(void* p); // 释放资源 int main() { void* obj = create_obj(); // 分配资源 // 业务逻辑... destroy_obj(obj); // 忘记调用,资源泄漏 return 0; }避免方法:
- 封装成 RAII 类,析构函数中调用释放函数;
- 记录所有 "分配 - 释放" API 对,确保成对调用。