C++智能指针详解
一、智能指针简介
智能指针是一个类似于指针的类,将指针交给这个类对象进行管理,我们就可以像使用指针一样使用这个类,并且它会自动释放资源。
智能指针运用了 RAII 的思想(资源获得即初始化)。RAII 是指,用对象的生命周期来管理资源,类对象创建时拿到资源,析构时释放资源。
RAII 优点:
1、不需要显式释放资源。
2、在对象生命周期内,资源始终都是有效的。
简单的智能指针的示例:
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
// 像指针一样使用,重载 * 和 ->
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
// 析构时释放资源
~SmartPtr()
{
cout << "释放资源\n";
if (_ptr)
delete _ptr;
}
private:
T* _ptr;
};
void func()
{
int* p1 = new int[10];
int* p2 = new int[10];
int* p3 = new int[10];
delete[] p1;
delete[] p2;
delete[] p3;
}
在上述代码中,指针 p1, p2, p3 在创建时都有可能出现异常,如果在 p1 创建时出现异常,那么我们只需要捕获;如果在 p2 创建时出现异常,那么我们除了捕获异常,还需要释放 p1;而如果在 p3 创建时出现异常,那么我们又要释放 p1 和 p2。
要写多个 try catch,这会让我们的代码变得十分复杂,并且可能会有遗漏,造成内存泄漏。
这时,智能指针的优势就体现出来了,只需要把指针交给智能指针进行管理,就能够在生命周期结束时自动释放。
用上面的简单的智能指针示例
void func()
{
SmartPtr<int> sp1(new int[10]);
SmartPtr<int> sp2(new int[10]);
SmartPtr<int> sp3(new int[10]);
}
在对象的生命周期结束后,会自动调用析构释放资源。我们就不需要写复杂的代码,也不用担心内存泄漏的问题了。
二、智能指针的拷贝问题
智能指针的拷贝如果不写的话,默认生成的是浅拷贝。而浅拷贝会使同一份资源释放两次,运行会报错。
void func()
{
SmartPtr<int> sp1(new int[10]);
SmartPtr<int> sp2(sp1);
}
这时候就有多种解决方案:
1、auto_ptr
将资源全部转给一方,将另一方置为空。(不靠谱,现在禁止使用了)
2、unique_ptr
拷贝有问题,干脆禁止拷贝。将拷贝封住,就可以了。(不需要拷贝的场景)
unique_ptr 的简单实现:
template<class T>
class Unique_Ptr
{
public:
Unique_Ptr(T* ptr)
:_ptr(ptr)
{}
// 像指针一样使用,重载 * 和 ->
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
// 析构时释放资源
~Unique_Ptr()
{
cout << "释放资源\n";
if (_ptr)
delete _ptr;
}
Unique_Ptr(const Unique_Ptr<T>& up) = delete;
Unique_Ptr<T>& operator=(const Unique_Ptr<T>& up) = delete;
private:
T* _ptr;
};
3、shared_ptr
通过引用计数解决多次析构问题
用一个引用计数表示当前共有多少对象在使用该指针,每次析构都减引用计数,当引用计数减到0,就释放资源。
为什么引用计数不能为 int 和 静态 static int ?
int:如果引用计数是 int ,当我们改了一个引用计数,其他的对象无法同步。
如:有三个对象 sp1, sp2, sp3,如果sp3拷贝sp2,无法告知sp1,sp1 无法同步引用计数。
static int:如果用静态的,整个类共用一个引用计数,无法区分shared_ptr 管理的多个指针的引用计数。
如:sp1(new int(1)); sp2(new int(2)); sp1 和 sp2 的引用计数肯定是不同的,但用静态无法区分,因为它是整个类共有的。
因此,引用计数用指针或引用最佳。
shared_ptr 简单实现代码
template<class T>
class Shared_Ptr
{
public:
// 引用计数初始为 1
Shared_Ptr(T* ptr)
:_ptr(ptr)
,_count(new int(1))
{}
// 像指针一样使用,重载 * 和 ->
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
// 返回引用计数
int use_count()
{
return *_count;
}
Shared_Ptr(const Shared_Ptr<T>& sp)
{
// 将资源拷贝过来,并 ++引用计数
_ptr = sp._ptr;
_count = sp._count;
++(*_count);
}
Shared_Ptr<T>& operator=(const Shared_Ptr<T>& sp)
{
// 防自己给自己赋值
if (sp._ptr != _ptr)
{
// 赋值会将原本的资源覆盖,因此要进行判断
// 如果是最后一个对象,就析构释放,不是就 减减当前计数
if (--(*_count) == 0)
{
delete _ptr;
delete _count;
}
// 拷贝资源,++拷贝的计数
_ptr = sp._ptr;
_count = sp._count;
++(*_count);
}
return *this;
}
// 析构时释放资源
~Shared_Ptr()
{
// 当引用计数减到 0,就释放资源
if (--(*_count) == 0)
{
cout << "释放资源\n";
delete _ptr;
delete _count;
}
else
{
// 打印调试
cout << "减减引用计数,当前引用计数为: " << *_count << endl;
}
}
private:
T* _ptr; // 指针
int* _count; // 引用计数
};
上述代码中存在线程安全问题,引用计数需要加锁保护!
多线程测试代码 测试记得把打印的调试信息注释掉
// 测试线程安全:拷贝 n 个对象
// 测试记得把打印的调试信息注释掉
void ThreadRoute(Shared_Ptr<int>& sp, int n, mutex& mtx)
{
for (int i = 0; i < n; ++i)
{
Shared_Ptr<int> test(sp);
}
}
void TestSharedThreadSafe()
{
Shared_Ptr<int> sp(new int(1));
mutex mtx;
int n = 10000;
// 因为不清楚内部实现,多线程的引用要使用库函数 ref
thread t1(ThreadRoute, ref(sp), n, ref(mtx));
thread t2(ThreadRoute, ref(sp), n, ref(mtx));
t1.join();
t2.join();
}
多线程版 shared_ptr 实现,在修改引用计数时,加锁保护
// 多线程
template<class T>
class Shared_Ptr
{
public:
// 引用计数初始为 1
Shared_Ptr(T* ptr)
:_ptr(ptr)
, _count(new int(1))
, _pmtx(new mutex)
{}
// 像指针一样使用,重载 * 和 ->
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
void AddCount()
{
// 对公共资源 引用计数加加,加锁保护
unique_lock<mutex> lock(*_pmtx);
++(*_count);
}
void DelCount()
{
// 对公共资源 引用计数减减,加锁保护
unique_lock<mutex> lock(*_pmtx);
--(*_count);
}
// 返回管理的指针
T* Get()
{
return _ptr;
}
// 返回引用计数
int use_count()
{
return *_count;
}
// 拷贝构造
Shared_Ptr(const Shared_Ptr<T>& sp)
{
// 将资源拷贝过来,并 ++引用计数
_ptr = sp._ptr;
_count = sp._count;
_pmtx = sp._pmtx;
// 将锁拿到后再 用锁保护,++引用计数
AddCount();
}
Shared_Ptr<T>& operator=(const Shared_Ptr<T>& sp)
{
// 防自己给自己赋值
if (sp._ptr != _ptr)
{
// 释放原本资源
release();
// 拷贝资源,++拷贝的计数
_pmtx = sp._pmtx;
_ptr = sp._ptr;
_count = sp._count;
AddCount();
}
return *this;
}
// 释放资源
void release()
{
unique_lock<mutex> lock(*_pmtx);
// 当引用计数减到 0,就释放资源
if (--(*_count) == 0)
{
// cout << "释放资源\n";
// 释放锁之前 解锁
lock.unlock();
delete _ptr;
delete _count;
delete _pmtx;
}
else
{
// 打印调试
// cout << "减减引用计数,当前引用计数为: " << *_count << endl;
}
}
// 析构时释放资源
~Shared_Ptr()
{
release();
}
private:
T* _ptr; // 指针
int* _count; // 引用计数
mutex* _pmtx; // 锁
};
三、shared_ptr 的循环引用问题
当存在类里面有智能指针互相指向时,就会出现循环引用问题。
因此,官方给 shared_ptr 配了一个小弟:weak_ptr
weak_ptr 不是常规的智能指针,它具有以下特点
-
它不支持 RAII
-
支持像指针一样使用
-
专门设计出来解决循环引用问题
核心:weak_ptr 支持用 shared_ptr 构造,它不会加加引用计数。
测试循环引用的代码:
struct ListNode
{
// 双向链表
Shared_Ptr<ListNode> _prev;
Shared_Ptr<ListNode> _next;
// 析构
~ListNode()
{
cout << "释放节点\n";
}
};
void CirculaReferenceProblem()
{
Shared_Ptr<ListNode> n1(new ListNode);
Shared_Ptr<ListNode> n2(new ListNode);
n1->_next = n2;
n2->_prev = n1;
}
weak_ptr 的简单实现
template<class T>
class Weak_Ptr
{
public:
Weak_Ptr()
:_ptr(nullptr)
{}
Weak_Ptr(const Shared_Ptr<T>& sp)
:_ptr(sp.Get())
{}
Weak_Ptr<T>& operator=(const Shared_Ptr<T>& sp)
{
_ptr = sp.Get();
return *this;
}
// 像指针一样使用,重载 * 和 ->
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
解决方案:在内部的互相引用处,用 weak_ptr 就可以了
struct ListNode
{
// 在内部的互相引用处,用 weak_ptr 就可以了
Weak_Ptr<ListNode> _prev;
Weak_Ptr<ListNode> _next;
~ListNode()
{
cout << "释放节点\n";
}
};
void CirculaReferenceProblem()
{
Shared_Ptr<ListNode> n1(new ListNode);
Shared_Ptr<ListNode> n2(new ListNode);
n1->_next = n2;
n2->_prev = n1;
}
四、定制删除器
有的时候我们使用 new [] 开辟空间或传入的是文件指针,就可以定制删除器来指定使用 delete [] 或fclose() 删除。
定制删除器就是传入一个可调用对象(仿函数或lambda或函数指针),在释放时调用。
改变:
1、成员加一个 function 包装的删除器,构造函数添加删除器模版
2、release() 中删除改为用定制删除器删除
添加定制删除器
template<class T>
class Shared_Ptr
{
public:
Shared_Ptr()
:_ptr(nullptr)
, _count(new int(1))
, _pmtx(new mutex)
{}
// 引用计数初始为 1
Shared_Ptr(T* ptr)
:_ptr(ptr)
, _count(new int(1))
, _pmtx(new mutex)
{}
// 定制删除器
template<class D>
Shared_Ptr(T* ptr, D del)
:_ptr(ptr)
, _count(new int(1))
, _pmtx(new mutex)
, _del(del)
{}
// 像指针一样使用,重载 * 和 ->
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
void AddCount()
{
// 对公共资源 引用计数加加,加锁保护
unique_lock<mutex> lock(*_pmtx);
++(*_count);
}
void DelCount()
{
// 对公共资源 引用计数减减,加锁保护
unique_lock<mutex> lock(*_pmtx);
--(*_count);
}
// 返回管理的指针
T* Get() const
{
return _ptr;
}
// 返回引用计数
int use_count()
{
return *_count;
}
// 拷贝构造
Shared_Ptr(const Shared_Ptr<T>& sp)
{
// 将资源拷贝过来,并 ++引用计数
_ptr = sp._ptr;
_count = sp._count;
_pmtx = sp._pmtx;
// 将锁拿到后再 用锁保护,++引用计数
AddCount();
}
Shared_Ptr<T>& operator=(const Shared_Ptr<T>& sp)
{
// 防自己给自己赋值
if (sp._ptr != _ptr)
{
// 释放原本资源
release();
// 拷贝资源,++拷贝的计数
_pmtx = sp._pmtx;
_ptr = sp._ptr;
_count = sp._count;
AddCount();
}
return *this;
}
// 释放资源
void release()
{
unique_lock<mutex> lock(*_pmtx);
// 当引用计数减到 0,就释放资源
if (--(*_count) == 0)
{
// cout << "释放资源\n";
// 释放锁之前 解锁
lock.unlock();
// delete _ptr;
// 改为用定制删除器删除
_del(_ptr);
delete _count;
delete _pmtx;
}
else
{
// 打印调试
// cout << "减减引用计数,当前引用计数为: " << *_count << endl;
}
}
// 析构时释放资源
~Shared_Ptr()
{
release();
}
private:
T* _ptr; // 指针
int* _count; // 引用计数
mutex* _pmtx; // 锁
function<void(T*)> _del = [](T* ptr) {
cout << "默认 delete\n";
delete ptr;
};
};
测试代码
template<class T>
struct DeleteArr
{
void operator()(T* ptr)
{
cout << "delete[] ptr";
delete[] ptr;
}
};
void TestDeletor()
{
// 如果不传定制删除器,运行会报错
Shared_Ptr<ListNode> sp(new ListNode[10], DeleteArr<ListNode>());
}
到此结束,感谢大家观看♪(・ω・)ノ