文章目录
- 一、为什么需要智能指针?
- 二、智能指针的使用及原理
- 1. RAII
- 2.智能指针的原理
- 3. auto_ptr
- 4. unique_ptr
- 5. shared_ptr
- 6. weak_ptr
- 7.删除器
一、为什么需要智能指针?
如果在 div() 输入的 b == 0,那么就会抛出一个异常,被 main() 捕获,但是在 Func() 中 new 申请的资源就会因没释放而发生泄露问题,这是一种异常安全问题。
#include <iostream>
using namespace std;
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
int* p = new int;
cout << div() << endl; // 异常安全问题
cout << "delete:" << p << endl;
delete p;
}
int main()
{
try
{
Func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
那么为了处理这里的异常安全问题,可以在 Func() 中捕获异常然后释放资源,再重新抛出,但这种方式并没有从根源上解决问题。因为 new 可能存在多个并且也有可能抛异常,那么在这种情况下,就很难判断是谁抛的异常。所以,当多个可能会抛异常的地方交织在一起的时候,这种捕获再重新抛出的方式会让处理者处理得焦头烂额。
因此,C++ 引入了智能指针。
二、智能指针的使用及原理
1. RAII
RAII(Resource Acquisition Is Initialization,资源获取就是初始化)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构时释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:一是不需要显式地释放资源。二是采用这种方式,对象所需的资源在其生命周期内始终保持有效。
2.智能指针的原理
总结一下智能指针的原理:
① RAII 特性。
② 重载 operator* 和 opertaor-> ,具有像指针一样的行为。
下面是我们简单设计的智能指针:
//RAII
//用起来像指针一样
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)
:_ptr(ptr)
{}
~SmartPtr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl; //测试用
delete _ptr;
}
}
//像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(new int);
SmartPtr<int> sp3(new int);
*sp1 = 10;
cout << *sp1 << endl;
(*sp1)++;
(*sp1)++;
cout << *sp1 << endl;
cout << div() << endl;
}
int main()
{
try
{
Func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
但是我们设计的智能指针有一个问题,就是拷贝问题。
我们没有实现它的拷贝构造函数,所以是浅拷贝。两个对象最后销毁时各调用了一次析构函数,结果就是对同一块空间释放了两次,导致程序崩溃。
int main()
{
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(sp1);
return 0;
}
那么该如何解决拷贝问题呢?
3. auto_ptr
为了处理这个问题,C++98 中智能指针 auto_ptr 的解决方案是管理权转移。
既然两个对象指向同一块空间最后会析构两次,如果永远只有一个对象指向一块空间,那么就不会出现上述问题了。
所以当一个对象拷贝构造另一个对象时,先进行值拷贝,然后把原对象置空,这就实现了管理权的转移。
// C++98 管理权转移 auto_ptr
template<class T>
class auto_ptr //我们简化模拟的auto_ptr
{
public:
auto_ptr(T* ptr = nullptr)
:_ptr(ptr)
{}
//拷贝构造函数
auto_ptr(auto_ptr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr; //管理权转移
}
//赋值重载函数
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
if (this != &ap)
{
if (_ptr) // 释放当前对象的资源
{
delete _ptr;
}
_ptr = ap._ptr; // 转移资源到当前对象中
ap._ptr = nullptr;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl; //测试用
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
int main()
{
auto_ptr<int> ap1(new int);
auto_ptr<int> ap2(ap1); //管理权转移
return 0;
}
虽然 auto_ptr 这样设计解决了拷贝问题,但同时也出现了一个很大的问题,就是原对象悬空了。如果不小心访问了它,就会出现访问空指针问题,导致程序崩溃。
int main()
{
auto_ptr<int> ap1(new int);
auto_ptr<int> ap2(ap1); //管理权转移
//ap1悬空
*ap2 = 10;
cout << *ap2 << endl;
cout << *ap1 << endl; //不小心访问了ap1
return 0;
}
结论:auto_ptr 是一个失败设计,很多公司明确要求不能使用 auto_ptr 。
4. unique_ptr
unique_ptr 是防拷贝和防赋值的智能指针。
// C++11库才更新智能指针实现
// C++11出来之前,boost -> scoped_ptr/shared_ptr/weak_ptr
// C++11将boost库中智能指针精华部分吸收了过来
// C++11 -> unique_ptr/shared_ptr/weak_ptr
// unique_ptr/scoped_ptr
// 原理:简单粗暴 -- 防拷贝
template<class T>
class unique_ptr //我们简化模拟的unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl; //测试用
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
private:
T* _ptr;
};
int main()
{
unique_ptr<int> up1(new int);
/*unique_ptr<int> up2(up1);*/ // 会编译报错,因为防拷贝
return 0;
}
5. shared_ptr
在某些场景下,需要用到支持拷贝的智能指针。
shared_ptr 的原理:引用计数,支持拷贝。
引用计数支持多个拷贝管理同一个资源,最后一个析构对象释放资源。
- shared_ptr 在其内部,给每份资源都维护了一份引用计数,用来记录该份资源被几个对象共享。
- 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,该资源的引用计数减一。如果引用计数是 0 ,就说明自己是最后一个使用该资源的对象,必须释放该资源;如果不是 0 ,就说明除了自己还有其他对象在使用该份资源,不能释放该资源。
- 引用计数有线程安全问题,是智能指针本身需要处理的,所以需要使用互斥锁来进行维护。
template<class T>
class shared_ptr //我们简化模拟的shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pRefCount(new int(1))
, _pmtx(new mutex)
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pRefCount(sp._pRefCount)
,_pmtx(sp._pmtx)
{
AddRef(); //引用计数+1
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr) //比较内部的指针才能真正避免自己给自己赋值
{
Release(); //释放资源
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
_pmtx = sp._pmtx;
AddRef(); //引用计数+1
}
return *this;
}
int use_count()
{
return *_pRefCount;
}
~shared_ptr()
{
Release();
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get() const
{
return _ptr;
}
private:
void Release() //释放资源
{
_pmtx->lock(); //保证引用计数的安全性
bool flag = false; //flag是局部变量,用于判断是否释放锁
if (--(*_pRefCount) == 0 && _ptr) //引用计数-1,并判断是否是0
{
cout << "delete:" << _ptr << endl; //测试用
delete _ptr;
delete _pRefCount;
flag = true;
}
_pmtx->unlock();
if (flag == true)
{
delete _pmtx; //mutex是new来的,最后需要delete,只能在解锁后delete
}
}
void AddRef() //引用计数+1
{
_pmtx->lock(); //保证引用计数的安全性
++(*_pRefCount);
_pmtx->unlock();
}
private:
T* _ptr; //指向资源
int* _pRefCount; //指向引用计数
mutex* _pmtx; //指向互斥锁,用于维护引用计数的安全性
};
测试代码1:
int main()
{
shared_ptr<int> sp1(new int);
shared_ptr<int> sp2(sp1);
shared_ptr<int> sp3(sp1);
shared_ptr<int> sp4(new int);
shared_ptr<int> sp5(sp4);
//sp1和sp2指向同一份资源
sp1 = sp1; //自己给自己赋值
sp1 = sp2; //自己给自己赋值
sp1 = sp4;
sp2 = sp4;
sp3 = sp4;
*sp1 = 2;
*sp2 = 3;
return 0;
}
测试代码2:
struct Date
{
int _year = 0;
int _month = 0;
int _day = 0;
};
// shared_ptr智能指针内部引用计数的加减是加锁保护的,所以是线程安全的
// 但是指向的资源不是线程安全的
// 指向堆上资源的线程安全问题是访问的人处理的,智能指针不管,也管不了
void SharePtrFunc(shared_ptr<Date>& sp, size_t n, mutex& mtx)
{
cout << sp.get() << endl;
for (size_t i = 0; i < n; ++i)
{
//内部引用计数的加减是线程安全的
shared_ptr<Date> copy(sp);
//指向的资源不是线程安全的,需要自行加锁保护
//只有这部分需要锁,后面的部分不需要锁
//因此,我们可以把这部分括起来,特地弄成一个局部域
//这样的话,对象出了作用域就会销毁,自动解锁
{
unique_lock<mutex> lk(mtx);
copy->_year++;
copy->_month++;
copy->_day++;
}
// ...
}
}
int main()
{
shared_ptr<Date> sp(new Date);
cout << sp.get() << endl;
const size_t n = 100000;
mutex mtx;
thread t1(SharePtrFunc, std::ref(sp), n, std::ref(mtx));
thread t2(SharePtrFunc, std::ref(sp), n, std::ref(mtx));
t1.join();
t2.join();
cout << sp->_year << endl;
cout << sp->_month << endl;
cout << sp->_day << endl;
cout << sp.use_count() << endl;
return 0;
}
但是 shared_ptr 在某种场景下会出现循环引用的问题。
比如下面的代码:
struct ListNode
{
int _val;
std::shared_ptr<ListNode> _prev;
std::shared_ptr<ListNode> _next;
~ListNode()
{
cout << "~ListNode" << endl;
}
};
int main()
{
std::shared_ptr<ListNode> n1(new ListNode);
std::shared_ptr<ListNode> n2(new ListNode);
cout << n1.use_count() << endl; // 1
cout << n2.use_count() << endl; // 1
// 循环引用
n1->_next = n2;
n2->_prev = n1;
cout << n1.use_count() << endl; // 2
cout << n2.use_count() << endl; // 2
return 0;
}
我们的预期结果是,最终两个节点都会被释放,即会调用两次 ListNode 的析构函数。
但实际的运行结果是,两个节点都没有被释放,即 ListNode 的析构函数一次都没有调用。
循环引用分析:
- n1 和 n2 两个智能指针对象各指向一个节点,引用计数都是 1 。
- 经过赋值后,n1 的 _next 指向 n2 所指向的节点,n2 的 _prev 指向 n1 所指向的节点,引用计数都变成 2 。
- 最后 n2 和 n1 先后析构,引用计数都减到 1 。此时 _next 指向下一个节点,_prev 指向上一个节点,于是就形成了这样的局面:两个节点最终都不会释放,造成内存泄漏的问题。
- 换言之,节点 2 的释放取决于 _next 的析构,_next 的析构取决于节点 1 的释放,节点 1 的释放取决于 _prev 的析构,_prev 的析构取决于节点 2 的释放,所以就形成了一个永远解不开的环,这就是循环引用。
shared_ptr 很好,但就是有循环引用的问题,设计者也没有很好的办法,于是之后又设计了专门应对这种情况的 weak_ptr 。
6. weak_ptr
weak_ptr 不是常规意义的智能指针,它没有一个接收原生指针的构造函数,也不符合 RAII 。
weak_ptr 是为了配合 shared_ptr 而引入的一种智能指针,它可以从一个 shared_ptr 或另一个 weak_ptr 对象来构造,它的构造和析构不会引起 shared_ptr 引用记数的增加或减少(不参与资源的释放管理),所以它可以解决 shared_ptr 循环引用的问题。
构造函数:
它的
use_count()
返回的是 shared_ptr 的引用计数。
template<class T>
class weak_ptr //我们简化模拟的weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
weak_ptr(const weak_ptr<T>& wp)
:_ptr(wp._ptr)
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
weak_ptr<T>& operator=(const weak_ptr<T>& wp)
{
_ptr = wp._ptr;
return *this;
}
private:
T* _ptr;
};
weak_ptr 解决了循环引用的问题:
struct ListNode
{
int _val;
//std::shared_ptr<ListNode> _prev;
//std::shared_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;
std::weak_ptr<ListNode> _next;
~ListNode()
{
cout << "~ListNode" << endl;
}
};
int main()
{
std::shared_ptr<ListNode> n1(new ListNode);
std::shared_ptr<ListNode> n2(new ListNode);
cout << n1.use_count() << endl; // 1
cout << n2.use_count() << endl; // 1
n1->_next = n2;
n2->_prev = n1;
cout << n1.use_count() << endl; // 1
cout << n2.use_count() << endl; // 1
return 0;
}
运行结果是,最终两个节点都会被释放,即调用了两次 ListNode 的析构函数。
7.删除器
new 和 delete 需要匹配使用:new 和 delete 、new[ ] 和 delete[ ] 。否则可能会报错。
如果我们使用了 new[ ] ,但最后使用 delete 而非 delete[ ] ,并且要释放资源对象的内部实现了析构函数,那么一定会运行出错。
智能指针里面有删除器,删除器是一个可调用对象。
智能指针默认使用默认删除器,而默认删除器使用的是 delete ,当我们使用 new[ ] 时,就会运行出错。
class A
{
public:
~A()
{
cout << "~A()" << endl;
}
private:
int _a1 = 0;
int _a2 = 0;
};
int main()
{
std::unique_ptr<A> up1(new A); // 没有问题
std::unique_ptr<A> up2(new A[10]); // 有问题
return 0;
}
但是我们申请的资源有可能不是 new 出来的,比如:new[ ]、malloc、fopen 。
那么该如何解决这种情况呢?这就需要我们定制删除器来解决了。
//定制删除器
template<class T>
struct DeleteArray
{
void operator()(const T* ptr)
{
cout << "delete[]:" << ptr << endl; // 测试用
delete[] ptr;
}
};
//定制删除器
struct DeleteFile
{
void operator()(FILE* ptr)
{
cout << "fclose:" << ptr << endl; // 测试用
fclose(ptr);
}
};
int main()
{
//删除器在类模板参数给 -- 类型
std::unique_ptr<A> up1(new A);
std::unique_ptr<A, DeleteArray<A>> up2(new A[10]);
std::unique_ptr<FILE, DeleteFile> up3(fopen("test.txt", "w"));
//删除器在构造函数的参数给 -- 对象
std::shared_ptr<A> sp1(new A);
std::shared_ptr<A> sp2(new A[10], DeleteArray<A>());
std::shared_ptr<FILE> sp3(fopen("test.txt", "w"), DeleteFile());
std::shared_ptr<A> sp4(new A[10], [](A* p) {delete[] p; });
std::shared_ptr<FILE> sp5(fopen("test.txt", "w"), [](FILE* p) {fclose(p); });
return 0;
}
我们用 unique_ptr 来简单模拟一下:
//定制删除器
template<class T>
struct DeleteArray
{
void operator()(const T* ptr)
{
cout << "delete[]:" << ptr << endl; // 测试用
delete[] ptr;
}
};
//定制删除器
struct DeleteFile
{
void operator()(FILE* ptr)
{
cout << "fclose:" << ptr << endl; // 测试用
fclose(ptr);
}
};
namespace MyLib
{
template<class T>
class default_delete
{
public:
void operator()(const T* ptr)
{
cout << "delete:" << ptr << endl; // 测试用
delete ptr;
}
};
//释放方式由D删除器决定
template<class T, class D = default_delete<T>>
class unique_ptr //我们简化模拟的unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
//cout << "delete:" << _ptr << endl; //测试用
//delete _ptr;
D del;
del(_ptr);
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
private:
T* _ptr;
};
}
int main()
{
MyLib::unique_ptr<A> up1(new A);
MyLib::unique_ptr<A, DeleteArray<A>> up2(new A[10]);
MyLib::unique_ptr<FILE, DeleteFile> up3(fopen("test.txt", "w"));
return 0;
}