一、核心概念深入解析
一、核心概念深入解析
1. shared_ptr
的线程安全性澄清
- 引用计数是原子操作:
shared_ptr
的引用计数(use_count
)在多线程中递增 / 递减是安全的(原子操作),但对象本身的读写需额外同步(如std::mutex
)。 - 误区纠正:线程安全仅针对计数管理,若多个线程同时修改对象数据,仍需加锁避免数据竞争。
2. shared_ptr
构造方式对比
方式 | 示例 | 安全性 | 性能 | 推荐度 |
---|---|---|---|---|
直接构造(显式) | shared_ptr<int> p(new int(42)); | 需避免裸指针重复使用 | 中等 | ★★☆☆☆ |
工厂函数 make_shared | auto p = make_shared<int>(42); | 安全(避免裸指针) | 优(单次分配) | ★★★★★ |
拷贝 / 移动构造 | shared_ptr<int> p2 = p1; | 安全 | 优 | ★★★★☆ |
make_shared
优势:- 减少内存分配次数(对象和控制块一次分配)。
- 避免表达式求值顺序导致的潜在泄漏(如
shared_ptr(populate(), commit())
中若populate
抛异常,commit
可能未调用)。
3. 删除器(Deleter)的高级用法
- 类型:可以是函数指针、lambda、函数对象。
- 示例:lambda 删除器
cpp
shared_ptr<FILE> file(fopen("test.txt", "w"), [](FILE* fp) { fclose(fp); cout << "File closed\n"; });
- 数组管理:
cpp
shared_ptr<int> arr(new int[5], [](int* p) { delete[] p; }); // 自定义删除器处理数组
4. 循环引用的本质与解决
- 场景:双向链表节点互相持有
shared_ptr
cpp
class Node { public:shared_ptr<Node> next;shared_ptr<Node> prev;~Node() { cout << "Node destroyed\n"; } };int main() {auto a = make_shared<Node>();auto b = make_shared<Node>();a->next = b; // a强引用b(b计数+1)b->prev = a; // b强引用a(a计数+1)// 离开作用域时,a和b计数均为1(循环引用),无法释放 → 内存泄漏 }
- 解决方案:用
weak_ptr
打破强引用cpp
class Node { public:shared_ptr<Node> next;weak_ptr<Node> prev; // 弱引用,不影响计数~Node() { cout << "Node destroyed\n"; } }; // 此时,a和b计数均为1,离开作用域时正常释放
5. weak_ptr
的核心特性
- 不影响引用计数:
weak_ptr
构造 / 赋值时,shared_ptr
计数不变。 - 访问数据的唯一方式:
cpp
weak_ptr<int> wp(sp); // wp观察sp if (auto tmp = wp.lock()) { // 转换为shared_ptr,非空时访问cout << *tmp << endl; }
- 用途:
- 避免循环引用。
- 实现 “观察者模式”(观察共享资源是否存在)。
- 缓存(如缓存对象,存在时直接使用,不存在时重建)。
二、面试高频问题与回答模板
问题 1:shared_ptr 循环引用是什么?如何解决?
回答:
循环引用指两个或多个 shared_ptr
互相强引用,导致引用计数无法归零,内存无法释放。
示例:双向链表节点互相持有 shared_ptr
,形成环。
解决方案:
将其中一个引用改为 weak_ptr
(弱引用),weak_ptr
不增加计数,打破循环。
cpp
class Node {
public:shared_ptr<Node> next;weak_ptr<Node> prev; // 弱引用
};
问题 2:weak_ptr 有什么作用?如何访问其指向的数据?
回答:
weak_ptr
是 shared_ptr
的辅助类,用于:
- 解决循环引用(弱引用不影响计数)。
- 观察
shared_ptr
是否存在(通过lock()
检查)。
访问数据步骤:
cpp
weak_ptr<int> wp(sp); // 关联shared_ptr
if (auto locked_sp = wp.lock()) { // 转换为shared_ptr,非空时有效cout << *locked_sp << endl; // 通过shared_ptr访问
}
问题 3:shared_ptr 和 unique_ptr 的区别?
回答:
特性 | shared_ptr | unique_ptr |
---|---|---|
所有权 | 共享(多个指针指向同一资源) | 独占(唯一所有权) |
引用计数 | 有 | 无 |
拷贝 / 赋值 | 允许(计数变化) | 禁止(仅可移动) |
适合场景 | 多指针共享资源 | 独占资源(如函数返回值) |
性能 | 稍低(计数开销) | 更高 |
问题 4:为什么推荐使用 make_shared 而非直接 new?
回答:
- 性能优化:
make_shared
一次分配内存(对象 + 控制块),减少new
和delete
次数。 - 异常安全:避免表达式求值顺序导致的资源泄漏(如
shared_ptr(populate(), commit())
中若populate
抛异常,commit
可能未调用,而make_shared
无此问题)。 - 代码简洁:自动推导类型,无需显式指定模板参数(
auto
配合使用)。
问题 5:shared_ptr 如何管理非堆内存资源(如文件句柄)?
回答:
通过自定义删除器指定释放逻辑,而非默认的 delete
。
示例:
cpp
void fclose_deleter(FILE* fp) { fclose(fp); }
shared_ptr<FILE> file(fopen("test.txt", "r"), fclose_deleter);
// 或用lambda:
// shared_ptr<FILE> file(fopen("test.txt", "r"), [](FILE* fp){ fclose(fp); });
三、总结:面试核心考点
shared_ptr
原理:引用计数、RAII、线程安全边界(计数安全,对象操作需同步)。- 最佳实践:优先
make_shared
,避免裸指针,用weak_ptr
解决循环引用。 weak_ptr
定位:辅助shared_ptr
,仅观察不持有资源,需通过lock()
访问数据。- 场景题:能结合双向链表等场景,分析循环引用成因并给出解决方案。
通过以上梳理,可系统掌握 shared_ptr
/weak_ptr
的核心知识,从容应对面试中的原理分析与场景题。