目录:
- 1. 智能指针的使用
- 2. RAII和智能指针
- 3. C++标准库智能指针的使用
- 3.1 auto_ptr
- 3.2 unique_ptr
- 3.3 shared_ptr
- 3.4 weak_ptr
- 3.5 make_shared
- 3.6 explicit 修饰构造函数
- 4. 删除器
- 4.1 数组的特化版本
- 4.2 自定义删除器
1. 智能指针的使用
只要是堆上 new 出来的资源,不想手动管理 delete,优先考虑智能指针;能用unique_ptr就不用shared_ptr。
int main() { try { Func(); } catch (const char* errmsg) { cout << errmsg << endl; } catch (const std::exception& e) { cout << e.what() << endl; } catch (...) { cout << "未知异常" << endl; } return 0; }void Func() { int* array1 = new int[10]; int* array2 = new int[10]; // ⚠️重点:这一行有可能抛出异常! try { int len, time; cin >> len >> time; cout << Divide(len, time) << endl; //除0抛异常 } catch (...) { //释放两块内存,重新抛出异常 delete[] array1; delete[] array2; throw; } //正常执行路径释放 delete[] array1; delete[] array2; }两个场景分析:
场景 1:Divide()函数抛出异常(除 0)
进入catch(...),执行delete[] array1、array2,内存正常释放。 👉 这个场景看起来没问题。
场景 2:int* array2 = new int[10];这里直接抛出异常(内存分配失败)
new分配内存失败,会抛出bad_alloc异常!
array1 = new int[10]成功,内存分配出来;执行 array2 的 new 时抛异常;
函数立刻终止,直接跳出 Func;
还没有进入 try 块!catch 捕获不到!
array1指向的内存没有释放 → 内存泄漏!
使用 unique_ptr 改造:
#include <memory> void Func() { // 全部用智能指针管理 std::unique_ptr<int[]> array1 = std::make_unique<int[]>(10); std::unique_ptr<int[]> array2 = std::make_unique<int[]>(10); int len, time; cin >> len >> time; cout << Divide(len, time) << endl; // 不需要任何try-catch手动释放! }分析两种异常情况
- array2 构造时 new 抛异常
array1是局部栈对象,已经构造完成。发生异常,函数退出,array1 自动调用析构释放内存。 - Divide 函数抛异常 函数提前退出,
array1、array2都是局部对象,离开作用域自动析构,两块内存全部释放。
2. RAII和智能指针
RAII 全称:Resource Acquisition Is Initialization,资源获取即初始化。 它是 C++ 管理资源最核心的设计思想。
核心思想一句话概括:
把资源交给局部对象托管:对象构造时获取资源,对象析构时释放资源;依靠对象生命周期,保证资源一定会释放。
智能指针就是应用 RAII 思想封装裸指针的类模板。 只靠 RAII 还不够,我们希望它用起来像普通指针,所以还要重载运算符模拟指针行为。
两大关键点:
- 遵循 RAII:构造接收裸指针,析构执行
delete自动释放内存; - 重载运算符:
operator*、operator->、operator[],让智能指针拥有和裸指针一样的访问语法。
简易智能指针示例:
template<class T> class SmartPtr { public: // 构造函数:获取资源,接收new出来的裸指针(RAII第一步) SmartPtr(T* ptr) :_ptr(ptr) {} // 析构函数:释放资源(RAII核心) ~SmartPtr() { cout << "delete[] " << _ptr << endl; delete[] _ptr; } // 重载解引用 *sp T& operator*() { return *_ptr; } // 重载箭头 ->sp T* operator->() { return _ptr; } // 重载下标 [] ,支持数组访问 sp[i] T& operator[](size_t i) { return _ptr[i]; } private: T* _ptr; // 私有成员:封装裸指针 };template<class T>类模板:支持任意类型指针int*、自定义类型*;- 私有成员
T* _ptr:封装裸指针,资源保存在对象内部; - 构造函数:接收外部
new出来的地址,完成资源获取; - 析构函数:对象销毁自动执行
delete[],自动释放堆内存; - 三个运算符重载:对外提供和原生指针一模一样的使用方式。
3. C++标准库智能指针的使用
智能指针概览:
C++ 标准库提供了多种智能指针,除了weak_ptr外,它们都符合 RAII 行为。它们的主要区别在于拷贝时的思路不同:
auto_ptr(C++98):拷贝时转移管理权(已废弃)。unique_ptr(C++11):独占所有权,不支持拷贝,只支持移动。shared_ptr(C++11):共享所有权,基于引用计数。weak_ptr(C++11):弱引用,不控制生命周期,用于解决循环引用问题。
3.1 auto_ptr
auto_ptr是 C++98 设计出来的智能指针。它的特点是:拷贝时会把被拷贝对象的资源管理权转移给拷贝对象。
致命缺陷:
这是一个非常糟糕的设计。当发生拷贝时,原对象(被拷贝者)会被置空(悬空)。如果后续继续访问原对象,就会导致程序崩溃。
代码示例:
#include <iostream> #include <memory> using namespace std; struct Date { int _year; Date(int year = 1) : _year(year) {} ~Date() { cout << "~Date()" << endl; } }; int main() { auto_ptr<Date> ap1(new Date); // 拷贝时,管理权限转移,被拷贝对象 ap1 悬空 auto_ptr<Date> ap2(ap1); // 空指针访问,ap1 对象已经悬空,下面代码会崩溃 // ap1->_year++; return 0; }3.2 unique_ptr
unique_ptr翻译过来是“唯一指针”。它的特点非常鲜明:不支持拷贝,只支持移动。
适用场景:如果不需要拷贝的场景,非常建议使用它。
移动语义:虽然不支持拷贝,但支持
std::move移动。注意,移动后原对象也会悬空,所以使用移动要谨慎。
示例:
#include <iostream> #include <memory> using namespace std; struct Date { int _year; Date(int year = 1) : _year(year) {} ~Date() { cout << "~Date()" << endl; } }; int main() { unique_ptr<Date> up1(new Date); // 不支持拷贝 // unique_ptr<Date> up2(up1); // 编译报错 // 支持移动,但是移动后 up1 也悬空 unique_ptr<Date> up3(move(up1)); return 0; }3.3 shared_ptr
shared_ptr是“共享指针”。它的特点是支持拷贝,也支持移动。底层是通过引用计数的方式实现的。
机制:每当有一个新的
shared_ptr指向同一块资源,引用计数 +1;每当一个shared_ptr析构,引用计数 -1。当引用计数为 0 时,释放资源。适用场景:如果需要拷贝的场景,就需要使用它。
示例:
int main() { shared_ptr<Date> sp1(new Date); // 支持拷贝,引用计数增加 shared_ptr<Date> sp2(sp1); shared_ptr<Date> sp3(sp2); cout << sp1.use_count() << endl; // 输出 3 sp1->_year++; cout << sp1->_year << endl; // 输出 2 cout << sp2->_year << endl; // 输出 2 cout << sp3->_year << endl; // 输出 2 // 支持移动,但是移动后 sp1 也悬空 shared_ptr<Date> sp4(move(sp1)); return 0; }3.4 weak_ptr
weak_ptr是“弱指针”。它完全不同于上面的智能指针。
特点:不支持 RAII,意味着不能用它直接管理资源。它不增加引用计数。
产生本质:是为了解决
shared_ptr的循环引用导致内存泄漏的问题。
weak_ptr 的核心价值:通常发生在两个对象互相持有对方的 shared_ptr 时,导致引用计数永远无法归零。
错误版本:
#include <iostream> #include <memory> struct Node { int val; std::shared_ptr<Node> next; // shared_ptr互相引用 Node(int v) : val(v) { std::cout << "构造 Node " << val << "\n"; } ~Node() { std::cout << "析构 Node " << val << "\n"; } }; int main() { auto a = std::make_shared<Node>(1); auto b = std::make_shared<Node>(2); a->next = b; b->next = a; // 循环引用! // main函数结束,a、b离开作用域 // 但是a、b内部互相持有shared_ptr,引用计数 != 0 // 析构函数不会调用 → 内存泄漏! return 0; }weak_ptr修复版本:
#include <iostream> #include <memory> struct Node { int val; std::weak_ptr<Node> next; // weak_ptr 弱指针,不增加引用计数 Node(int v) : val(v) { std::cout << "构造 Node " << val << "\n"; } ~Node() { std::cout << "析构 Node " << val << "\n"; } }; int main() { auto a = std::make_shared<Node>(1); auto b = std::make_shared<Node>(2); a->next = b; b->next = a; // weak_ptr使用:lock() 尝试提升为shared_ptr if (auto sp = a->next.lock()) { std::cout << "a 的 next 值:" << sp->val << "\n"; } // main结束,a,b销毁;weak_ptr不影响计数,对象正常释放 return 0; }3.5 make_shared
shared_ptr除了支持用指向资源的指针构造,还支持make_shared用初始化资源对象的值直接构造。
template <class T, class... Args> shared_ptr<T> make_shared(Args&&... args);基本用法:
#include <memory> // 直接使用构造函数 std::shared_ptr<Foo> p1(new Foo(1, 2)); // 使用 make_shared(推荐) auto p2 = std::make_shared<Foo>(1, 2);make_shared将参数完美转发给Foo的构造函数,返回值是一个shared_ptr<Foo>。
优势:
内存分配次数少:直接构造
shared_ptr需要两次内存分配(对象本身 + 控制块),而make_shared只分配一次(对象和控制块在同一块内存)。异常安全:避免因参数求值顺序导致的内存泄漏。
int main() { // 直接构造 shared_ptr<Date> sp1(new Date(2024, 9, 11)); // 使用 make_shared 构造 shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11); // 配合 auto 使用更简洁 auto sp3 = make_shared<Date>(2024, 9, 11); return 0; }3.6 explicit 修饰构造函数
shared_ptr和unique_ptr的构造函数都使用了explicit修饰。这是为了防止普通指针隐式类型转换成智能指针对象。
#include <iostream> #include <memory> struct Date { int _year; Date(int year = 1) : _year(year) {} ~Date() { std::cout << "~Date()" << std::endl; } }; void takeSmartPtr(std::shared_ptr<Date> sp) { std::cout << "管理权已移交,当前引用计数: " << sp.use_count() << std::endl; } int main() { // 错误:禁止隐式转换 (编译报错) // shared_ptr<Date> sp1 = new Date(); // takeSmartPtr(new Date()); // 正确:必须显式调用构造函数 std::shared_ptr<Date> sp2(new Date()); takeSmartPtr(sp2); // 正确:使用 make_shared (更加推荐) auto sp3 = std::make_shared<Date>(); takeSmartPtr(sp3); return 0; }4. 删除器
智能指针析构时默认是进行delete释放资源。这就意味着,如果不是new出来的资源,交给智能指针管理,析构时就会崩溃。
为了解决这个问题,智能指针支持在构造时给定一个删除器。删除器本质是一个可调用对象(仿函数、函数指针、Lambda),在智能指针析构时会调用它来释放资源。
4.1 数组的特化版本
因为new[]经常使用,为了简洁,unique_ptr和shared_ptr都特化了一份[]的版本。这样在析构时就会自动调用delete[]。
// 如果没有特化,直接这样写会崩溃(因为默认 delete 不匹配 new[]) // unique_ptr<Date> up1(new Date[10]); // 使用特化版本,管理 new[] 的资源 unique_ptr<Date[]> up1(new Date[5]); shared_ptr<Date[]> sp1(new Date[5]);4.2 自定义删除器
unique_ptr和shared_ptr支持删除器的方式有所不同:
unique_ptr:是在类模板参数中支持的。shared_ptr:是在构造函数参数中支持的。
方法1:
template<class T> class DeleteArray { public: void operator()(T* ptr) { delete[] ptr; } }; // unique_ptr 需要将删除器类型作为模板参数 unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]); // shared_ptr 只需要在构造函数中传入删除器对象 shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());方法2:
// 函数指针做删除器 template<class T> void DeleteArrayFunc(T* ptr) { delete[] ptr; } // unique_ptr 传入函数指针类型 unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>); // shared_ptr 直接传函数指针 shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>); // Lambda 表达式做删除器 auto delArrOBJ = [](Date* ptr) { delete[] ptr; }; // unique_ptr 使用 decltype 推导 Lambda 类型 unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ); // shared_ptr 直接传 Lambda shared_ptr<Date> sp4(new Date[5], delArrOBJ);