【C++】零基础入门 · 第 14 节:智能指针(unique_ptr、shared_ptr、weak_ptr)
在第 9 节中,我们学习了new和delete来手动管理动态内存,在第 13 节中我们了解了异常处理和 RAII 原则。今天,我们来学习一个把这两者完美结合的工具——智能指针(Smart Pointer)。
智能指针的核心理念很简单:让对象自动管理内存,程序员不需要手动调用delete。它是现代 C++ 中最重要的特性之一,也是写出安全、不泄漏内存的代码的关键。
1. 为什么需要智能指针
1.1 裸指针的烦恼
回顾一下使用裸指针(raw pointer)的问题:
voidrisky(){int*ptr=newint(42);// 如果这里发生了异常...doSomething();// 可能抛出异常deleteptr;// 这行可能永远执行不到!}如果doSomething()抛出异常,delete ptr不会被执行,内存就泄漏了。虽然我们可以用try-catch来解决,但代码会变得非常臃肿。
1.2 智能指针的解决方案
智能指针利用 RAII 原则:在构造时获取资源(new),在析构时释放资源(delete)。当智能指针离开作用域时,析构函数会自动被调用,即使是因为异常导致的离开。
#include<memory>voidsafe(){std::unique_ptr<int>ptr=std::make_unique<int>(42);doSomething();// 即使抛出异常,ptr 的析构函数也会自动调用// 不需要手动 delete!}2. unique_ptr:独占所有权
2.1 基本用法
unique_ptr是最常用的智能指针。它表示独占所有权——同一时刻只有一个unique_ptr拥有某个对象。
#include<iostream>#include<memory>usingnamespacestd;intmain(){// 创建 unique_ptr(推荐使用 make_unique)unique_ptr<int>ptr=make_unique<int>(42);// 通过 * 解引用访问值cout<<"值:"<<*ptr<<endl;// 输出:42// 通过 -> 访问成员(用于对象时)// ptr->member// 离开作用域时自动释放内存return0;}2.2 不可复制,只能移动
unique_ptr不允许复制(copy),因为复制会导致两个指针指向同一块内存,违反了「独占」的原则。但它允许移动(move):
#include<iostream>#include<memory>usingnamespacestd;intmain(){unique_ptr<int>ptr1=make_unique<int>(42);// unique_ptr<int> ptr2 = ptr1; // 编译错误!不能复制unique_ptr<int>ptr2=std::move(ptr1);// 移动所有权// 移动后,ptr1 变为空指针cout<<"ptr1:"<<(ptr1?"有效":"为空")<<endl;// 输出:为空cout<<"ptr2:"<<*ptr2<<endl;// 输出:42return0;}2.3 管理数组
unique_ptr也可以管理动态数组:
#include<iostream>#include<memory>usingnamespacestd;intmain(){unique_ptr<int[]>arr=make_unique<int[]>(5);for(inti=0;i<5;i++){arr[i]=i*10;}for(inti=0;i<5;i++){cout<<arr[i]<<" ";// 输出:0 10 20 30 40}cout<<endl;// 自动调用 delete[] 释放return0;}2.4 自定义删除器
有时候你需要在释放资源时执行自定义操作(比如关闭文件、释放网络连接):
#include<iostream>#include<memory>#include<cstdio>usingnamespacestd;intmain(){// 用 unique_ptr 管理 FILE*,自定义删除器调用 fcloseunique_ptr<FILE,decltype(&fclose)>file(fopen("test.txt","w"),&fclose);if(file){fprintf(file.get(),"Hello, Smart Pointer!\n");}// file 离开作用域时自动调用 fclosereturn0;}3. shared_ptr:共享所有权
3.1 基本用法
shared_ptr表示共享所有权——多个shared_ptr可以同时拥有同一个对象。它内部维护一个引用计数,当最后一个shared_ptr被销毁时,才会释放内存。
#include<iostream>#include<memory>usingnamespacestd;intmain(){shared_ptr<int>ptr1=make_shared<int>(42);cout<<"引用计数:"<<ptr1.use_count()<<endl;// 输出:1{shared_ptr<int>ptr2=ptr1;// 复制,引用计数 +1cout<<"引用计数:"<<ptr1.use_count()<<endl;// 输出:2}// ptr2 离开作用域,引用计数 -1cout<<"引用计数:"<<ptr1.use_count()<<endl;// 输出:1return0;}// ptr1 离开作用域,引用计数归零,自动释放内存3.2 shared_ptr 的开销
shared_ptr比unique_ptr有更多的内存和性能开销:
- 每个
shared_ptr需要额外存储一个指向控制块的指针 - 控制块中包含引用计数、弱引用计数、删除器等信息
- 引用计数的增减是原子操作,有微小的性能开销
所以:能用unique_ptr就不要用shared_ptr。
3.3 make_shared vs new
推荐使用make_shared而不是new来创建shared_ptr:
// 推荐:一次内存分配(对象和控制块一起分配)autoptr=make_shared<int>(42);// 不推荐:两次内存分配(一次 new 对象,一次分配控制块)shared_ptr<int>ptr(newint(42));make_shared不仅代码更简洁,而且性能更好(减少一次内存分配)。
4. weak_ptr:打破循环引用
4.1 循环引用的问题
shared_ptr有一个致命的陷阱——循环引用:
#include<iostream>#include<memory>usingnamespacestd;structB;// 前向声明structA{shared_ptr<B>b_ptr;~A(){cout<<"A 被销毁"<<endl;}};structB{shared_ptr<A>a_ptr;~B(){cout<<"B 被销毁"<<endl;}};intmain(){autoa=make_shared<A>();autob=make_shared<B>();a->b_ptr=b;// A 指向 Bb->a_ptr=a;// B 指向 A// 离开作用域后,a 的引用计数 = 1(被 b->a_ptr 引用)// b 的引用计数 = 1(被 a->b_ptr 引用)// 两者都不会归零,内存泄漏!return0;}运行这段代码,你会发现A 和 B 的析构函数都不会被调用——内存泄漏了。
4.2 weak_ptr 的解决方案
weak_ptr是shared_ptr的「观察者」,它不增加引用计数。通过weak_ptr可以打破循环引用:
#include<iostream>#include<memory>usingnamespacestd;structB;structA{shared_ptr<B>b_ptr;~A(){cout<<"A 被销毁"<<endl;}};structB{weak_ptr<A>a_ptr;// 改用 weak_ptr!~B(){cout<<"B 被销毁"<<endl;}};intmain(){autoa=make_shared<A>();autob=make_shared<B>();a->b_ptr=b;b->a_ptr=a;return0;}// 输出:// A 被销毁// B 被销毁4.3 使用 weak_ptr 访问对象
weak_ptr不能直接访问对象,需要先调用lock()获取一个shared_ptr:
weak_ptr<int>wp=...;// 使用前先检查对象是否还存在if(autosp=wp.lock()){cout<<"对象存在,值为:"<<*sp<<endl;}else{cout<<"对象已被销毁"<<endl;}lock()的行为:
- 如果对象还存在(引用计数 > 0),返回一个有效的
shared_ptr - 如果对象已被销毁,返回一个空的
shared_ptr
5. 三种智能指针对比
| 特性 | unique_ptr | shared_ptr | weak_ptr |
|---|---|---|---|
| 所有权 | 独占 | 共享 | 不拥有(观察) |
| 可复制 | 否 | 是 | 是 |
| 可移动 | 是 | 是 | - |
| 引用计数 | 无 | 有 | 不增加计数 |
| 内存开销 | 最小 | 较大(控制块) | 较小 |
| 典型场景 | 独占资源管理 | 共享资源 | 打破循环引用 |
| 创建方式 | make_unique | make_shared | 从shared_ptr创建 |
6. 使用建议
6.1 优先使用 unique_ptr
如果你的场景中只有一个指针拥有对象,用unique_ptr。它没有额外开销,语义也最清晰。
6.2 需要共享时用 shared_ptr
当多个地方需要共享同一个对象,且对象的生命周期不确定时,用shared_ptr。
6.3 用 weak_ptr 打破循环
如果两个shared_ptr互相引用,把其中一个改为weak_ptr。
6.4 函数参数传递
// 只读访问:传引用或裸指针voidprocess(constWidget&widget);voidprocess(Widget*widget);// 接收所有权:传 unique_ptr(按值)voidtakeOwnership(unique_ptr<Widget>widget);// 共享所有权:传 shared_ptr(按值或按引用)voidshare(shared_ptr<Widget>widget);6.5 不要混用裸指针和智能指针
// 错误!两个独立的智能指针管理同一块内存int*raw=newint(42);unique_ptr<int>p1(raw);unique_ptr<int>p2(raw);// p1 和 p2 都会 delete raw,双重释放!// 正确:只通过智能指针创建autop1=make_unique<int>(42);7. 总结
这一节我们学习了 C++ 的三种智能指针:
unique_ptr:独占所有权,不可复制,推荐首选。shared_ptr:共享所有权,引用计数管理,有额外开销。weak_ptr:观察者,不增加引用计数,用于打破循环引用。
智能指针是现代 C++ 内存管理的基石。掌握了它们,你就再也不需要担心内存泄漏和悬垂指针的问题。下一节我们将继续探索 C++ 的更多高级特性。加油!
