C++的RAII思想以及在智能指针上的应用
文章目录
- 何为RAII?
- 内存泄漏的场景
- SmartPtr
- C++98的失败设计:auto_ptr
- unique_ptr
- shared_ptr
- 循环引用
- weak_ptr
- 定制删除器
何为RAII?
RAII的百度百科介绍
一般情况下,C++申请资源后都需要手动释放资源,一旦忘记资源的释放就会造成内存泄漏,为了解决内存泄漏问题,C++引入了RAII机制。
RAII是一种利用对象的声明周期来控制资源释放的技术。比如一个局部对象,出了作用域就被销毁,RAII利用这一特性将资源与对象绑定在一起,当局部对象释放时,绑定在其身上的资源也要被释放。
内存泄漏的场景
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void func()
{
int* p1 = new int[10];
int* p2 = new int[10];
int* p3 = new int[10];
int* p4 = new int[10];
try
{
div();
}
catch (...)
{
delete[] p1;
delete[] p2;
delete[] p3;
delete[] p4;
throw;
}
delete[] p1;
delete[] p2;
delete[] p3;
delete[] p4;
}
int main()
{
try
{
func();
}
catch (const exception& e)
{
cout << e.what() << endl;
// ...
}
return 0;
}
main函数调用func函数并捕获其中的异常,func函数申请资源后调用div函数并捕获其中的异常,div函数可能抛出异常。当资源被申请后,调用div函数时发生了异常,func函数就能捕获其异常,并释放之前申请的资源。
但程序还有问题,如果func函数只申请一次资源,申请失败时,main函数捕获申请失败的异常,这没有问题,但上面的func函数申请了四次资源,假如前三次申请资源成功,第四次申请资源失败,异常被抛出并被main函数捕获,但前三次申请的资源没有被释放,造成了资源泄漏。总不能在main函数中释放资源,p1到p4都是局部对象,出了作用域就销毁了,main函数想释放资源也不知道资源在哪。而使用RAII的思想就能解决这样的问题。
当申请资源时,就构造一个对象,在对象的声明周期内,资源能被正常使用,对象析构时,资源也随之释放。
SmartPtr
SmartPtr类的定义
template <class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr() { cout << "delete:" << _ptr << endl; delete _ptr; _ptr = nullptr; }
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};
SmartPtr类管理一个指针,使用SmartPtr类对象时可以像指针一样使用,支持*,->操作,特殊的是SmartPtr类对象在析构时会将管理的指针上的空间释放,也就是申请资源的生命周期和SmartPtr类对象的生命周期一样,对象生命结束,资源的生命也结束。
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void func()
{
SmartPtr<int> p1(new int);
SmartPtr<int> p2(new int);
SmartPtr<int> p3(new int);
SmartPtr<int> p4(new int);
}
int main()
{
try
{
func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
尽管没有显式地写出delete释放申请的资源,但SmartPtr在析构时顺便清理了资源。
总结一下智能指针的特点:1.不用显式地写出析构函数。2.资源在智能指针的生命周期中始终有效。3.可以像指针一样的使用。4.最重要的是:具有RAII特性
C++98的失败设计:auto_ptr
上面的SmartPtr还存在着问题:拷贝构造对象和赋值时,多个SmartPtr指向同一块空间,当SmartPtr析构时便会造成资源的多次释放,导致程序崩溃。所以针对这个问题,智能指针有很多版本,C++98只有一个智能指针版本auto_ptr,auto_ptr对于拷贝问题的解决方案是:管理权的转移,比如将p1拷贝给p2,也就是将p1对于资源的管理权转移给了p2,即将p1的指针置空,后面的代码也就不能使用p1对象。这个智能指针被很多人诟病,许多公司明确要求不能使用auto_ptr指针,因为管理权转移导致了原指针不能使用,相比后续的智能指针,auto_ptr确实是个失败的设计。
auto_ptr的模拟实现
template <class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr<T>& p)
{
_ptr = p._ptr;
p._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& p)
{
_ptr = p._ptr;
p._ptr = nullptr;
}
~auto_ptr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};
unique_ptr
boost作为C++的一个开源库,承担着C++新功能的开发,如果boost库中有好用的设计出现,C++的标准库便会将好用的设计引入,出现在下一次的更新中。unique_ptr最初就是boost库中的scoped_ptr,C++的标准库汲取scoped_ptr中的精华并设计出了unique_ptr。unique_ptr解决智能指针拷贝问题的方案是:禁止拷贝
unique_ptr的模拟实现
template <class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
unique_ptr(const unique_ptr<T>& p) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& p) = delete;
~unique_ptr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};
在拷贝构造函数和赋值重载函数后加上delete,表示不能调用该函数,并且不能定义该函数。unique用这种简单粗暴的方式解决智能指针的拷贝问题
shared_ptr
shared_ptr允许拷贝智能指针,其采用了计数的方式进行拷贝,只有一个指针指向资源时,计数为1,两个指针指向,计数为2,以此类推。对象析构时,只要将计数减一,如果减一后的计数为1,说明该对象是指向资源的最后一个对象,需要完成资源的释放。
注意:shared_ptr中又存储了一个指向_count的指针,在拷贝构造后,两对象的_ptr指向同一块空间,需要将_count的值+1,达到计数增加的效果。
template <class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
: _ptr(ptr)
, _pCount(new int(1))
{}
shared_ptr(const shared_ptr<T>& p)
{
_ptr = p._ptr;
_pCount = p._pCount;
*_pCount++;
}
void Release()
{
if (--(*_pCount) == 0) // 当计数为0,需要释放pCount
{
if (_ptr) // 如果_ptr为空,只要释放pCount
{
delete _ptr;
_ptr = nullptr;
}
cout << "~shared_ptr()" << endl;
delete _pCount;
_pCount = nullptr;
}
}
shared_ptr<T>& operator=(const shared_ptr<T>& p)
{
// 指向资源不同时,使两指针指向同一资源,并且计数增加
if (_ptr != p._ptr) // 当指向资源相同时,没有必要进行赋值
{
Release(); // 先释放该指针之前指向的空间
_ptr = p._ptr;
_pCount = p._pCount;
* _pCount++;
}
}
~shared_ptr()
{
Release();
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
int* _pCount;
};
循环引用
shared_ptr还存在一个问题:循环引用。假设现在有Node这样一个类,有三个成员,两个分别指向前后节点的智能指针,一个保存数据的成员变量_val。当申请了Node类型的资源后,用shared_ptr托管,此时的智能指针可以正常的释放资源
struct Node
{
Node()
: _prev(nullptr)
, _next(nullptr)
, _val(0)
{}
myPtr::shared_ptr<Node> _prev;
myPtr::shared_ptr<Node> _next;
int _val;
};
int main()
{
myPtr::shared_ptr<Node> p1(new Node);
myPtr::shared_ptr<Node> p2(new Node);
// p1->_next = p2;
// p2->_prev = p1;
return 0;
}
但是当p1指针的_next指向p2,p2的_prev指向p1后,资源就不能正常释放,产生了内存泄漏
分析一下前后两段代码,当两智能指针没有产生连接时,p1和p2的计数都为1,释放资源正常。但它们产生连接后,p1的_next也是一个智能指针,将_next指向p2,相当于shared_ptr的一次赋值,此时的p2计数为2,p2的_prev指向p1后,p1的计数也为2。当main函数指向完,p1和p2的生命周期结束,自动调用其析构函数,但其析构函数是根据计数决定是否析构,两指针的计数都为2,析构后只是将计数减小为1,并没有释放资源。
此时要释放p1的资源,就要释放p2的_prev,_prev作为Node类的成员,只有Node类释放时才会被释放,所以现在要释放p2,而要释放p2就要释放p1的_next,要释放_next就要释放p1,这就回到了开头的问题。对于循环引用的问题,需要使用weak_ptr来解决。
weak_ptr
weak_ptr是解决shared_ptr循环引用问题的关键,用shared_ptr作为Node的前后指针类型,使得Node的前后指针也参与了资源的管理,weak_ptr作为弱指针,即不参与资源的管理,只是用来指向资源,类似于容器里的迭代器。
weak_ptr的定义
template <class T>
class weak_ptr
{
public:
weak_ptr(T* ptr)
:_ptr(ptr)
{}
weak_ptr(weak_ptr<T>& p)
{
_ptr = p._ptr;
}
weak_ptr<T>& operator=(weak_ptr<T>& p)
{
_ptr = p._ptr;
return *this;
}
weak_ptr<T>& operator=(shared_ptr<T>& p)
{
_ptr = p.get();
return *this;
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};
// Node的前后指针类型改为weak_ptr
struct Node
{
Node()
: _prev(nullptr)
, _next(nullptr)
, _val(0)
{}
myPtr::weak_ptr<Node> _prev;
myPtr::weak_ptr<Node> _next;
int _val;
};
定制删除器
上面模拟实现的智能指针中的析构函数,都是用delete进行释放资源。但这就有一个问题,如果资源需要用delete[]析构,或者不是new的,而是malloc的,又或者是一个文件指针,需要用fclose进行关闭,这时的析构函数就不能写死delete释放资源了,需要用不同的方式进行释放资源,但默认的释放方式为delete,C++提供了定制删除器进行特定资源的释放,名字看着唬人,其本质就是一个仿函数。
DefaultDelete为默认的删除器,执行的是delete语句,将unique_ptr改造,加一个新的模板参数Delete,接收默认值,使用者可以根据资源的不同传入相应的删除器。析构函数释放资源时,用Delete类构造一个对象,利用该对象对()的重载释放资源。
template <class T>
struct DefaultDelete
{
void operator()(T* p)
{
delete p;
cout << "delete:" << p << endl;
}
};
template <class T, class Delete = DefaultDelete<T>>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
unique_ptr(const unique_ptr<T>& p) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& p) = delete;
~unique_ptr()
{
Delete del;
//cout << "delete:" << _ptr << endl;
del(_ptr);
_ptr = nullptr;
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};
库中的unique_ptr也实现了定制删除器,但shared_ptr用于底层结构实现不同,定制删除器作为实参在其构造函数的参数被传入。
所以,在使用shared_ptr的删除器时,需要将对象传入,而不是类型,而传入对象,可以用lambda表达式代替。