目录
一. 智能指针的概念及原理
1.1 什么是智能指针
1.2 智能指针的原理
二. 智能指针的拷贝问题
三. auto_ptr
3.1 auto_ptr的拷贝构造和赋值问题
3.2 auto_ptr的模拟实现
四. unique_ptr
六. weak_ptr
七. 总结
智能指针RAII(Resource Acquisition Is Initialization),是一种利用对象的生命周期来管理资源的技术。如果我们采用传统的new/delete来申请和释放资源,如果忘记调用delete,或者在调用delete之前程序抛出异常,都会导致内存泄漏问题,如代码1.1,Func函数中的new p2和Div都可能抛异常,导致后面的delete没有执行从而引发内存泄漏,采用智能指针对资源进行管理,能够杜绝这类问题。
代码1.1:因异常引发的内存泄漏
int Div(int a, int b)
{
if (b == 0)
throw "除0错误";
else
return a / b;
}
void Func()
{
int* p1 = new int[5];
int* p2 = new int[5];
//这里Div函数会抛异常,main函数会捕获异常,delete[]没有执行,引发内存泄漏
int ret = Div(5, 0);
delete[] p1;
delete[] p2;
}
int main()
{
try
{
Func();
}
catch (std::exception& e)
{
std::cout << e.what() << std::endl;
}
catch (...)
{
std::cout << "未知错误" << std::endl;
}
return 0;
}
智能指针是一个类,在对象构造时调用构造函数获取资源,在对象生命周期内,保证资源不被释放,在对象生命周期结束时,编译器自动调用析构函数来释放资源。这就相当于,将管理资源的责任移交给了对象,这样即使程序抛出异常也不存在内存泄漏,因为捕获异常往往跳出函数体,执行流会离开对象的作用域,对象生命周期结束,编译器自动调用析构函数释放了资源。
采用智能指针管理资源,有如下优点:
代码1.2定义了一种简易的智能指针SmartPtr,在其析构函数中会对资源进行释放。因为申请的资源可能是通过new T、new T[n]、malloc(...)这几种方法的任意之一来申请的,每种方式申请的资源需要不同的关键字/函数来释放资源,否则程序可能会崩溃。
因此,需要一个模板参数Del,这个模板参数用于接收仿函数,以匹配不同的资源释放方式。我们默认采用delete的方式释放资源,C++标准库中提供的智能指针,如果不显示给定仿函数来定义释放资源的方式,也是默认delete释放资源。
代码1.2:智能指针的简易实现 -- SmartPtr
template
struct Delete
{
void operator()(T* ptr) { delete ptr;}
};
template
struct DeleteArray
{
void operator()(T* ptr) { delete[] ptr; }
};
template
struct Free
{
void operator()(T* ptr) { free(ptr); }
};
template>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr) //构造函数
: _ptr(ptr)
{ }
~SmartPtr()
{
Del del;
del(_ptr);
}
private:
T* _ptr; //管理的资源
};
智能指针,顾名思义,不能仅仅是用于资源管理,还应当具有指针的一般功能。因此,需要重载operator*、operator->函数(见代码1.3),用于访问指针指向的资源。注意:C++标准库中的智能指针均不重载operator[]函数。
智能指针原理总结:
代码1.3:SmartPrt
template>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr) //构造函数
: _ptr(ptr)
{ }
~SmartPtr()
{
Del del;
del(_ptr);
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr; //管理的资源
};
我们要求智能指针具有一般的指针行为,因此,我们也就需要智能指针支持拷贝。但是,智能指针中的成员涉及到执行动态申请资源的指针,按照一般要求,应当进行深拷贝。
但是如果我们进行深拷贝,就会让两个智能指针指向不同的空间,但是我们所希望的是两个指针共同管理一块资源,因此我们就是要浅拷贝(值拷贝)。
但是值拷贝会存在对同一块空间多次释放的问题,对此,C++标准库中的智能指针auto_ptr和shared_ptr分别采用了管理权转移和引用计数的方法来解决问题,但一般会通过引用计数解决多次释放的问题(见第3、4章)。
智能指针拷贝问题总结:
auto_ptr采用管理权转移的方法进行赋值和拷贝构造,假设原先有一个auto_ptr对象p1,要通过p1构造p2,当拷贝构造完成后,用于拷贝构造传参的对象p1中管理资源的指针会被更改为nullptr,赋值也一样,假设p2=p1,p1中资源的管理权会转移给p2,p2原本的资源会被释放。
采用管理权转移的方法进行智能指针拷贝是一种极不负责任的行为,auto_ptr已经被很多公司明令禁止使用,一般项目中也极少使用auto_ptr。
这里最需要注意的问题是实现operator=函数时的自赋值检验,因为p2=p1要涉及到资源释放,如果p1和p2指向同一块资源,p2的资源被先行释放,那么p2=p1后,p2依旧指向原来的空间,但这块空间的使用权利已经换给了操作系统。
namespace zhang
{
template
class auto_ptr
{
public:
//构造函数
auto_ptr(T* ptr = nullptr)
: _ptr(ptr)
{ }
//拷贝构造函数
auto_ptr(auto_ptr& ap)
: _ptr(ap._ptr)
{
ap._ptr = nullptr; //管理权转移
}
//赋值函数
auto_ptr& operator=(auto_ptr& ap)
{
if (_ptr != ap._ptr) //自赋值检验
{
delete _ptr;
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
//析构函数
~auto_ptr()
{
delete _ptr;
}
private:
T* _ptr;
};
}
unique直接将拷贝构造和赋值禁止,也就不存在浅拷贝的多次释放同一块空间的问题。
namespace zhang
{
template
class unique_ptr
{
public:
//构造函数
unique_ptr(T* ptr = nullptr)
: _ptr(ptr)
{ }
//使用delete关键字强行禁止拷贝构造函数和赋值函数的生成
unique_ptr(const unique_ptr& up) = delete;
unique_ptr& operator=(const unique_ptr& up) = delete;
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
//析构函数
~unique_ptr()
{
delete _ptr;
}
private:
T* _ptr;
};
}
shared是C++11标准库中新纳入的智能指针,它通过引用计数的方法,较好的解决了拷贝和赋值的问题,是实际项目中最常用的智能指针。
接口函数 | 功能 |
---|---|
shared_ptr(T* ptr = nullptr, Del del = Delete |
构造函数,del为定制删除器,是一个仿函数对象,用于不同情况下的资源释放操作 |
shared_ptr(shared_ptr |
拷贝构造函数 |
shared_ptr |
赋值运算符重载函数 |
T& operator*() | 解引用操作符重载函数 |
T* operator->() | 成员访问操作符重载函数 |
T* get() | 获取shared_ptr内部管理资源的指针 |
long int use_count() | 获取引用计数(当前智能指针管理的资源被多少智能指针共同管理) |
bool unique() | 判断当前智能指针管理的资源是否只有它本身在管理(引用计数是否为1) |
namespace zhang
{
template>
class shared_ptr
{
public:
//构造函数
shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pcount(new long int(1))
{ }
//拷贝构造函数
shared_ptr(shared_ptr& sp)
: _ptr(sp._ptr)
, _pcount(sp._pcount)
{
++(*_pcount); //引用计数+1
}
//赋值函数
shared_ptr& operator=(shared_ptr& sp)
{
if (_ptr == sp._ptr) //自赋值检查
{
return *this;
}
//this的引用计数-1,并判断是否需要释放资源
if (--(*_pcount) == 0)
{
Del del;
del(_ptr);
delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
return *this;
}
//指针获取函数
T* get()
{
return _ptr;
}
//引用计数获取函数
long int use_count()
{
return *_pcount;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
bool unique()
{
return *_pcount == 1;
}
//析构函数
~shared_ptr()
{
if (--(*_pcount) == 0)
{
Del del;
del(_ptr);
delete _pcount;
}
}
private:
T* _ptr; //指向动态申请空间的指针
long int* _pcount; //引用计数
};
}
在绝大部分情况下,shared_ptr能够解决智能指针赋值造成的多次析构问题,也不会引发内存泄漏。但是,代码4.1展现了一种特殊情况,定义一个Node节点,其中包含两个shared_ptr成员_prev和_next。在主函数中实例化出两个shared_ptr
代码4.1:循环引用
struct Node
{
int _val;
std::shared_ptr _prev;
std::shared_ptr _next;
~Node()
{
std::cout << "~Node()" << std::endl;
}
};
int main()
{
std::shared_ptr n1(new Node);
std::shared_ptr n2(new Node);
n1->_next = n2;
n2->_prev = n1;
return 0;
}
在代码4.1中,循环引用的成因如下:
为了避免循环引用,可以把Node节点中的_next和_prev成员变量的类型改为weak_ptr
代码4.2:使用weak_ptr避免引用计数
struct Node
{
int _val;
std::weak_ptr _prev;
std::weak_ptr _next;
~Node()
{
std::cout << "~Node()" << std::endl;
}
};
int main()
{
std::shared_ptr n1(new Node);
std::shared_ptr n2(new Node);
n1->_next = n2;
n2->_prev = n1;
return 0;
}
weak_ptr不参与资源的管理和释放,可以使用shared_ptr对象来构造weak_ptr对象,但是不能直接使用指针来构造weak_ptr对象,在weak_ptr中,也没有operator*函数和operator->成员函数,不具有一般指针的行为,因此,weak_ptr严格意义上并不是智能指针,weak_ptr的出现,就是为了解决shared_ptr的循环引用问题。
weak_ptr在进行拷贝构造和赋值时,不增加引用计数,由于weak_ptr不参与资源管理,也不需要显示定义析构函数来释放资源。
template
class weak_ptr
{
public:
//默认构造函数
weak_ptr()
: _ptr(nullptr)
{ }
//拷贝构造函数
weak_ptr(weak_ptr& wp)
: _ptr(wp._ptr)
{ }
//采用shared_ptr构造
weak_ptr(shared_ptr& sp)
: _ptr(sp.get())
{ }
//赋值函数
weak_ptr& operator=(weak_ptr& wp)
{
_ptr = wp._ptr;
}
//通过shared_ptr对象赋值
weak_ptr& operator=(shared_ptr& sp)
{
_ptr = sp.get();
}
private:
T* _ptr;
};