1. 为什么面试官总爱考shared_ptr实现?
作为C++开发者,在技术面试中被要求手写shared_ptr实现几乎是必经之路。这背后有几个深层原因:
首先,shared_ptr完美体现了RAII(Resource Acquisition Is Initialization)这一C++核心思想。它通过对象生命周期自动管理资源,避免了手动new/delete可能导致的内存泄漏问题。面试官通过这个题目可以快速判断候选人对C++核心编程范式的理解程度。
其次,shared_ptr的引用计数机制涉及多线程安全、拷贝控制等中高级主题。一个看似简单的智能指针,实际上需要考虑:
- 引用计数的线程安全性(早期实现使用原子操作)
- 循环引用问题及其解决方案(weak_ptr)
- 自定义删除器的应用场景
- 移动语义与拷贝语义的区别
提示:在实际面试中,面试官通常不会要求完整实现所有功能,而是重点关注引用计数机制和线程安全设计。这也是为什么我们下面实现的版本会做适当简化。
2. 基础框架搭建:成员变量设计
我们先从最基础的成员变量开始。一个最小化的shared_ptr需要包含两个核心成员:
cpp复制template<typename T>
class SharedPtr {
private:
T* _ptr; // 管理的原始指针
int* _count; // 引用计数指针
// 删除器函数对象类型省略...
};
这里有几个关键设计点:
-
引用计数为什么用指针?
多个SharedPtr对象需要共享同一个计数器。如果直接用int类型,每个SharedPtr会有自己独立的计数器副本,无法实现真正的共享。使用指针可以让所有管理同一资源的SharedPtr访问同一个计数器。 -
模板化的设计
使用模板使我们的SharedPtr可以管理任意类型的资源,这是现代C++智能指针的标准做法。 -
删除器的省略
完整实现需要考虑自定义删除器,但为简化起见,我们先使用默认的delete操作。后面会单独讨论这个主题。
3. 构造函数实现细节
3.1 基本构造函数
cpp复制explicit SharedPtr(T* ptr = nullptr)
: _ptr(ptr), _count(new int(1)) {
if (ptr == nullptr) {
*_count = 0; // 空指针的引用计数为0
}
}
关键点解析:
explicit关键字防止隐式转换,避免意外的指针转换- 默认参数允许创建空智能指针
- 新创建的SharedPtr引用计数初始化为1(空指针特殊处理为0)
3.2 拷贝构造函数
cpp复制SharedPtr(const SharedPtr& other)
: _ptr(other._ptr), _count(other._count) {
if (_ptr) {
(*_count)++; // 注意运算符优先级
}
}
这里有一个经典陷阱:运算符优先级。*_count++会被解析为*(_count++),而我们实际需要的是(*_count)++。这种细节正是面试官考察的重点。
3.3 移动构造函数(C++11)
cpp复制SharedPtr(SharedPtr&& other) noexcept
: _ptr(other._ptr), _count(other._count) {
other._ptr = nullptr;
other._count = nullptr;
}
移动构造不增加引用计数,而是"窃取"资源所有权。这是现代C++的重要优化,避免了不必要的引用计数操作。
4. 赋值运算符的重载
赋值运算符需要考虑自赋值和资源释放两个关键问题:
cpp复制SharedPtr& operator=(const SharedPtr& other) {
if (this == &other) { // 自赋值检查
return *this;
}
// 减少当前对象的引用计数
release();
// 接管新资源
_ptr = other._ptr;
_count = other._count;
if (_ptr) {
(*_count)++;
}
return *this;
}
资源释放的逻辑封装在release()私有方法中:
cpp复制void release() {
if (_ptr == nullptr) return;
(*_count)--;
if (*_count == 0) {
delete _ptr;
delete _count;
}
_ptr = nullptr;
_count = nullptr;
}
5. 析构函数实现
析构函数的实现相对简单,但需要考虑空指针的情况:
cpp复制~SharedPtr() {
release();
}
这里复用release()方法,确保引用计数正确减少并在必要时释放资源。
6. 常用成员函数实现
6.1 解引用和箭头运算符
cpp复制T& operator*() const {
return *_ptr;
}
T* operator->() const {
return _ptr;
}
这两个运算符使得SharedPtr可以像原始指针一样使用,是智能指针的核心接口。
6.2 use_count()和unique()
cpp复制int use_count() const {
return _ptr ? *_count : 0;
}
bool unique() const {
return use_count() == 1;
}
这些辅助函数在实际调试和资源管理中非常有用。
7. 线程安全问题的考虑
原始实现不是线程安全的。要实现线程安全的引用计数,可以使用原子操作:
cpp复制#include <atomic>
// 修改_count类型
std::atomic<int>* _count;
// 修改引用计数操作
_count->fetch_add(1, std::memory_order_relaxed);
_count->fetch_sub(1, std::memory_order_acq_rel);
但要注意,即使引用计数是原子的,对托管对象的访问仍然需要额外的同步机制。
8. 自定义删除器的实现
考虑管理数组或特殊资源的情况,我们需要支持自定义删除器:
cpp复制template<typename Deleter = std::default_delete<T>>
class SharedPtr {
private:
Deleter _deleter;
// ...其他成员
public:
template<typename D = Deleter>
SharedPtr(T* ptr, D&& deleter = Deleter())
: _ptr(ptr), _count(new int(1)), _deleter(std::forward<D>(deleter)) {
// ...
}
void release() {
// ...
if (*_count == 0) {
_deleter(_ptr); // 使用自定义删除器
delete _count;
}
// ...
}
};
使用示例:
cpp复制// 管理数组
SharedPtr<int[]> arr(new int[10], [](int* p) { delete[] p; });
// 管理文件指针
SharedPtr<FILE> file(fopen("test.txt", "r"), [](FILE* fp) {
if (fp) fclose(fp);
});
9. 循环引用与weak_ptr
虽然不要求完整实现weak_ptr,但需要理解循环引用问题:
cpp复制struct Node {
SharedPtr<Node> next;
// 如果使用SharedPtr<Node> prev; 会导致循环引用
WeakPtr<Node> prev; // 正确的做法
};
weak_ptr不增加引用计数,打破循环引用。这是shared_ptr设计中的重要组成部分。
10. 面试中的常见问题与回答技巧
在实际面试中,除了写代码,还可能会被问到以下问题:
-
为什么shared_ptr的引用计数要放在堆上?
答:因为需要被多个shared_ptr实例共享,栈上的变量无法实现这一点。 -
shared_ptr的线程安全性如何?
答:引用计数本身是线程安全的(原子操作),但对托管对象的访问需要额外同步。 -
shared_ptr的大小是多少?
答:通常是两个指针大小(管理对象指针和引用计数指针),如果包含删除器和分配器会更大。 -
make_shared和直接构造shared_ptr的区别?
答:make_shared可以一次性分配内存,效率更高,但无法指定自定义删除器。 -
shared_ptr可能的内存泄漏场景?
答:循环引用是最典型的例子,需要使用weak_ptr来避免。
在实现过程中,建议边写边解释设计思路,这比单纯写代码更能展示你的理解深度。例如,在实现引用计数时,可以主动说明:"这里我把引用计数设计为指针,是为了让多个shared_ptr实例可以共享同一个计数器..."
11. 完整实现示例代码
以下是整合后的简化实现(不包含线程安全和完整删除器支持):
cpp复制template<typename T>
class SharedPtr {
public:
explicit SharedPtr(T* ptr = nullptr)
: _ptr(ptr), _count(new int(1)) {
if (ptr == nullptr) {
*_count = 0;
}
}
SharedPtr(const SharedPtr& other)
: _ptr(other._ptr), _count(other._count) {
if (_ptr) {
(*_count)++;
}
}
SharedPtr(SharedPtr&& other) noexcept
: _ptr(other._ptr), _count(other._count) {
other._ptr = nullptr;
other._count = nullptr;
}
~SharedPtr() {
release();
}
SharedPtr& operator=(const SharedPtr& other) {
if (this == &other) {
return *this;
}
release();
_ptr = other._ptr;
_count = other._count;
if (_ptr) {
(*_count)++;
}
return *this;
}
T& operator*() const { return *_ptr; }
T* operator->() const { return _ptr; }
int use_count() const { return _ptr ? *_count : 0; }
bool unique() const { return use_count() == 1; }
private:
void release() {
if (_ptr == nullptr) return;
(*_count)--;
if (*_count == 0) {
delete _ptr;
delete _count;
}
_ptr = nullptr;
_count = nullptr;
}
T* _ptr;
int* _count;
};
12. 实际面试中的实现建议
根据我的面试和被面试经验,在有限时间内实现shared_ptr时,建议按以下优先级:
-
必须实现的核心部分:
- 引用计数机制
- 基本构造函数和拷贝控制
- 解引用运算符
-
加分项:
- 移动语义支持
- 自定义删除器
- 线程安全版本
-
可以提及但不必实现:
- weak_ptr相关机制
- make_shared优化
- 分配器支持
记住,面试官更关注你对核心机制的理解,而不是完整实现所有功能。主动解释设计决策和潜在问题,比默默写代码更能展示你的实力。
