C++标准库提供了多种智能指针,核心区别在于如何处理拷贝行为。这直接决定了它们在什么场景下能用、什么场景下会出问题。
目录
[1. auto_ptr:被时代抛弃的设计](#1. auto_ptr:被时代抛弃的设计)
[2. unique_ptr:独占,只移不拷](#2. unique_ptr:独占,只移不拷)
[3. shared_ptr:共享所有权与引用计数](#3. shared_ptr:共享所有权与引用计数)
[4. 模拟实现:引用计数的本质](#4. 模拟实现:引用计数的本质)
1. auto_ptr:被时代抛弃的设计
auto_ptr是C++98的产物,思路是"拷贝即转移管理权"。执行auto_ptr<T> ap2(ap1)之后,ap1内部的指针变成nullptr,资源归ap2。后续如果访问ap1,就是空指针行为,轻则崩溃,重则更难排查。
cpp
auto_ptr<Date> ap1(new Date);
auto_ptr<Date> ap2(ap1);
// ap1已经悬空
// ap1->_year++; // 未定义行为
这个设计违背直觉------看起来是拷贝,却暗中修改了源对象。C++11引入移动语义后,auto_ptr被标记为废弃,现代代码不应该再用。
2. unique_ptr:独占,只移不拷
unique_ptr把"独占"语义用语言机制明确表达出来:禁止拷贝,只支持移动 。拷贝构造和拷贝赋值被= delete了,移动构造和移动赋值则转移所有权,源指针同时置空。
cpp
unique_ptr<Date> up1(new Date);
// unique_ptr<Date> up2(up1); // 编译错误
unique_ptr<Date> up2(move(up1)); // OK,但up1悬空了
移动之后up1也不可再访问,但至少移动需要显式写move,让使用者在代码层面意识到所有权在转移。这种设计明确、安全。对于不需要共享资源的场景,unique_ptr是首选:零额外开销,独占语义清晰,也可以平稳转换成shared_ptr。
unique_ptr构造用explicit修饰,防止裸指针隐式转换,这是为了避免不经意间把资源交给智能指针而用户无感知。
自定义删除器方面,unique_ptr把它作为模板参数:
cpp
// 函数指针做删除器
unique_ptr<Date, void(*)(Date*)> up(new Date[5], [](Date* p){ delete[] p; });
// 仿函数做删除器
unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
因为删除器类型是模板参数的一部分,不同删除器的unique_ptr是不同类型。这一点在使用时需要注意:作为函数参数传递时,类型会比较严格。
另外,unique_ptr和shared_ptr都对operator new[]做了特化,可以直接unique_ptr<Date[]> up(new Date[5]),析构时会调delete[],不需要指定删除器。
3. shared_ptr:共享所有权与引用计数
当多个所有者需要共享同一份资源时,shared_ptr上场。它的核心机制是引用计数 :每份资源有一块独立的内存记录当前有多少个shared_ptr指向它。拷贝时计数加一,析构时计数减一,减到零时释放资源。
cpp
shared_ptr<Date> sp1(new Date(2024, 9, 11));
shared_ptr<Date> sp2(sp1);
shared_ptr<Date> sp3 = sp1;
cout << sp1.use_count() << endl; // 3
引用计数存在堆上 ,与管理的资源独立。为什么不能用静态成员?因为一份资源对应一个引用计数,不同的资源对象必须有不同的计数,静态成员是所有对象共享的,根本做不到。所以shared_ptr构造时会new int(1),然后所有拷贝构造和拷贝赋值都操作这个堆上的计数。
shared_ptr的自定义删除器通过构造函数参数 传递,并非模板参数,这一点与unique_ptr不同:
cpp
shared_ptr<FILE> sp(fopen("test.cpp", "r"), [](FILE* f) { fclose(f); });
此外,shared_ptr提供了make_shared<T>(args...),直接用构造资源所需的参数来分配和构造,在堆上一次分配同时包含对象和引用计数控制块,比先new再传给shared_ptr构造函数少一次内存分配,也避免了部分异常安全问题。
4. 模拟实现:引用计数的本质
以下是shared_ptr核心骨架的简化实现,关键点在于引用计数是堆上的int*,以及拷贝、赋值操作对计数的正确维护:
cpp
template<class T>
class shared_ptr {
public:
explicit shared_ptr(T* ptr = nullptr)
: _ptr(ptr), _pcount(new int(1)) {}
template<class D>
shared_ptr(T* ptr, D del)
: _ptr(ptr), _pcount(new int(1)), _del(del) {}
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr), _pcount(sp._pcount), _del(sp._del) {
++(*_pcount);
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp) {
if (_ptr != sp._ptr) {
release(); // 先把自己持有的资源释放(如果计数归零)
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
_del = sp._del;
}
return *this;
}
~shared_ptr() { release(); }
T* get() const { return _ptr; }
int use_count() const { return *_pcount; }
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
void release() {
if (--(*_pcount) == 0) {
_del(_ptr);
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
T* _ptr;
int* _pcount;
function<void(T*)> _del = [](T* ptr) { delete ptr; };
};
release()是核心逻辑:每次计数减一,归零时代表当前对象是最后一个管理者,负责释放资源和计数本身。_del默认为delete,但可以通过构造时传入的删除器覆盖,实现文件句柄等非内存资源的统一管理。拷贝赋值先处理自己的release再指向新对象,顺序不能错------先加新计数再减老计数可以处理自身赋值自身的边界情况,但这里通过_ptr != sp._ptr的检查来避免。