【C++】零基础入门 · 第 16 节:智能指针
在 C 中动态内存管理是一个让人头疼的问题。忘记delete会导致内存泄漏多次delete会导致程序崩溃。智能指针的出现让内存管理变得自动化和安全。1. 为什么需要智能指针1.1 手动内存管理的问题voidproblematic(){int*ptrnewint(42);// 如果这里发生异常...doSomething();// 可能抛出异常deleteptr;// 这行可能永远执行不到}常见问题内存泄漏忘记delete悬垂指针delete后继续使用重复释放多次delete同一个指针异常安全异常导致delete未执行1.2 智能指针的解决方案智能指针利用RAIIResource Acquisition Is Initialization思想构造时获取资源new析构时释放资源delete离开作用域时自动调用析构函数2. unique_ptr独占指针2.1 基本特性unique_ptr表示独占所有权同一时刻只能有一个unique_ptr指向同一个对象不能复制只能移动适用于大多数场景的首选2.2 基本用法#includeiostream#includememoryusingnamespacestd;intmain(){// 创建 unique_ptrunique_ptrintptr1make_uniqueint(42);// 访问值cout*ptr1endl;// 输出42// 检查是否为空if(ptr1){coutptr1 不为空endl;}// 获取原始指针不推荐频繁使用int*rawptr1.get();cout*rawendl;// 输出42// 离开作用域时自动释放内存return0;}2.3 管理数组#includeiostream#includememoryusingnamespacestd;intmain(){// 管理动态数组unique_ptrint[]arrmake_uniqueint[](5);for(inti0;i5;i){arr[i]i*10;}for(inti0;i5;i){coutarr[i] ;// 输出0 10 20 30 40}coutendl;// 自动调用 delete[]return0;}2.4 转移所有权#includeiostream#includememoryusingnamespacestd;intmain(){unique_ptrintptr1make_uniqueint(42);// unique_ptrint ptr2 ptr1; // 错误不能复制unique_ptrintptr2move(ptr1);// 移动所有权// ptr1 现在为空if(!ptr1){coutptr1 为空endl;}cout*ptr2endl;// 输出42return0;}2.5 作为函数参数和返回值#includeiostream#includememoryusingnamespacestd;// 作为参数转移所有权voidtakeOwnership(unique_ptrintptr){cout接收到*ptrendl;// 函数结束时自动释放}// 作为返回值unique_ptrintcreateObject(){returnmake_uniqueint(100);}// 引用传递不转移所有权voiduseObject(constunique_ptrintptr){cout使用对象*ptrendl;}intmain(){autoptrmake_uniqueint(42);useObject(ptr);// 引用传递ptr 仍然有效takeOwnership(move(ptr));// 移动所有权ptr 变为空autonewObjcreateObject();cout*newObjendl;// 输出100return0;}2.6 自定义删除器#includeiostream#includememory#includecstdiousingnamespacestd;intmain(){// 使用自定义删除器管理文件unique_ptrFILE,decltype(fclose)file(fopen(test.txt,w),fclose);if(file){fprintf(file.get(),Hello, Smart Pointer!\n);}// 文件会在 unique_ptr 销毁时自动关闭return0;}3. shared_ptr共享指针3.1 基本特性shared_ptr表示共享所有权多个shared_ptr可以指向同一个对象内部使用引用计数当最后一个shared_ptr销毁时释放对象3.2 基本用法#includeiostream#includememoryusingnamespacestd;intmain(){// 创建 shared_ptrshared_ptrintptr1make_sharedint(42);cout引用计数ptr1.use_count()endl;// 输出1{shared_ptrintptr2ptr1;// 复制引用计数1cout引用计数ptr1.use_count()endl;// 输出2shared_ptrintptr3ptr1;cout引用计数ptr1.use_count()endl;// 输出3}// ptr2 和 ptr3 销毁引用计数-2cout引用计数ptr1.use_count()endl;// 输出1cout*ptr1endl;// 输出42return0;}// ptr1 销毁对象释放3.3 引用计数机制#includeiostream#includememoryusingnamespacestd;classMyClass{public:MyClass(intv):value(v){cout构造valueendl;}~MyClass(){cout析构valueendl;}intvalue;};intmain(){cout 创建对象 endl;shared_ptrMyClassptr1make_sharedMyClass(100);cout\n 复制指针 endl;shared_ptrMyClassptr2ptr1;shared_ptrMyClassptr3ptr1;cout引用计数ptr1.use_count()endl;cout\n 重置指针 endl;ptr2.reset();// 引用计数-1cout引用计数ptr1.use_count()endl;cout\n 程序结束 endl;return0;}输出 创建对象 构造100 复制指针 引用计数3 重置指针 引用计数2 程序结束 析构1003.4 make_shared 的优势// 方式一分开写两次内存分配shared_ptrintptr1(newint(42));// 方式二make_shared一次内存分配shared_ptrintptr2make_sharedint(42);make_shared的优势性能更好一次内存分配对象和控制块一起分配异常安全避免中间状态的内存泄漏更简洁不需要写new3.5 shared_ptr 的陷阱陷阱 1循环引用#includeiostream#includememoryusingnamespacestd;classB;// 前向声明classA{public:shared_ptrBb_ptr;~A(){coutA 析构endl;}};classB{public:shared_ptrAa_ptr;// 问题所在~B(){coutB 析构endl;}};intmain(){shared_ptrAamake_sharedA();shared_ptrBbmake_sharedB();a-b_ptrb;b-a_ptra;couta 引用计数a.use_count()endl;// 输出2coutb 引用计数b.use_count()endl;// 输出2return0;// a 和 b 都不会被析构内存泄漏}解决方案使用weak_ptr见下一节陷阱 2从原始指针创建多个 shared_ptrint*rawnewint(42);shared_ptrintptr1(raw);shared_ptrintptr2(raw);// 错误会导致重复释放陷阱 3shared_ptr 管理数组C17 前// C17 前需要自定义删除器shared_ptrintarr(newint[10],default_deleteint[]());// C17 后可以直接使用shared_ptrint[]arr2(newint[10]);4. weak_ptr弱引用指针4.1 基本特性weak_ptr是shared_ptr的助手不增加引用计数不拥有对象的所有权用于解决循环引用问题可以检测对象是否还存在4.2 基本用法#includeiostream#includememoryusingnamespacestd;intmain(){weak_ptrintweak;{shared_ptrintsharedmake_sharedint(42);weakshared;// weak 指向 shared 管理的对象coutshared 引用计数shared.use_count()endl;// 输出1coutweak 是否过期weak.expired()endl;// 输出0 (false)// 通过 weak 获取 sharedif(autolockedweak.lock()){cout值*lockedendl;// 输出42coutshared 引用计数locked.use_count()endl;// 输出2}}// shared 销毁对象释放coutweak 是否过期weak.expired()endl;// 输出1 (true)autolockedweak.lock();if(!locked){cout对象已不存在endl;}return0;}4.3 解决循环引用#includeiostream#includememoryusingnamespacestd;classB;classA{public:shared_ptrBb_ptr;~A(){coutA 析构endl;}};classB{public:weak_ptrAa_ptr;// 使用 weak_ptr 打破循环~B(){coutB 析构endl;}};intmain(){shared_ptrAamake_sharedA();shared_ptrBbmake_sharedB();a-b_ptrb;b-a_ptra;couta 引用计数a.use_count()endl;// 输出1coutb 引用计数b.use_count()endl;// 输出2return0;// a 和 b 都能正常析构}4.4 实际应用缓存系统#includeiostream#includememory#includeunordered_mapusingnamespacestd;classData{public:string content;Data(string c):content(c){coutData 构造contentendl;}~Data(){coutData 析构contentendl;}};classCache{private:unordered_mapstring,weak_ptrDatacache;public:shared_ptrDataget(conststringkey){autoitcache.find(key);if(it!cache.end()){// 尝试获取 shared_ptrif(autoptrit-second.lock()){cout缓存命中keyendl;returnptr;}else{// 对象已被释放删除过期条目cache.erase(it);}}cout缓存未命中keyendl;returnnullptr;}voidput(conststringkey,shared_ptrDatadata){cache[key]data;}};intmain(){Cache cache;{autodatamake_sharedData(Hello);cache.put(greeting,data);autoretrievedcache.get(greeting);if(retrieved){cout内容retrieved-contentendl;}}// data 销毁autoretrievedcache.get(greeting);if(!retrieved){cout数据已被释放endl;}return0;}5. 智能指针对比特性unique_ptrshared_ptrweak_ptr所有权独占共享无复制❌✅✅移动✅✅-引用计数无有不影响内存开销最小较大较小适用场景多数场景共享所有权打破循环引用6. 最佳实践6.1 优先使用 unique_ptr// 好明确的所有权unique_ptrWidgetcreateWidget(){returnmake_uniqueWidget();}// 只在需要共享时使用 shared_ptrshared_ptrWidgetsharedmake_sharedWidget();6.2 使用 make_unique 和 make_shared// 好autoptrmake_uniqueint(42);// 避免unique_ptrintptr(newint(42));6.3 避免混用原始指针和智能指针// 危险int*rawnewint(42);shared_ptrintptr(raw);deleteraw;// 重复释放// 好shared_ptrintptrmake_sharedint(42);6.4 使用 weak_ptr 打破循环引用当两个对象需要相互引用时一方使用weak_ptr。7. 总结智能指针是现代 C 内存管理的核心unique_ptr独占所有权首选方案shared_ptr共享所有权引用计数weak_ptr弱引用解决循环引用核心原则优先使用栈对象必要时用智能指针优先使用unique_ptr使用make_unique和make_shared避免混用原始指针和智能指针下一节我们将学习多线程编程智能指针在其中会发挥重要作用。