文章目录
一、引入
double div()
{
double a, b;
cin >> a >> b;
if (b == 0)
{
throw "除0错误";
}
return a / b;
}
void fun()
{
int* p = new int[10];
cout << div() << endl;
delete[]p;
}
int main()
{
try
{
fun();
}
catch (const char* errstr)
{
cout << errstr << endl;
}
catch (...)
{
cout << "未知错误" << endl;
}
return 0;
}
在上一章【C++】异常中为了这种会导致内存泄漏的情况,我们的办法是在fun函数内在try+catch重复throw出异常。
void fun()
{
int* p = new int[10];
try
{
cout << div() << endl;
}
catch (...)
{
delete[]p;
cout << "delete[]p 1" << endl;
throw "除0错误";
}
cout << "delete[]p 2" << endl;
delete[]p;
}
但是如果有两个空间需要释放:
void fun()
{
int* p1 = new int[10];
int* p2 = new int[15];
try
{
cout << div() << endl;
}
catch (...)
{
delete[]p1;
delete[]p2;
throw "除0错误";
}
delete[]p1;
delete[]p2;
}
我们要知道new也是会抛异常的,如果p1抛出了异常,那么p2就没有被释放掉,造成内存泄漏。
而智能指针就能很好的解决这个问题。
二、智能指针
2.1 智能指针保存与释放资源RAII
template <class T>
class SmartPtr
{
public:
// 保存资源
SmartPtr(T* ptr)
: _ptr(ptr)
{}
// 释放资源
~SmartPtr()
{
delete[]_ptr;
cout << _ptr << endl;
}
private:
T* _ptr;
};
有了这个以后不管谁抛异常我们就不需要再考虑资源回收的问题了:
void fun()
{
int* p1 = new int[10];
SmartPtr<int> sp1(p1);
SmartPtr<int> sp2(new int[15]);
cout << div() << endl;
}
当我们把堆上的资源交给智能指针后,出了作用域后就会直接释放,就算抛了异常也会出作用域后销毁。
而智能指针的构造函数相当于保存资源、析构函数相当于释放资源。
RAII:
RAII是一种利用对象生命周期来控制程序资源的资源。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象
把资源的声明周期和对象的生命周期绑定到一起。
这样就有两点好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效。
而智能指针既然是指针就要像指针一样使用,还要有其他的操作。
2.2 智能指针的其他操作
template <class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
: _ptr(ptr)
{}
~SmartPtr()
{
delete[]_ptr;
cout << _ptr << endl;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t pos)
{
return _ptr[pos];
}
private:
T* _ptr;
};
这样我们就可以像一个指针一样使用它。
1.3 智能指针拷贝问题
原生指针的拷贝就是指向同一块空间,但是原生指针销毁的时候不会清理资源,而智能指针就会对同一块空间析构两次。
先来看看库中的智能指针怎么解决这个问题的。
1.4 auto_ptr管理权转移
int main()
{
std::auto_ptr<int> ptr1(new int);
std::auto_ptr<int> ptr2(ptr1);
return 0;
}
????????????
可以看到auto_ptr使用拷贝构造就是把原来的资源转移走。
这样就有可能会造成空指针访问。
1.5 unique_ptr防拷贝
unique_ptr的方法简单粗暴:直接不让拷贝。
int main()
{
std::unique_ptr<int> ptr1(new int);
std::unique_ptr<int> ptr2(ptr1);
return 0;
}
1.6 shared_ptr引用计数❗️❗️
当两个指针同时指向一块空间时,就增加一个引用计数。
1.6.1 引用计数的实现
在开辟空间的时候在堆上申请一块空间,指针同时指向资源和堆上的空间,堆上的空间就可以用来计数。
// SmartPtr.h
template <class T>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
, _pcnt(new int(1))
{}
SmartPtr(const SmartPtr<T>& sp)
: _ptr(sp._ptr)
, _pcnt(sp._pcnt)
{
(*_pcnt)++;
}
~SmartPtr()
{
(*_pcnt)--;
if (*_pcnt == 0)
{
delete _ptr;
delete _pcnt;
cout << _ptr << endl;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t pos)
{
return _ptr[pos];
}
private:
T* _ptr;
int* _pcnt;
};
// SmartpPtr.cpp
int main()
{
SmartPtr<int> ptr1(new int);
SmartPtr<int> ptr2(ptr1);
return 0;
}
1.6.2 赋值问题
这里首先要解决的是自己给自己赋值的问题,不能直接this != &sp
,因为有可能是不一样的智能指针指向同一块资源空间。
正确写法:if (_ptr != sp->_ptr)
其次就要解决释放左边的空间,这里注意不能直接释放,而是要看引用计数是否为0,为0才能释放。
SmartPtr<T>& operator=(const SmartPtr<T>& sp)
{
if (_ptr != sp._ptr)
{
(*_pcnt)--;
if (*_pcnt == 0)
{
this->~SmartPtr();
//delete _pcnt;
//delete _ptr;
}
_ptr = sp._ptr;
_pcnt = sp._pcnt;
(*_pcnt)++;
}
return *this;
}
1.6.3 多线程拷贝问题
假设我们现在是多线程的情况要对一个智能指针进行拷贝。
我们先在智能指针类放一个能获取引用计数的成员函数:
// 获取引用计数值
int use_count()
{
return *_pcnt;
}
然后多线程开始不停的用临时的智能指针进行拷贝:
void test()
{
const int N = 100000;
SmartPtr<int> sp1(new int[10]);
std::thread t1([&]() {
for (int i = 0; i < N; i++)
{
SmartPtr<int> sp2(sp1);
}
});
std::thread t2([&]() {
for (int i = 0; i < N; i++)
{
SmartPtr<int> sp3(sp1);
}
});
t1.join();
t2.join();
cout << sp1.use_count() << endl;
}
按道理来说t1和t2线程内部定义的智能指针都是临时对象,拷贝完就会销毁,按道理说最后输出应该为1。但是我们来看看结果:
不仅如此有时候程序还会崩溃。
原因:
我们知道
++
和--
都不是原子性的,假设现在引用计数是1,本来应该是线程1和线程2都会把引用计数++,结果变成3,但是它们同时++,导致结果变成了2,然后线程1和线程2临时对象销毁,引用计数都要–,结果导致引用计数变成0,销毁资源,造成野指针。
为了解决这种情况我们就可以进行加锁,让线程串行访问。
所以我们要加一个成员变量(锁)。而因为要同时保护引用计数++
和--
所以必须是同一把锁。
而因为锁是防拷贝的,所以我们要使用指针。
所有引用计数改变的地方都要保护起来。
template <class T>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
, _pcnt(new int(1))
, _pmutex(new std::mutex)
{}
SmartPtr(const SmartPtr<T>& sp)
: _ptr(sp._ptr)
, _pcnt(sp._pcnt)
, _pmutex(sp._pmutex)
{
_pmutex->lock();
(*_pcnt)++;
_pmutex->unlock();
}
~SmartPtr()
{
_pmutex->lock();
(*_pcnt)--;
_pmutex->unlock();
if (*_pcnt == 0)
{
delete _ptr;
delete _pcnt;
delete _pmutex;
//cout << _ptr << endl;
}
}
SmartPtr<T>& operator=(const SmartPtr<T>& sp)
{
if (_ptr != sp._ptr)
{
_pmutex->lock();
(*_pcnt)--;
_pmutex->unlock();
if (*_pcnt == 0)
{
this->~SmartPtr();
//delete _pcnt;
//delete _ptr;
}
_ptr = sp._ptr;
_pcnt = sp._pcnt;
_pmutex = sp._pmutex;
_pmutex->lock();
(*_pcnt)++;
_pmutex->unlock();
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t pos)
{
return _ptr[pos];
}
// 获取引用计数值
int use_count()
{
return *_pcnt;
}
private:
T* _ptr;
int* _pcnt;
std::mutex* _pmutex;
};
void test()
{
const int N = 100000;
SmartPtr<int> sp1(new int[10]);
std::thread t1([&]() {
for (int i = 0; i < N; i++)
{
SmartPtr<int> sp2 = sp1;
}
});
std::thread t2([&]() {
for (int i = 0; i < N; i++)
{
SmartPtr<int> sp3 = sp1;
}
});
t1.join();
t2.join();
cout << sp1.use_count() << endl;
}
多次测试结果全部为1。
这里要注意的是share_ptr本身是线程安全的,但是它指向的资源不一定是线程安全的。
1.6.4 循环引用问题❗️❗️
现在我们写一个链表链接的代码:
struct ListNode
{
int val;
ListNode* left;
ListNode* right;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void test()
{
ListNode* n1 = new ListNode;
ListNode* n2 = new ListNode;
n1->right = n2;
n2->left = n1;
delete n1;
delete n2;
}
现在我们不想自己delete资源,可以考虑使用智能指针。
而智能指针不能赋值给自定义指针,所以我们要改变指针的类型。
struct ListNode
{
int val;
SmartPtr<ListNode> left;
SmartPtr<ListNode> right;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void test()
{
SmartPtr<ListNode> n1 = new ListNode;
SmartPtr<ListNode> n2 = new ListNode;
n1->right = n2;
n2->left = n1;
}
但是我们看到结果并没有释放掉资源,这是怎么回事呢?
对于n1资源,有n1指向和n2的left指向,所以引用计数为2,n2资源同理。当n1和n2出了作用域,两个的引用计数都变成1。
但此时right和left是随着对象的销毁才能销毁,但是对象想要销毁,引用计数就要减为0,引用计数减为0,就要指针销毁,这样就成了个死循环。这里要注意的是如果只链接了一个就不会有这种问题。
而为了解决这个问题,我们引入了weak_ptr
。
1.7 weak_ptr不管资源
这里的weak_ptr没有引用计数,不支持RAII,只指向资源,不管理资源。
struct ListNode
{
int val;
std::weak_ptr<ListNode> left;
std::weak_ptr<ListNode> right;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void test()
{
std::shared_ptr<ListNode> n1(new ListNode);
std::shared_ptr<ListNode> n2(new ListNode);
n1->right = n2;
n2->left = n1;
}
1.7.1 weak_ptr简单实现
namespace yyh
{
template <class T>
class weak_ptr
{
public:
weak_ptr()
: _ptr(nullptr)
{}
weak_ptr(const SmartPtr<T>& p)
: _ptr(p.get())
{}
weak_ptr<T>& operator=(const SmartPtr<T>& p)
{
_ptr = p.get();
return *this;
}
private:
T* _ptr;
};
}
struct ListNode
{
int val;
yyh::weak_ptr<ListNode> left;
yyh::weak_ptr<ListNode> right;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void test()
{
SmartPtr<ListNode> n1(new ListNode);
SmartPtr<ListNode> n2(new ListNode);
n1->right = n2;
n2->left = n1;
}
三、定制删除器
普通的内置类型确实可以让智能指针自动释放,但是如果不是内置类型呢?
void test()
{
std::shared_ptr<std::string> n(new std::string[10]);
}
这样直接会导致程序崩溃,因为delete类型不匹配([]
)。
这里的del就是定制删除器,定制删除器就是一个可调用对象。
template <class D>
class Delete
{
public:
void operator()(const D* del)
{
delete[]del;
cout << "delete[]del" << endl;
}
};
void test()
{
std::shared_ptr<std::string> n(new std::string[10],
Delete<std::string>());
}
3.1 模拟实现定制删除器
这里我们不能像库里那样在构造的时候把参数传进去,因为要删除是在析构函数中,无法从构造函数传递到析构函数。所以我们可以给整个类增加一个模板参数,增加一个新的成员变量。
而如果要增加一个模板参数,我们为了让前面的代码运行,所以增加一个默认的删除器。
template <class T>
class DefultDelete// 默认
{
public:
void operator()(T* ptr)
{
delete ptr;
}
};
template <class D>
class Delete
{
public:
void operator()(const D* del)
{
delete[]del;
cout << "delete[]del" << endl;
}
};
template <class T, class D = DefultDelete<T>>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
, _pcnt(new int(1))
, _pmutex(new std::mutex)
{}
SmartPtr(const SmartPtr<T>& sp)
: _ptr(sp._ptr)
, _pcnt(sp._pcnt)
, _pmutex(sp._pmutex)
{
_pmutex->lock();
(*_pcnt)++;
_pmutex->unlock();
}
~SmartPtr()
{
_pmutex->lock();
(*_pcnt)--;
_pmutex->unlock();
if (*_pcnt == 0)
{
//delete _ptr;
_del(_ptr);
delete _pcnt;
delete _pmutex;
//cout << _ptr << endl;
}
}
SmartPtr<T>& operator=(const SmartPtr<T>& sp)
{
if (_ptr != sp._ptr)
{
_pmutex->lock();
(*_pcnt)--;
_pmutex->unlock();
if (*_pcnt == 0)
{
this->~SmartPtr();
//delete _pcnt;
//delete _ptr;
}
_ptr = sp._ptr;
_pcnt = sp._pcnt;
_pmutex = sp._pmutex;
_pmutex->lock();
(*_pcnt)++;
_pmutex->unlock();
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t pos)
{
return _ptr[pos];
}
// 获取引用计数值
int use_count()
{
return *_pcnt;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _pcnt;
std::mutex* _pmutex;
D _del;
};
namespace yyh
{
template <class T>
class weak_ptr
{
public:
weak_ptr()
: _ptr(nullptr)
{}
weak_ptr(const SmartPtr<T>& p)
: _ptr(p.get())
{}
weak_ptr<T>& operator=(const SmartPtr<T>& p)
{
_ptr = p.get();
return *this;
}
private:
T* _ptr;
};
}
struct ListNode
{
int val;
yyh::weak_ptr<ListNode> left;
yyh::weak_ptr<ListNode> right;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void test()
{
SmartPtr<ListNode> n1(new ListNode);
SmartPtr<ListNode, Delete<ListNode>> n2(new ListNode[5]);
}