C++中的智能指针
引言
普通指针使用时存在挂起引用以及内存泄漏的问题,C++ 11中引入了智能指针来解决它
挂起引用:当多个指针指向一个内存时,其中一个指针释放后其他指针依旧执行释放操作,就发生了挂起引用
内存泄漏:在堆区开辟空间后没有在使用结束时将其释放,即new后无delete
std::unique_ptr
std::auto_ptr,时代的眼泪
std::unique_ptr
是std::auto_ptr
的替代品,解决了C++ 11之前std::auto_ptr
的很多缺漏
简单的看一下std::auto_ptr
的复制构造函数
template<typename T>
class auto_ptr {
public:
// Codes..
auto_ptr(auto_ptr& atp) {
m_ptr = atp.m_ptr;
atp.m_ptr = nullptr;
}
private:
T* m_ptr;
};
可以很容易的看出,该函数将指针所有权从一个对象转移到另外一个对象,且将原对象置空。该函数中std::auto_ptr
实际上在运用引用去实现移动语义。但若是在转移所有权后仍去访问前一个对象(现在已经被置为空指针),程序会崩溃。
std::auto_ptr<int> atp(new int(10));
std::auto_ptr<int> atp2(atp);
// auto _data = atp.data; // undefined behavior
// 此时的atp已经为nullptr,因为“移动函数”将所有权转接给了另外一个对象
u_ptr的移动构造
在C++ 11中引入了右值引用,可在此基础上实现移动构造函数。如今std::auto_ptr
在C++17中被移除,正式被std::unique_ptr
替代
// unique_ptr的移动构造函数
unique_ptr(unique_ptr&& unip) : m_ptr(unip.m_ptr)
{
unip.m_ptr = nullptr;
}
虽然说这个函数与std::auto_ptr
中的做法一样,但它只能接受右值作为参数。传递右值,即为转换指针所有权
std::unique_ptr<int> a(new int(10));
// std::unique_ptr<int> b(a); // error
std::unique_ptr<int> b(std::move(a)); // ok
u_ptr的构造方式
与std::shared_ptr
不同,该智能指针用于不能被多个实例共享的内存管理,也就是说,仅有一个实例拥有内存所有权
对于智能指针而言,声明后会默认置为空
std::unique_ptr<int> unip1; // 默认置nullptr
std::unique_ptr<int> unip2(new int(5)); // 旧方法
std::unique_ptr<int> unip3 = std::make_unique<int>(10); // 新规范
// std::unique_ptr<int> unip3 = std::make_unique<int>(new int(10)); // 迷惑操作
std::unique_ptr<std::string> unip4 = std::make_unique<std::string>("Pointer");
std::array<int, 10> arr = { 1,2,3,4,5,6,7,8,9,0 };
std::unique_ptr<std::array<int, 10>> unip5 = std::make_unique<std::array<int, 10>>(arr);
需要注意的是,接受指针参数的ctor
是explicit
的,因此我们不能将一个内置指针隐式转换为一个智能指针
template <class _Dx2 = _Dx, _Unique_ptr_enable_default_t<_Dx2> = 0>
explicit unique_ptr(pointer _Ptr) noexcept : _Mypair(_Zero_then_variadic_args_t{}, _Ptr) {}
// std::unique_ptr<int> unip = new int(10); // 错误
u_ptr的unique性
std::unique_ptr
通过删除拷贝构造函数和拷贝赋值函数来确保unique
性,它是move-only的,独占资源
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
可以通过移动语义转移所有权
std::unique_ptr<int> unip = std::make_unique<int>(10);
// std::unique_ptr<int> copy_unip = unip;
// std::unique_ptr<int> copy_unip(unip);
// 移动构造 unip被置空,所有权转到move_unip上
std::unique_ptr<int> move_unip = std::move(unip);
但我们可以“拷贝或是赋值”一份即将销毁的std::unique_ptr
,最常见的例子是从函数返回一个std::unique_ptr
std::unique_ptr<int> clone()
{
std::unique_ptr<int> unip = std::make_unique<int>(10);
// 返回值实际上调用到移动构造
return unip;
}
// 编译器优化为一次移动构造
std::unique_ptr<int> p1 = clone();
std::unique_ptr<int> p2;
// p2调用一次移动赋值 clone调用一次移动构造
p2 = clone();
std::unique_ptr
对象可以传给左值常量引用常数,因为这样并不会改变内存所有权,也可以使用移动语义进行右值传值(注意区分转移所有权和std::move
的区别)
class Test {};
void right_param(std::unique_ptr<Test>&& t) {}
void cref_param(const std::unique_ptr<Test>& t) {}
int main()
{
auto temp = std::make_unique<Test>();
// 错误 无法将右值引用绑定到左值
// right_param(temp);
// 利用std::move强转为右值类型
right_param(std::move(temp));
cref_param(std::move(temp));
cref_param(temp);
}
u_ptr构造的坑点
对于该智能指针来说,应避免犯以下两个错误(std::unique_ptr
与std::shared_ptr
同理)
-
用同一份指针初始化多个智能指针
int *p = new int(10); std::unique_ptr<int> unip(p); std::unique_ptr<int> unip2(p);
拯救方法,释放其中一个智能指针的管理权
unip.release();
-
混用普通指针与智能指针
int *p = new int(10); std::unique_ptr<int> unip(p); delete p; // 离开作用域后unip将会释放一块已经释放的内存
u_ptr的主动弃权与释放
std::unique_ptr
中有get()
,release()
,reset()
方法
get()
可以获得智能指针中管理的资源,返回的是指针release()
会返回指向所管理的资源的指针,并释放其管理权(并不会析构)reset()
会调用构造函数并回收内存,同时reset()
还可以接受一个内置指针
std::unique_ptr<float> unip = std::make_unique<float>(10);
std::unique_ptr<float> unip2 = std::make_unique<float>(20);
// unip.reset(unip2.get()) // 绝对禁止的操作 会导致挂起引用
unip.reset(unip2.release()); // unip析构其管理的内存后 接收unip2转接过来的所有权
// unip = std::move(unip2); // 与reset-release相同
u_ptr的size
在std::unique_ptr
使用默认删除器的时候,它的大小与原始指针的大小相同。当用户传入自定义删除器时,std::unique_ptr
的大小需要分情况讨论
void deleter(int* p) { delete p; }
int main() {
// 32位环境下 uniquePtr1的大小是8
std::unique_ptr<int, void(*)(int*)> uniquePtr1(new int(10), deleter);
// 32位环境下 uniquePtr2的大小是48 函数对象本身的大小是40
std::unique_ptr<int, std::function<void(int*)>> uniquePtr2(new int(10), deleter);
// 32位环境下 uniquePtr3的大小是4
auto lambda = [](int* p) { delete p; };
std::unique_ptr<int, decltype(lambda)> uniquePtr3(new int(10), lambda);
}
综上所述,无状态的函数对象(例如无捕获的lambda表达式)的存储大小是最优的。并且若std::unique_ptr
中使用了自定义删除器,即使它们管理的是同类型的数据,它们本身也是不同的类型
std::shared_ptr
std::shared_ptr
与std::unique_ptr
的主要区别在于前者是使用引用计数的智能指针,后者是独占资源的智能指针。强引用计数维护了“引用同一个真实指针对象的智能指针实例”的数目。这意味着,可以有多个std::shared_ptr
实例可以指向同一块动态分配的资源块和控制块
s_ptr的构造
与std::unique_ptr
不同,std::shared_ptr
支持拷贝构造,也允许拷贝赋值
std::shared_ptr<int> shrp = std::make_shared<int>(10);
std::shared_ptr<int> shrp2 = shrp;
std::shared_ptr<int> shrp3(shrp2);
std::cout << shrp.use_count() << std::endl; // 3
更重要的是,std::shared_ptr
可以由std::unique_ptr
移动而来(或从返回std::unique_ptr
的函数构建,本质上也是移动)
auto lambda = [](int* p) { delete p; };
std::unique_ptr<int, std::function<void(int*)>> uniquePtr(new int(10), lambda);
// 构建sharedPtr
std::shared_ptr<int> sharedPtr = std::move(uniquePtr);
s_ptr的应用场景
假设第三方库有这么一些代码
struct IntData {
int data = 100;
~IntData() { std::cout << "int data destroy" << std::endl; }
};
class OtherLibHandler {
public:
template<typename T>
void* Create() { return new T(); }
template<typename T>
void Release(void *p) { delete reinterpret_cast<T*>(p); }
};
static OtherLibHandler& GetHandle() {
static OtherLibHandler p;
return p;
}
第三方库通常会通过接口提供原始指针,用来管理其中的内存,既然是原始指针,就总会出现忘记使用库中的释放函数,导致内存泄露的情况
void* p = GetHandle().Create<IntData>();
// Codes..
// GetHandle().Release<IntData>(p); //程序员由于不可抗力导致漏写这行代码了
那么使用智能指针去维护就会显得非常方便,以下仅提供代码思路
auto deleter = [](void* p) { GetHandle().Release<IntData>(p); };
std::shared_ptr<void> shrp(GetHandle().Create<IntData>(), deleter);
包装成一个函数
template<typename T>
std::shared_ptr<T> GetShared(void* p) {
std::shared_ptr<T> shrp(reinterpret_cast<T*>(p), [](void* p) { GetHandle().Release<T>(p); });
return shrp;
}
int main() {
std::shared_ptr<Intdata> shrp = GetShared<IntData>(GetHandle().Create<IntData>());
}
但是包装成函数后会有这么一个问题,GetShared
返回出来的智能指针必须得被接下来(不管是左值或是右值引用,或是常量左值引用),否则该对象将会被释放,导致后续代码去访问野指针的内容
void* p = GetHandle().Create<IntData>();
GetShared<IntData>(p);
// 出现不确定的结果
std::cout << reinterpret_cast<IntData*>(p)->data << std::endl;
那我们可以使用宏来确保一定创建一个对象来接住返回值
#define CreateShared_IntData(p) auto shared_##p = GetShared<IntData>(p);
template<typename T>
std::shared_ptr<T> GetShared(void* p) {
std::shared_ptr<T> shrp(reinterpret_cast<T*>(p), [](void* p) { GetHandle().Release<T>(p); });
return shrp;
}
int main() {
auto origin_p = GetHandle().Create<IntData>();
CreateShared_IntData(origin_p);
// 100
std::cout << shared_origin_p->data << std::endl;
}
s_ptr的API
- 使用
use_count()
来查看std::shared_ptr
的强引用次数 reset()
则可以释放关联内存块的所有权(相当于std::unique_ptr
的release()
,但它没有返回值 ),如果是最后一个指向该资源的std::shared_ptr
,就释放这块内存。同时reset()
还可以传入一个新的原始指针,来让智能指针管理新的内存- 三种智能指针都有
swap()
,用于交换两个同种类型的指针对象
s_ptr的坑点
上文中提到过我们应该避免用同一份指针初始化多个智能指针
int *p = new int(10);
std::shared_ptr<int> shrp(p);
std::shared_ptr<int> shrp2(p);
// 导致同一片地址的资源被释放两次
因为这两个智能指针是独立初始化的,所以它们之间并没有通讯共享引用计数。std::shared_ptr
的内部实际上维护两个指针,一个用于管理实际的资源,另外一个则指向一个控制块,其中记录了有哪些std::shared_ptr
共同管理同一片内存。
// std::shared_ptr的基类_Ptr_base维护了两个指针
private:
element_type* _Ptr{nullptr};
_Ref_count_base* _Rep{nullptr};
这是在初始化完成的,所以如果单独初始化两个对象,尽管管理的是同一块内存,它们各自的”控制块“没有互相记录的。但是如果是使用复制构造函数还有赋值运算时,控制块将会同步更新,这样就达到了引用计数的目的。
s_ptr的引用次数
std::shared_ptr
中采用原子操作进行引用计数的增加和减少,因此它在多线程环境下依旧能正常工作
并且s_ptr的引用次数在移动操作中不会增加或者减少。对于移动操作来说,代表了一个std::shared_ptr
的消散和一个std::shared_ptr
的诞生,结果就是不需要进行任何引用计数的操作。因此std::shared_ptr
的移动操作会比复制操作更快
s_ptr的size
std::shared_ptr
的大小是普通指针的两倍,因为他内部维护了两个指针分别指向资源块和控制块。
与std::unique_ptr
不同,std::shared_ptr
自定义删除器不会改变智能指针的类型,这意味着提供不同种类删除器的std::shared_pt
r能够被放到同一个容器中,如
std::vector<std::shared_ptr<int>> vec;
并且为std::shared_ptr
提供删除器不会改变智能指针本身的大小,std::shared_ptr
会对删除器(或内存分配器)做出一份拷贝,然后存放在控制块上(在堆上),由指向控制块的指针管理
死锁与自锁
如果两个强引用类型的智能指针互相引用会如何
//照搬知乎上的代码
class Person
{
public:
Person(std::string name) :
m_name(std::move(name)), m_partner(nullptr)
{
std::cout << m_name << " created" << std::endl;
}
~Person()
{
std::cout << m_name << " destoryed" << std::endl;
}
friend void partnerUp(std::shared_ptr<Person>& shrp1, std::shared_ptr<Person>& shrp2)
{
if (!shrp1 || !shrp2)
return;
shrp1->m_partner = shrp2;
shrp2->m_partner = shrp1;
}
private:
string m_name;
std::shared_ptr<Person> m_partner;
};
int main()
{
{
auto shrp1 = std::make_shared<Person>("Lucy");
auto shrp2 = std::make_shared<Person>("Ricky");
partnerUp(shrp1, shrp2); // 互相设为伙伴
}
return 0;
}
运行之后可以发现控制台只输出了两行created字符,也就是说对象并没有被析构,导致了内存的泄漏。当作用域结束时,理应执行析构函数,但是当析构shrp1时,却发现shrp2内部引用了shrp1,那么就得先析构shrp2,但是发现shrp1中内部又引用了shrp1,互相引用导致了 “ 死锁 ” ,最终导致内存泄漏
“自锁”同理。
auto shrp = std::make_shared<Person>("Lucy");
partnerUp(shrp, shrp);
那么这种情况下就需要使用到std::weak_ptr
了
std::weak_ptr
std::weak_ptr
能够引用std::shared_ptr
中的内存,但是它只作为旁观者的存在,并不享有内存的所有权,也就是说使用std::weak_ptr
去接受一个std::shared_ptr
,并不会增加强引用计数,当然也不会影响到std::shared_ptr
的析构,有效的阻止了 “ 死锁 ”, “ 自锁 ” 的问题
class Person
{
//Codes..
private:
string m_name;
std::weak_ptr<Person> m_partner; //此时程序能够正常析构了
};
我们并不能直接访问std::weak_ptr
中记录的数据,即std::weak_ptr
没有operator*
,operator->
之类的重载。std::weak_ptr
更大的作用是作为一位旁观者
w_ptr的构建
std::weak_ptr
一般通过std::shared_ptr
来创建的,会增加源std::shared_ptr
的弱引用计数,增加计数是原子操作,因此是线程安全的
std::shared_ptr<int> shrp = std::make_shared<int>(20);
std::weak_ptr<int> wkp(shrp);
若利用一个已经失效的std::weak_ptr
去构建一个std::shared_ptr
(注意std::shared_ptr
接受std::weak_ptr
的构造函数是显式的),那么会抛出std::bad_weak_ptr
类型的异常
std::shared_ptr<int> shrp1 = std::make_shared<int>(20);
std::weak_ptr<int> wkp(shrp1);
shrp1 = nullptr;
// 抛出异常
std::shared_ptr<int> shrp2(wkp);
w_ptr的API
-
使用
lock()
可以返回一个它所监视的std::shared_ptr
。若std::weak_ptr
失效,那么将会返回nullptr
-
使用
expired()
可以用于检测std::weak_ptr
是否失效bool expired() const noexcept { return this->use_count() == 0; }
-
使用
use_count()
可以获取他所监视的std::shared_ptr
的强引用计数std::shared_ptr<int> shrp = std::make_shared<int>(10); std::weak_ptr<int> wkp = shrp; auto shrp2 = wkp.lock(); auto shrp3 = wkp.lock(); std::cout << wkp.use_count() << std::endl; // 3
注意,
std::weak_ptr
和std::shared_ptr
一样,都是派生自_Ptr_base
的,use_count()
是基类中的方法
w_ptr的应用场景
如Effective Modern C++中所说,若有一个运行代价高昂的函数,且传入的ID会频繁的重复
std::unique_ptr<const Widget> loadWidget(int ID);
那么为了提高效率,一个直观的方法就是使用哈希表对ID进行缓存
std::shared_ptr<const Widget> fastLoadWidget(int ID)
{
static std::unordered_map<int, std::weak_ptr<const Widget>> cached;
std::shared_ptr<const Widget> objPtr = cached[ID].lock();
if (objPtr == nullptr)
{
// 使用std::unique_ptr构建std::shared_ptr
objPtr = loadWidget(ID);
// 使用std::shared_ptr构建std::weak_ptr
cached[ID] = objPtr;
}
return objPtr;
}
树状环路问题
在树状结构中,可能存在父节点指向子节点,子节点指向父节点的环路。但是这种数据结构的特性是:子节点通常是被父节点拥有,子节点无法脱离父节点生存,当父节点被析构时,子节点也需要被析构
因此这种情况可以不使用std::shared_ptr
和std::weak_ptr
的组合,改为使用std::unique_ptr
和原始指针,能在保证安全的前提下,效率更高
其他
std::make_unique的实现
运用变参模板,简单实现my_make_unique
// 不支持处理数组
template<typename T, typename... Ts>
std::unique_ptr<T> my_make_unique(Ts&&... args) {
return std::unique_ptr<T>(new T(std::forward<Ts>(args)...));
}
这里在new T
使对形参的完美转发采用的是小括号而非大括号,与标准库中的设计一致,因此:
// 创建一个含有10个元素的动态数组 元素的值都是20
std::unique_ptr<std::vector<int>> uniquePtr = my_make_unique<std::vector<int>>(10, 20);
// 数组中含有两个元素 分别是10和20
std::unique_ptr<std::vector<int>> uniquePtr = my_make_unique<std::vector<int>>(std::initializer_list<int>{10, 20});
完整的make_unique
实现
// 普通构造函数
template <class _Ty, class... _Types, enable_if_t<!is_array_v<_Ty>, int> = 0>
unique_ptr<_Ty> make_unique(_Types&&... _Args) {
return unique_ptr<_Ty>(new _Ty(_STD forward<_Types>(_Args)...));
}
// std::unique_ptr<T[]>
template <class _Ty, enable_if_t<is_array_v<_Ty> && extent_v<_Ty> == 0, int> = 0>
unique_ptr<_Ty> make_unique(const size_t _Size) {
using _Elem = remove_extent_t<_Ty>;
return unique_ptr<_Ty>(new _Elem[_Size]());
}
// 过滤定长数组
template <class _Ty, class... _Types, enable_if_t<extent_v<_Ty> != 0, int> = 0>
void make_unique(_Types&&...) = delete;
std::make_的优点
对于std::shared_ptr
而言,内部维护了两个指针指向资源块和控制块,前者是智能指针管理的对象,后者用于记录引用次数。如果使用构造函数,利用一个指向堆上数组的指针初始化智能指针,那么控制块是单独分配的,即资源块与控制块处于两个内存中
而若采用std::make_shared
方法,资源块和控制块将分配到同块内存中,而在程序运行中,内存分配是代价高昂的操作,因此std::make_shared
效率更高
另一个比较重要的点是异常安全,使用std::make_
是异常安全的。考虑以下代码
void processWidget(std::shared_ptr<Widget> spw, int priority);
processWidget(std::shared_ptr<Widget>(new Widget()), computePriority());
当以上述的形式调用函数的时候,一共需要执行三件事
- 一个
Widget
需要被new
出来,即需要在堆上分配内存 - 一个
std::shared_ptr<Widget>
对象需要被创建出来 computePriority()
需要被调用
由于编译器可能会重排指令,导致computePriority()
操作可能会发生中间,且此时computePriority()
在执行途中抛出了异常,导致std::shared_ptr<Widget>
没有被创建,进而导致new
出来的对象没有办法被释放,导致内存泄漏
而因为std::make_
是一个函数,所以在堆上分配内存和创建std::shared_ptr<Widget>
是一个整体,不会被分割,所以是异常安全的
std::make_的缺陷
-
std::make_
函数都不允许使用定制删除器,deleter
只能在std::unique_ptr
或std::shared_ptr
的构造函数中传入// 先构建好一个智能指针对象再传入 使异常安全 std::shared_ptr<Widget> spw(new Widget(), funcObj); // 移动传参 提高性能 processWidget(std::move(spw), computePriority());
-
std::make_
函数不能完美的传递std::initializer_list
// auto unip = std::make_unique<std::vector<int>>({ 1,2,3 }); // 错误 std::initializer_list<int> il = { 1,2,3 }; auto unip = std::make_unique<std::vector<int>>(il); // 正确
-
当构造函数是私有或者保护时,无法
std::make_
,std::make_
实际上是类似调用到了构造函数。解决方法是将std::make_
声明为友元函数class PrivateCtorClass { friend std::unique_ptr<PrivateCtorClass> std::make_unique<PrivateCtorClass>(); PrivateCtorClass() : data(100) {} public: int data; }; int main() { std::unique_ptr<PrivateCtorClass> up = std::make_unique<PrivateCtorClass>(); std::cout << up->data << std::endl; }
-
对于
std::shared_ptr
来说,对象的内存可能无法及时回收虽然
std::make_shared
减少了内存分配次数,提高了性能,但是在回收内存的时候存在一点问题:我们知道,当对象的引用计数降为0时(强引用次数降为0,与弱引用次数无关,
std::weak_ptr
只是个旁观者),对象被销毁(调用析构函数并释放内存)。但是对于资源快与控制块处于同一块内存上的情况而言,由于内存必须完整地申请释放,因此只有当控制块也可以被回收时,这块内存才会被释放。而控制块中包含了两个计数,一个为强引用(std::shared_ptr
)计数,一个为弱引用(std::weak_ptr
)计数,当两个引用计数都归为0时,控制块才能释放。那么也就是说只要还有一个std::weak_ptr
指向控制块(弱引用计数 > 0),控制块就不能被释放,也就代表这块内存也不能被释放(即使std::shared_ptr
已经离开作用域了)总的来说,通过
std::make_shared
分配出来的这块内存,只有当最后一个std::shared_ptr
与std::weak_ptr
都被销毁时,才会被释放。下面为验证代码int main() { // 64位的程序才能申请2G大小的内存 std::shared_ptr<char[]> shrp(new char[INT32_MAX]); std::weak_ptr<char[]> wp(shrp); shrp.reset(); // 在等待任意键的时候 shrp管理的资源已经得到释放 程序内存占用很低 system("pause"); }
int main() { // 64位的程序才能申请2G大小的内存 std::shared_ptr<char[]> shrp = std::make_shared<char[]>(INT32_MAX); std::weak_ptr<char[]> wp(shrp); shrp.reset(); // 在等待任意键的时候 shrp管理的资源没有得到释放 程序内存占用高达2G system("pause"); }
使用智能指针管理动态二维数组
正常我们申请动态二维数组是这么做的
int** p = new int*[3];
for (int i = 0; i < 3; i++)
p[i] = new int[5]{};
for (int i = 0; i < 3; i++)
delete[] p[i];
delete[] p;
有了智能指针后,我们可以做出这么一件明明可以用std::vector
但就是偏偏要折磨自己的事情
// C++20下编译通过 创建一个3*5的元素都为10的二维数组
std::shared_ptr<std::shared_ptr<int[]>[]> sp = std::make_shared<std::shared_ptr<int[]>[]>(3);
for (int i = 0; i < 3; i++)
sp[i] = std::make_shared<int[]>(5, 10);
智能指针的deleter
智能指针与动态数组
在C++17之前,std::shared_ptr
的构造与std::unique_ptr
存在一点差异:std::unique_ptr
支持动态数组,而std::share_ptr
不支持动态数组
std::unique_ptr<int[]> unip(new int[10]); // 合法
std::shared_ptr<int[]> shrp(new int [10]); // 在C++ 17之前时非法操作 不能传入数组类型作为模板参数
std::shared_ptr<int> shrp2(new int[10]); // 可编译,但存在未定义行为
因此,对于shrp2
而言,在它析构时会发生这种情况
int* p = new int[10];
delete p;
这对于new int[10]
而言肯定是非法的,对它应使用delete[]
自定义deleter-s_ptr
对此我们有两种解决方法,一种是传入std::default_delete
,另外一种是自行构造删除器
std::shared_ptr<int> shrp(new int[10], std::default_delete<int[]>());
// lambda表达式转换为函数指针
std::shared_ptr<int> shrp1(new int[10], [](int* p) { delete[] p; });
void deleter(int* p) { delete[] p; }
std::shared_ptr<int> shrp2(new int[10], deleter);
// 还可以封装一个my_make_shared_dynamic_arr的方法
template<typename T>
decltype(auto) my_make_shared_dynamic_arr(std::size_t size)
{
return std::shared_ptr<T>(new T[size], std::default_delete<T[]>());
}
std::shared_ptr<int> shrp3 = my_make_shared_dynamic_arr(10);
虽然说能用了,但存在着几个缺点
- 我们想管理的是
int[]
类型,但std::shared_ptr
的模板类型却为int
- 需要显示提供删除器
- 无法使用
std::make_shared
,无法保证异常安全 - 由于
std::shared_ptr<int>
没有重载operator[]
,故需要使用.get()[Index]
来获取数组中的元素
有人可能会说了,那使用std::shared_ptr<int*>
不就行了吗,数组退化成指针
其实事情并没有这么简单
int* p = new int[10]{};
std::shared_ptr<int*> shrp(&p, [](int** p) { delete[] *p; });
std::cout << (*(shrp.get()))[5] << std::endl;
自定义deleter-u_ptr
对于std::shared_ptr
来说,自定义一个删除器是比较简单的,而对于std::unique_ptr
来说,情况有点不同
std::shared_ptr<int> shrp(new int(10), [](int* p) { delete p; }); // 正确
// std::unique_ptr<int> unip(new int(10), [](int* p) { delete p; }); // 错误
std::unique_ptr
在指定删除器时需要在模板参数中传入删除器的类型
// 一个接受int*且无返回值的函数指针
std::unique_ptr<int,void(*)(int*)> unip(new int(10), [](int* p) { delete p; });
如果在lambda表达式中捕获了变量,那么需要使用std::function
来进行包装,因为捕获了变量的lambda表达式无法转换为函数指针
// std::unique_ptr<int, void(*)(int*)> unip(new int(10), [&](int* p) {delete p; });
std::unique_ptr<int, std::function<void(int*)>> unip(new int(10), [&](int* p) { delete p; });
除了用lambda表达式,我们还可以这么干
template<typename T>
void deleter(T* p) { delete[] p; }
//使用decltype类型推断
std::unique_ptr<int, decltype(deleter<int>)*> unip(new int(10), deleter<int>);
//使用typedef取别名
typedef void(*deleter_int)(int*);
std::unique_ptr<int, deleter_int> unip(new int(10), deleter<int>);
//联合typedef与decltype
typedef decltype(deleter<int>)* deleter_int_TD;
std::unique_ptr<int, deleter_int_TD> unip(new int(10), deleter<int>);
利用using
+decltype
,间接直观
template<typename T>
void deleter(T* p) { delete[] p; }
template<typename T>
using deleter_t = decltype(deleter<T>)*;
int main() {
std::unique_ptr<int, deleter_t<int>> unip(new int(10), deleter<int>);
}
C++17后的做法
在C++ 17中,std::shared_ptr
支持传入T[]
类型作为模板参数
std::shared_ptr<int[]> shrp(new int[10]{});
std::cout << shrp[3] << std::endl; // 0
在C++20中,可以使用std::make_shared
来创建智能指针
// 分配一个管理有10个int元素的动态数组的std::shared_ptr
std::shared_ptr<int[]> shrp = std::make_shared<int[]>(10);
// 同理也有
std::unique_ptr<int[]> unip = std::unique_ptr<int[]>(10);
手撕智能指针
struct second_ctor_tag {};
struct first_and_second_ctor_tag {};
// 只有空类和非final类才能压缩
template<typename FirstType, typename SecondType, bool = std::is_empty_v<FirstType> && !std::is_final_v<FirstType>>
class my_compressed_pair : private FirstType {
private:
SecondType val;
public:
template<typename... SecondParamTypes>
my_compressed_pair(second_ctor_tag, SecondParamTypes&& ... params)
: FirstType(), val(std::forward<SecondParamTypes>(params)...) {}
template<typename FirstParamType, typename... SecondParamTypes>
my_compressed_pair(first_and_second_ctor_tag, FirstParamType&& first_param, SecondParamTypes&& ... second_params)
: FirstType(std::forward<FirstParamType>(first_param)),
val(std::forward<SecondParamTypes>(second_params)...) {}
inline const FirstType& get_first() const { return *this; }
inline const SecondType& get_second() const { return val; }
inline FirstType& get_first() { return *this; }
inline SecondType& get_second() { return val; }
};
// 针对无法压缩的类的特化
template<typename FirstType, typename SecondType>
class my_compressed_pair<FirstType, SecondType, false> {
private:
FirstType first_val;
SecondType second_val;
public:
template<typename... SecondParamTypes>
my_compressed_pair(second_ctor_tag, SecondParamTypes&& ... params)
: first_val(), second_val(std::forward<SecondParamTypes>(params)...) {}
template<typename FirstParamType, typename... SecondParamTypes>
my_compressed_pair(first_and_second_ctor_tag, FirstParamType&& first_param, SecondParamTypes&& ... second_params)
: first_val(std::forward<FirstParamType>(first_param)),
second_val(std::forward<SecondParamTypes>(second_params)...) {}
inline const FirstType& get_first() const { return first_val; }
inline const SecondType& get_second() const { return second_val; }
inline FirstType& get_first() { return first_val; }
inline SecondType& get_second() { return second_val; }
};
template<typename T>
struct default_deleter {
void operator()(T*pointer) const {
delete pointer;
}
};
my_unique_ptr
- 利用空类继承的Tricky,减少了仿函数以及无捕获Lambda表达式作为删除器时智能指针的空间占用
- 显式的
bool
类型转换,否则在智能指针之间互相比较时会转换为对资源有效性检验结果的比较 - 使用标签分发,调用不同的
my_unique_ptr
构造函数 - 缺陷:包括但不限于以下几点,本实现仅供了解
std::unique_ptr
工作机制使用- 没有特化
T[]
版本的智能指针 - 没有对各类模板类型做进一步的限制(如对返回引用类型施加
add_lvalue_reference_t<T>
) - 没有实现多态类型智能指针的转换关系(如
my_unique_ptr<son>
到my_unique_ptr<father>
的转换)
- 没有特化
template<typename T, typename U = default_deleter<T>>
class my_unique_ptr {
using PointerType = std::remove_reference_t<T>*;
using ElementType = T;
using DeleterType = U;
private:
my_compressed_pair<DeleterType, PointerType> compressed_pair;
public:
template<typename = std::enable_if_t<std::is_class_v<DeleterType>, void>>
explicit my_unique_ptr(PointerType _data = nullptr) : compressed_pair(second_ctor_tag{}, _data) {}
template<typename Dx, typename = std::enable_if_t<std::is_convertible_v<Dx, DeleterType>, void>>
my_unique_ptr(PointerType _data, Dx&& _deleter)
: compressed_pair(first_and_second_ctor_tag{}, std::forward<Dx>(_deleter), _data) {}
my_unique_ptr(my_unique_ptr&& _another) : compressed_pair(first_and_second_ctor_tag{},
std::move(_another.get_deleter()), _another.release()) {}
my_unique_ptr& operator=(my_unique_ptr&& _another) {
if (this != &_another) {
reset(_another.release());
compressed_pair.get_first() = std::move(_another.get_deleter());
}
return *this;
}
my_unique_ptr(const my_unique_ptr&) = delete;
my_unique_ptr& operator=(const my_unique_ptr&) = delete;
~my_unique_ptr() {
reset();
}
public:
PointerType release() {
PointerType originPointer = get();
compressed_pair.get_second() = nullptr;
return originPointer;
}
void reset(PointerType new_pointer = nullptr) {
PointerType originPointer = get();
compressed_pair.get_second() = new_pointer;
compressed_pair.get_first()(originPointer);
};
public:
PointerType get() const {
return compressed_pair.get_second();
}
PointerType operator->() const {
return get();
}
ElementType& operator*() const {
return *get();
}
const DeleterType& get_deleter() const {
return compressed_pair.get_first();
}
DeleterType& get_deleter() {
return compressed_pair.get_first();
}
void swap(my_unique_ptr& _another) {
std::swap(compressed_pair.get_first(), _another.compressed_pair.get_first());
std::swap(compressed_pair.get_second(), _another.compressed_pair.get_second());
}
explicit operator bool() const {
return static_cast<bool>(compressed_pair.get_second());
}
};
my_shared_ptr
- 通过虚函数的调用使
my_shared_ptr
本身不需要包含删除器类型 - 控制块中的计数是原子类型
release
和移动赋值拷贝赋值本质上都是对swap
进行巧妙操作- 缺陷:包括但不限于以下几点,本实现仅供了解
std::shared_ptr
工作机制使用- 没有特化
T[]
版本的智能指针 - 没有对各类模板类型做进一步的限制(如对返回引用类型施加
add_lvalue_reference_t<T>
) - 没有实现多态类型智能指针的转换关系(如
my_shared_ptr<son>
到my_shared_ptr<father>
的转换)
- 没有特化
class base_control_block {
public:
std::atomic<std::size_t> strong_count;
std::atomic<std::size_t> weak_count;
virtual void destroy() = 0;
base_control_block() : strong_count(0), weak_count(0) {}
};
template<typename T, typename U = default_deleter<T>>
class control_block : public base_control_block {
public:
using PointerType = std::remove_reference_t<T>*;
using ElementType = T;
using DeleterType = U;
my_compressed_pair<DeleterType, PointerType> compressed_pair;
template<typename = std::enable_if_t<std::is_class_v<DeleterType>, void>>
control_block(PointerType _data = nullptr) : base_control_block(), compressed_pair(second_ctor_tag {}, _data) {}
template<typename Dx, typename = std::enable_if_t<std::is_convertible_v<Dx, DeleterType>, void>>
control_block(PointerType _data, Dx&& _deleter)
: base_control_block(), compressed_pair(first_and_second_ctor_tag {}, std::forward<Dx>(_deleter), _data) {}
void destroy() override {
compressed_pair.get_first()(compressed_pair.get_second());
}
};
template<typename T>
class my_shared_ptr {
public:
using PointerType = std::remove_reference_t<T>*;
using ElementType = T;
private:
PointerType pResources;
base_control_block*pControl;
public:
my_shared_ptr(PointerType _newResources = nullptr)
: pResources(_newResources), pControl(new control_block<T>(_newResources)) {
++pControl->strong_count;
}
template<typename DeleterType>
my_shared_ptr(PointerType _newResources, DeleterType&& _deleter)
: pResources(_newResources),
pControl(new control_block<T, DeleterType>(_newResources, std::forward<DeleterType>(_deleter))) {
++pControl->strong_count;
}
my_shared_ptr(const my_shared_ptr& _copy) : pResources(_copy.pResources), pControl(_copy.pControl) {
++pControl->strong_count;
}
my_shared_ptr(my_shared_ptr&& _another) : pResources(_another.pResources), pControl(_another.pControl) {
_another.pControl = nullptr;
_another.pResources = nullptr;
}
template<typename _Ty, typename = std::enable_if_t<std::is_same_v<_Ty, my_shared_ptr<ElementType>>, void>>
void operator=(_Ty&& _uni) {
my_shared_ptr(std::forward<_Ty>(_uni)).swap(*this);
}
~my_shared_ptr() {
if (pControl != nullptr) {
--pControl->strong_count;
if (pControl->strong_count == 0)
pControl->destroy();
}
}
public:
PointerType get() const {
return pResources;
}
PointerType operator->() const {
return get();
}
const ElementType& operator*() const {
return *pResources;
}
ElementType& operator*() {
return *pResources;
}
void release() {
my_shared_ptr{}.swap(*this);
}
void swap(my_shared_ptr& _another) {
std::swap(this->pControl, _another.pControl);
std::swap(this->pResources, _another.pResources);
}
std::size_t use_count() const {
return pControl->strong_count;
}
explicit operator bool() const {
return static_cast<bool>(pResources);
}
};