1. RAII
在很多时候,我们在使用 new 申请出一块堆上的空间后会忘记使用delete释放资源,甚至在抛异常的情况中,就算我们并没有忘记释放资源,依然会出现内存泄漏的问题,如下代码所示。
#include<iostream>
using namespace std;
double divsion(double left, double right)
{
if (right == 0)
throw "right==0";
return left / right;
}
void fun(double left,double right)
{
double* ret = new double;
try
{
*ret = divsion(left, right);
}
catch (const char* erremg)
{
cout << erremg << endl;
}
delete ret;
cout << ret << endl;
}
int main()
{
while (1)
{
double left, right;
cin >> left >> right;
fun(left, right);
}
}
这里在fun函数开辟了一块double 类型和大小的空间,但是一旦divsion函数出现抛异常,只会执行catch中的语句并不会执行delete ret 的语句。
那么现在RAII的思想就可以帮助我们更好的解决问题。
RAII:用一个对象来存放和管理在堆上开辟的资源,这就是RAII的思想。(老婆怕你乱花钱,叫你把工资全给她,她帮你管)
在C++中实现RAII思想的方式就是智能指针,代码理解的方式如下所示。这里使用int_smart_point这个对象来存放在堆上申请的int类型的空间,在主函数结束时int_smart_point对象会自动的释放空间。
#include<iostream>
using namespace std;
template<class T>
class SmPtr
{
public:
SmPtr(T* ptr)
:_ptr(ptr)
{
}
~SmPtr()
{
delete _ptr;
}
private:
T* _ptr;
};
int main()
{
SmPtr<int> int_smart_point1 = new int;
return 0;
}
2.智能指针
在RAII的思想上我们还需要让这个类拥有指针的功能和行为,因此我们需要在类中重载 * 和 ->运算符。
在以下的代码中,重载了SmPtr这个类的* 和 ->,但是 int_smart_p1 赋值给 int_smart_p2的时候会出现问题,因为p1指向的地址拷贝给p2时是浅拷贝,p1 和 p2 指向的是同一片空间,在main函数执行完毕后,会先释放p2的空间,随后再释放p1指向的空间,在这个时候就出现了内存泄漏的问题。
#include<iostream>
using namespace std;
template<class T>
class SmPtr
{
public:
SmPtr(T* ptr)
:_ptr(ptr)
{
}
~SmPtr()
{
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T& operator->()
{
return _ptr;
}
private:
T* _ptr;
};
int main()
{
SmPtr<int> int_smart_point1 = new int;
#include<iostream>
using namespace std;
template<class T>
class SmPtr
{
public:
SmPtr(T* ptr)
:_ptr(ptr)
{
}
~SmPtr()
{
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T& operator->()
{
return _ptr;
}
private:
T* _ptr;
};
int main()
{
SmPtr<int> int_smart_point1 = new int;
SmPtr<int> int_smart_point2 = int_smart_point1; //出错
return 0;
}
return 0;
}
3. C++ 98 中的智能指针
在 98 中编译器采用了权限转移(auto_ptr)的方式来避免 一个智能指针拷贝另一个智能指针会将同一片空间析构两次的问题,它的做法是每次需要将一个指针拷贝给另一个指针时会将被拷贝指针指向空间的掌控权交给另一个指针,并且被拷贝的指针指针会失去去该空间的掌控权。
auto_ptr这个类在C++ memory 标准库中,这样的用法因为会把一个智能指针设为空所以如果后续代码中用户对此空间进行操作的话,编译器就会报错。因此很多公司都很不建议使用这样的方式来写代码。
#include<iostream>
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{
}
auto_ptr(auto_ptr<T>& sp)
:_ptr(sp._ptr)
{
sp._ptr = nullptr;
}
~auto_ptr()
{
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T& operator->()
{
return _ptr;
}
private:
T* _ptr;
};
int main()
{
auto_ptr<int>p1(new int);
auto_ptr<int>p2(p1);
return 0;
}
4. unique_ptr
这是在C++中新增的一个类,它的作用就是直接禁止用户将一个智能指针拷贝给另一个智能指针。
非常简单粗暴,就是不给你用,人家直接就是写在库里的。
5.shared_ptr
这也是C++11 中新增的类,它的功能就是支持智能之间的相互拷贝,它在类中增加了一个引用计数,每当有两个对象指向了同一片空间引用计数就会加一,引用计数等于0的时候才会真正的释放指向的那片空间。
这里引用计数器是 shared_ptr 这个类的核心,那么这个引用计数器实现的方式就是这里的关键了,这里不能使用 static 静态变量实现,因为这样的话所有同类型的 shared_ptr 对象都能访问这个static 变量,怎么可能所有的shared_ptr 对象都指向一块空间呢。
这里引用计数器我们可以在堆上开辟一个空间专门做引用计数器,拷贝赋值时直接让堆上这个int型的变量自加就成。
智能指针的同类型构造函数
#include<iostream>
using namespace std;
template<class T>
class Shared_ptr
{
public:
Shared_ptr(T* ptr=nullptr)
:_ptr(ptr)
, _count(new int(1))
{
*_count = 1;
cout << "p1第一次计数=" << *_count << endl;
}
Shared_ptr(Shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _count(sp._count)
{
++(*_count);
cout << "p1 被构造完了一次的计数" << *(sp._count) << endl;
cout << "p2 构造了完了的计数" << *_count << endl;
}
private:
T* _ptr;
int* _count;
};
int main()
{
Shared_ptr<int>p1;
Shared_ptr<int>p2(p1);
return 0;
}
以下是以上代码的运行结果
以下是shared_ptr的实现代码
#include<iostream>
using namespace std;
template<class T>
class Shared_ptr
{
public:
Shared_ptr(T* ptr=nullptr)
:_ptr(ptr)
, _count(new int)
{
*_count = 1;
}
Shared_ptr(Shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _count(sp._count)
{
++(*_count);
}
Shared_ptr& operator=(Shared_ptr<T>& sp)
{
if (--(*_count) == 0)
{
delete _ptr;
delete _count;
}
_ptr = sp._ptr;
++(*(sp._count));
_count = sp._count;
return *this;
}
private:
T* _ptr;
int* _count;
};
int main()
{
Shared_ptr<int>p1;
Shared_ptr<int>p2 = p1;
Shared_ptr<int>p3 = p2;
return 0;
}
6.循环引用
shared_ptr 虽然已经解决了智能指针之间相互复制的问题,但是存在循环引用的问题。如下所示
struct Node
{
Node()
{
}
A _a;
shared_ptr<Node> _prev;
shared_ptr<Node> _next;
};
int main()
{
shared_ptr<Node> p1 (new Node);
shared_ptr<Node> p2 (new Node);
p1->_next = p2;
p2->_prev = p1;
return 0;
}
在释放此段代码的空间时会发现析构p2时,p2中的_prev需要先析构p1这个对象,但是在析构p1时,p1中的_next 需要p2被析构,这时就产生了死锁的状态。
为了解决这个问题,C++中出现了 weak_ptr 这个类。它的出现是专门解决循环引用的问题的。无法单独使用,它本身不会修改引用计数,不参与资源的管控,但是可以访问资源。将代码改成如下所示就不会报错了。
struct Node
{
Node()
{
}
A _a;
weak_ptr<Node> _prev;
weak_ptr<Node> _next;
};
int main()
{
shared_ptr<Node> p1 (new Node);
shared_ptr<Node> p2 (new Node);
p1->_next = p2;
p2->_prev = p1;
return 0;
}
7. 定制删除器
如果申请多个多个对象时,使用的是new[],智能指针在析构时候,默认使用的是 delete 你并不是delete[] 所以在申请多个空间后析构时会发生问题,这时候我们需要用到定制删除器来解决该问题。
如何能在结构外部按照自己的想法来控制内部代码的运行逻辑呢?其实就是用仿函数或其它可调用类型来解决:
template<class T>
struct DeleteArray {
void operator()(T* ptr) {
delete[] ptr;
}
};
void fclo(FILE* fp) {
cout << "fclose(fp);" << endl;
fclose(fp);
}
int main() {
//传递仿函数对象
lzh::shared_ptr<A> p1(new A[10], DeleteArray<A>());
//传递lambda
lzh::shared_ptr<A> p2((A*)malloc(sizeof(A)), [](A* ptr) {
cout << "free(ptr)" << endl;
free(ptr);
});
//传递函数指针
lzh::shared_ptr<FILE> p3(fopen("test.cpp", "r"), fclo);
return 0;
}
上面的这种行为就称为定制删除器,不管用什么方式申请资源,最终通过它都可以正确释放。
为了能支持定制删除器,需要对shared_ptr的代码实现进行修改,修改后如下:
template<class T>
class shared_ptr {
public:
shared_ptr(T* ptr = nullptr) :_ptr(ptr), _pcnt(new int(1))
{}
//新增一个模板构造函数
template<class D>
//用D类型创建可调用对象
//由于不是类模板
//那么析构函数那里该如何拿到这个对象呢?
//可以使用包装器,知道它的参数和返回值
//就可以在类中定义一个包装器对象来接收它
shared_ptr(T* ptr, D del) : _ptr(ptr), _pcnt(new int(1)),
_del(del)
{}
~shared_ptr() {
if (--(*_pcnt) == 0) {
//使用删除器释放
_del(_ptr);
delete _pcnt;
}
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
shared_ptr(const lzh::shared_ptr<T>& sp) :_ptr(sp._ptr), _pcnt(sp._pcnt)
{
++(*_pcnt);
}
lzh::shared_ptr<T>& operator=(const lzh::shared_ptr<T>& sp) {
if (_ptr == sp._ptr) {
return *this;
}
if (--(*_pcnt) == 0) {
//使用删除器释放
_del(_ptr);
delete _pcnt;
}
_ptr = sp._ptr;
_pcnt = sp._pcnt;
++(*_pcnt);
return *this;
}
int use_count() const {
return *_pcnt;
}
T* get() const {
return _ptr;
}
private:
T* _ptr;
int* _pcnt;
//包装器用来接收构造函数中的可调用对象
//而申请单个对象时不需要传递删除器
//所以针对最基本的情况要给它一个缺省值
//当释放单个对象时直接使用默认的删除器即可
function<void(T*)> _del = [](T* ptr) { delete ptr; };
};