前言
C++提供了
4种智能指针
用于对分配的
内存进行自动释放
,分别是
auto_ptr、scope_ptr、shared_ptr、weak_ptr
。其中
auto_ptr在C++98标准引入,后三种在C++11标准中加入
。但是auto_ptr已经被C++摒弃,不建议使用。
为什么引入智能指针
①在C++中,如果使用普通指针
来创建一个指向某个对象的指针,那么在使用完这个对象之后我们需要自己释放它
,如果忘记释放
,那么会造成一个悬挂指针
,也就是说这个指针现在指向的内存区域其内容程序员无法把握和控制
,就很可能造成内存泄漏。
②还记得C++的异常机制
吗,在析构函数中是不能抛出异常
的,否则可能导致资源泄漏
(内存泄漏、句柄未关闭等)。
因此智能指针
的出现实际上就是为了可以方便的控制对象的生命期
,在智能指针中,一个对象什么时候
和在什么条件
下要被析构或者是删除是受智能指针本身决定的,用户并不需要管理。
智能指针的功能
智能指针是利用了一种叫做RAII(资源获取即初始化
)的技术对普通的指针
进行封装
,这使得智能指针实质是一个对象,行为表现的却像一个指针
。在构造函数完成资源的分配和初始化,在析构函数完成资源的清理,可以保证资源的正确初始化和释放。
auto_ptr智能指针
auto_ptr的核心思想是资源管理权的转移
,即在构造对象时赋予资源管理权,析构对象时撤销管理权
,但是auto_ptr尽量在任何情况下都不要使用,资源管理权的转移带来的最重要的缺点就是安全性问题
,下面具体阐述一下新旧版本的auto_ptr
方便理解。
既然要控制资源的转移,我们可以多定义一个成员对象_owner来标识该成员对象的资源管理权,在调用构造函数创建对象时赋予其资源管理权,即_owner置为true,在调用拷贝构造函数、赋值运算符的时候将其管理权释放,即_owner置为false,就起到了资源管理权的转移。
template<class T>
class AutoPtr
{
public:
AutoPtr(T* ptr = NULL);
AutoPtr(AutoPtr<T>& ap);
AutoPtr<T>& operator=(AutoPtr<T>& ap);
~AutoPtr();
T& operator*();
T& operator*()const;
T* operator->();
T* operator->()const;
protected:
T* _ptr;
mutable bool _owner;
};
template<class T>//构造函数
AutoPtr<T>::AutoPtr(T* ptr)
:_ptr(ptr)
,_owner(true)
{}
template<class T>//拷贝构造
AutoPtr<T>::AutoPtr(AutoPtr<T>& ap)
:_ptr(ap._ptr)
,_owner(true)
{
ap._owner = false;
}
template<class T>//赋值运算符的重载
AutoPtr<T>& AutoPtr<T>::operator=(AutoPtr<T>& ap)
{
if (this != &ap)
{
delete this->_ptr;
this->_ptr = ap._ptr;
this->_owner = true;
ap._owner = false;
}
return *this;
}
template<class T>//析构函数
AutoPtr<T>::~AutoPtr()
{
if (this->_ptr)
{
this->_owner = false;
delete this->_ptr;
}
}
template<class T>//重载*
T& AutoPtr<T>::operator*()
{
return *(this->_ptr);
}
template<class T>
T& AutoPtr<T>::operator*()const
{
return *(this->_ptr);
}
template<class T>//重载->
T* AutoPtr<T>::operator->()
{
return this->_ptr;
}
template<class T>//重载->const
T* AutoPtr<T>::operator->()const
{
return this->_ptr;
}
下面写一个测试用例来明确一下auto_ptr的缺陷在哪里,比如下面这段程序:
int main()
{
AutoPtr<int> p1(new int);
if (true)
{
AutoPtr<int> p2(p1);
}
*p1 = 10;
system("pause");
return 0;
}
p2出了if的作用域后会被释放,空间会回收,当再次试图给已经释放空间的对象赋值时,必然导致寻址失败导致程序奔溃
,这时p1虚无定所,也就成了一个野指针。
继承旧版aoto_ptr的资源管理权转移
的思想,去除标识管理权的_owner,当我们在调用拷贝构造函数、赋值运算符后直接将原对象置为NULL,禁止其访问原来的内存空间。
template<class T>
class AutoPtr
{
public:
AutoPtr();
AutoPtr(T* ptr);
AutoPtr(AutoPtr<T>& ap);
AutoPtr<T>& operator=(AutoPtr<T>& ap);
~AutoPtr();
T& operator*();
T& operator*()const;
T* operator->();
T* operator->()const;
protected:
T* _ptr;
};
template<class T>//构造函数
AutoPtr<T>::AutoPtr()
:_ptr(NULL)
{}
template<class T>
AutoPtr<T>::AutoPtr(T* ptr) // 不能写成const T* ptr,const类型的赋值不能给非const类型
:_ptr(ptr)
{}
template<class T>//拷贝构造
AutoPtr<T>::AutoPtr(AutoPtr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = NULL;
}
template<class T>//赋值运算符的重载
AutoPtr<T>& AutoPtr<T>::operator=(AutoPtr<T>& ap)
{
if (this != &ap)
{
delete this->_ptr;
this->_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
template<class T>//析构函数
AutoPtr<T>::~AutoPtr()
{
if (this->_ptr)
{
delete this->_ptr;
this->_ptr = NULL;
}
}
template<class T>//重载*
T& AutoPtr<T>::operator*()
{
return *(this->_ptr);
}
template<class T>
T& AutoPtr<T>::operator*()const
{
return *(this->_ptr);
}
template<class T>//重载->
T* AutoPtr<T>::operator->()
{
return this->_ptr;
}
template<class T>
T* AutoPtr<T>::operator->()const
{
return this->_ptr;
}
既然不让用总得有些理由吧,下面这段测试代码就足以让程序奔溃:
int main()
{
AutoPtr<int> p(new int(1));
AutoPtr<int> p1(p);
AutoPtr<int> p2 = p1;
*p1 = 10;
system("pause");
return 0;
}
这样的结果虽出乎意料,但也情理之中,对一个已经释放的对象重新赋值,必然导致程序奔溃,这样的程序是非常不安全的,到最后你会发现自己曾经种下的果树,作为所有者也无法吃到果子。
AutoPtr<int> FunTest()
{
AutoPtr<int> p(new int(1));
return p;
}
int main()
{
AutoPtr<int> ap1(FunTest());
system("pause");
return 0;
}
上述代码,FunTest()函数以值的形式返回,为一个临时对象,临时对象时具有常性的,因此在调用拷贝构造函数的时候,不能写成const T* ptr,const类型的赋值不能给非const类型,所以我们必须将赋值运算符重载与拷贝构造函数都设置为非const类型。
int main()
{
AutoPtr<int> p(AutoPtr<int>(new int(1)));
system("pause");
return 0;
}
在VS2017下,这段程序是正常执行的,在new一个对象时产生了一个无名对象,无名对象是具有常性的,在拷贝构造无名对象时,拷贝构造函数应该用const类型的参数来接收,但是这样的程序却成功运行了,原因就在于VS2017下编译器对其进行了优化,即不会再去调用它的拷贝构造函数,而是直接取调用构造函数。
为了验证这个程序的跨平台效果,我决定在g++上测试一下程序的正确性:
我们发现在g++上测试编译阶段都过不了,用一个具有常性的对象去拷贝构造一个对象时,必须用const类型对象的引用来接收具有const性质的的对象。
auto_ptr的总结
说了这么多,其实最终目的就是千万别用这个已经被C++摈弃的库函数, 资源权限的转移带来的是安全性的问题
,当你理解了它的使用带来的众多内存泄漏,程序崩溃的实例
时,相信你对这个智能指针也必须保持敬畏之心了。
鉴于auto_ptr指针带来的缺陷,我们引入了守卫指针即scoped_ptr,它的核心思想就是防拷贝
,因为在大多数情况下用不到拷贝构造函数
和赋值运算符的重载
,因此我们只需要给出常用的构造函数、析构函数以及常用运算符的重载。
scoped_ptr智能指针(unique_ptr)
它的实现原理的思想是防止对象的赋值与拷贝
,它与auto_ptr的共同之处在于scoped_ptr和auto_ptr都是利用了一个栈上的对象
去管理一个堆上的对象
,从而使得堆上的对象随着栈上的对象销毁时自动删除。
scoped_ptr如何防拷贝
①赋值运算符的重载与拷贝构造函数公有声明
:不可取
,原因在于:对于公有声明的函数在类外是可以对其定义的,非常不安全,无法防止别人拷贝。
②赋值运算符的重载与拷贝构造函数私有声明加定义
:不可取,原因在于:类外的确无法进行访问,但是在类内却可以拷贝,依旧没有起到防止拷贝的作用。
③赋值运算符的重载与拷贝构造函数私有声明
:可取,原因在于:只私有声明不定义,类外不展示,成功做到防止别人拷贝的作用。
scoped_ptr的疑惑
很多人会认为,既然不让拷贝就直接什么都不写就可以了,为什么还要私有声明,不是多此一举吗?其实这样的观点是错误不可取的。
如果你不写声明,那么编译器会自动调用系统自身的拷贝构造函数和赋值运算符重载,既然是防拷贝,编译器都为用户提供了一个公共接口了,岂不是又功亏一篑,因此,私有声明时很有必要
的。
模拟实现scoped_ptr(独占内存)
#include <iostream>
using namespace std;
template<class T>
class ScopedPtr
{
public:
ScopedPtr(T* ptr = NULL);
~ScopedPtr();
T& operator*();
T& operator*()const;
T* operator->();
T* operator->()const;
private:
ScopedPtr(const ScopedPtr<T>& ap);
ScopedPtr<T>& operator=(const ScopedPtr<T>& ap);
T* _ptr;
};
template<class T>//构造函数
ScopedPtr<T>::ScopedPtr(T* ptr)
:_ptr(ptr)
{}
template<class T>//析构函数
ScopedPtr<T>::~ScopedPtr()
{
if (this->_ptr)
{
delete this->_ptr;
_ptr = NULL;
}
}
template<class T>//重载*
T& ScopedPtr<T>::operator*()
{
return *(this->_ptr);
}
template<class T>
T& ScopedPtr<T>::operator*()const
{
return *(this->_ptr);
}
template<class T>//重载->
T* ScopedPtr<T>::operator->()
{
return this->_ptr;
}
template<class T>//重载->
T* ScopedPtr<T>::operator->()const
{
return this->_ptr;
}
int main()
{
ScopedPtr<int> p(new int(1));
system("pause");
return 0;
}
scoped_ptr的总结
防止拷贝的核心
就是私有声明
值运算符的重载和拷贝构造函数·,不会像`auto_ptr那样移交资源的管理权,并且scoped_ptr所管理的声明周期仅仅局限于一个作用域内,无法传到作用域之外,这也就意味着他不能作为函数的返回值,不能共享所有权,因此,也就造成了功能的单一。
同时scoped_ptr也不能用于管理数组对象,因为scoped_ptr是通过delete来删除所管理的对象的,而数组对象必须通过delete[]来释放。
模拟实现scoped_array
scoped_array的特点:
1.构造函数接受的的指针必须是new[]返回的结果,而不能是new返回的结果.
2.没有*、->操作符的重载,因为scoped_array管理的不是一个普通的指针.
3.析构函数使用delete[]释放内存而不是用delete;
4.提供operator[]操作符,因此我们可以像普通数组一样操作他,
5.没有begin\end类似于容器的迭代器操作函数.
#include <iostream>
#include <assert.h>
using namespace std;
template<class T>
class ScopedArray
{
public:
ScopedArray(T* ptr = NULL);
~ScopedArray();
T& operator[](size_t index);
private:
ScopedArray(const ScopedArray<T>& ap);
ScopedArray<T>& operator=(const ScopedArray<T>& ap);
T* _ptr;
};
template<class T>//构造函数
ScopedArray<T>::ScopedArray(T* ptr)
:_ptr(ptr)
{}
template<class T>//析构函数
ScopedArray<T>::~ScopedArray()
{
if (this->_ptr)
{
delete[] this->_ptr;
_ptr = NULL;
}
}
template<class T>
T& ScopedArray<T>::operator[](size_t index)
{
assert(index);
return _ptr[index];
}
int main()
{
ScopedArray<int> p(new int[10]);
system("pause");
return 0;
}
- 为什么标准库中没有引入scoped_array
原因很简单,因为人家C++标准库已经有了一个更为完善的容器stl::vector来处理连续的空间,根本不需要boost库里面的scoped_array。
从之前智能指针·auto_ptr的极度不安全
,到scoped_ptr的无法赋值拷贝
,我们看到的是功能的缺失,安全性的思考
,那么是否有一种智能指针能两者兼得
呢?shared_ptr无疑成为了首选之选,它是最像指针的智能指针
,也是Boost库中最有价值、最重要的组成部分之一。
shared_ptr的设计思想
共享指针,核心思想是通过·引用计数
来实现的,引入一个变量指针pCount来指向一块内存空间,当调用构造函数、赋值运算符的重载以及拷贝构造函数时都会对其引入的计数器pCount+1,只有当仅有一个指针指向这块空间时,即pCount=1时,才可以释放空间,否则引用计数pCount-1。
大家从这一点想到了什么?没错,之前STL中的string类
的写时拷贝
也是引入的这个思想,两者有异曲同工之妙,具体看源代码。
存在缺陷的版本源代码
#include <iostream>
using namespace std;
template<class T>
class SharedPtr
{
public:
SharedPtr();
SharedPtr(T* ptr);
SharedPtr(const SharedPtr<T>& ap);
~SharedPtr();
SharedPtr<T>& operator=(SharedPtr<T> ap);
T& operator*();
T& operator*()const;
T* operator->();
T* operator->()const;
long GetCount()const;
T* GetPtr()const;
private:
T* _ptr;
long* _pCount;
};
template<class T>//构造函数
SharedPtr<T>::SharedPtr()
:_ptr(NULL)
,_pCount(new long(1))
{}
template<class T>//构造函数(带参数)
SharedPtr<T>::SharedPtr(T* ptr)
:_ptr(ptr)
,_pCount(new long(1))
{}
template<class T>//拷贝构造函数
SharedPtr<T>::SharedPtr(const SharedPtr<T>& ap)
:_ptr(ap._ptr)
,_pCount(ap._pCount)
{
++(*this->_pCount);
}
template<class T>//析构函数
SharedPtr<T>::~SharedPtr()
{
if (--(*this->_pCount) == 0)
{
delete this->_ptr;
delete this->_pCount;
}
}
template<class T>//赋值运算符的重载
SharedPtr<T>& SharedPtr<T>::operator=(SharedPtr<T> ap)
{
if (this != &ap)
{
if (--(*this->_pCount) == 0)
{
delete this->_ptr;
delete this->_pCount;
}
this->_ptr = ap._ptr;
this->_pCount = ap._pCount;
++(*this->_pCount);
}
return *this;
}
template<class T>//重载*运算符
T& SharedPtr<T>::operator*()
{
return *(this->_ptr);
}
template<class T>//重载*运算符const
T& SharedPtr<T>::operator*()const
{
return *(this->_ptr);
}
template<class T>//重载->运算符
T* SharedPtr<T>::operator->()
{
return this->_ptr;
}
template<class T>//重载->运算符const
T* SharedPtr<T>::operator->()const
{
return this->_ptr;
}
template<class T>//获取当前引用计数的个数
long SharedPtr<T>::GetCount()const
{
return *(this->_pCount);
}
template<class T>
T* SharedPtr<T>::GetPtr()const//当前的对象指针
{
return this->_ptr;
}
int main()
{
SharedPtr<int> ap1(new int(2));
SharedPtr<int> ap2(ap1);
SharedPtr<int> ap3 = ap2;
system("pause");
return 0;
}
问题1->循环引用
比如下面的程序,我们定义一个双向循环链表的结构体,在结构体外定义两个节点,让p1的后继指向p2,p2的前驱指向p1,那么程序是否会正常运行呢?
#include<iostream>
#include <memory>
using namespace std;
struct ListNode
{
ListNode(int value)
:_value(value)
{
cout << "ListNode()" << endl;
}
~ListNode()
{
cout << "~ListNode()" << endl;
}
int _value;
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
};
void FunTest()
{
shared_ptr<ListNode> p1(new ListNode(1));
shared_ptr<ListNode> p2(new ListNode(2));
cout << p1.use_count() << endl;
cout << p2.use_count() << endl;
p1->_next = p2;
p2->_prev = p1;
cout << p1.use_count() << endl;
cout << p2.use_count() << endl;
}
int main()
{
FunTest();
system("pause");
return 0;
}
当资源要释放时,p1节点释放的前提是p2释放,而p2的释放又依赖于p1,就形成了一个互相等待
的局面,上升到操作系统
的话,就等于进程之间形成了死锁
,只不过这里是资源释放
的依赖关系,而操作系统是资源竞争
的关系。最终程序形成了循环引用
,两个节点都无法释放资源
,内存泄漏
也就顺理成章。
解决方案(循环引用)
为了解决shared_ptr带来的循环引用的问题,我们引入了weak_ptr
这个弱引用指针
,它不能单独使用
,用来辅助
shared_ptr。weak_ptr智能指针会对引用计数
做出特殊的处理,对上述情况不在加1
。比如下面的代码:
#include<iostream>
#include <memory>
using namespace std;
struct ListNode
{
ListNode(int value)
:_value(value)
{
cout << "ListNode()" << endl;
}
~ListNode()
{
cout << "~ListNode()" << endl;
}
int _value;
weak_ptr<ListNode> _prev;
weak_ptr<ListNode> _next;
};
void FunTest()
{
shared_ptr<ListNode> p1(new ListNode(1));
shared_ptr<ListNode> p2(new ListNode(2));
cout << p1.use_count() << endl;
cout << p2.use_count() << endl;
p1->_next = p2;
p2->_prev = p1;
cout << p1.use_count() << endl;
cout << p2.use_count() << endl;
}
int main()
{
FunTest();
system("pause");
return 0;
}
weak_ptr
是一种不控制所指向对象生存周期
的智能指针,它指向一个由shared_ptr管理的对象,将一个weak_ptr绑定到一个shared_ptr
,不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被销毁,即使有weak_ptr指向对象,对象还是会被释放。
你会发现当我们将链表结构体中的前驱指针与后继指针定义为weak_ptr时,引用计数不仅没有增加
,而且也成功调用了两个节点的析构函数,防止了内存泄漏以及循环引用计数
的问题。
问题2->线程安全性问题
shared_ptr 对象提供与内建类型
一样的线程安全级别
。一个shared_ptr 实例
可以同时被多个线程“读”(
仅使用不变操作进行访问)。 不同的 shared_ptr 实例可以同时被多个线程“写入”(使用类似 operator= 或 reset 这样的可变操作进行访问)(即使这些实 例是拷贝,而且共享下层的引用计数),任何其它的同时访问
的结果会导致未定义行为
。总结一下主要有3个方面:
①同一个shared_ptr被多个线程“读”是安全的。
②同一个shared_ptr被多个线程“写”是不安全的。
③共享引用计数的不同的shared_ptr被多个线程”写“ 是安全的。
问题3->内存泄漏问题
解决了循环引用
问题,尴尬的是还有一个内存泄漏
问题,也就是说,当我们用malloc申请出来的空间,是无法释放的,因为malloc申请的空间只能用free去释放。当我们打开一个文件的时候,相应的也会维护一个文件指针,当程序运行完毕的时候,就必须关闭这个文件,否则会造成内存泄漏。
解决方案(内存泄漏)
定置删除器
。如果指针是一个指向文件类型的,在析构函数中只需要关闭文件即可,而不是释放空间。如果空间是malloc出来的,那么析构函数中必须要调用free来释放资源,而不是delete来释放空间。这里我们会用到仿函数
,具体的实现见代码。
删除器版本的shared_ptr
#include <iostream>
using namespace std;
#pragma warning (disable:4996)
//关闭文件
struct Fclose
{
void operator()(void* ptr)
{
fclose((FILE*)ptr);
cout << "fclose()" << endl;
}
};
//释放malloc
struct Free
{
void operator()(void *ptr)
{
free(ptr);
cout << "free()" << endl;
}
};
//默认为delete
struct DefaultDelete
{
void operator()(void *ptr)
{
delete ptr;
cout << "delete()" << endl;
}
};
template<class T,class Deleter = DefaultDelete>
class SharedPtr
{
public:
SharedPtr(T* ptr);
SharedPtr(T* ptr, Deleter del);
SharedPtr(const SharedPtr<T, Deleter>& ap);
~SharedPtr();
SharedPtr<T, Deleter>& operator=(SharedPtr<T, Deleter> ap);
T& operator*();
T& operator*()const;
T* operator->();
T* operator->()const;
long GetCount()const;
T* GetPtr()const;
private:
T* _ptr;
long* _count;
Deleter _del;
};
template<class T,class Deleter = DefaultDelete>
SharedPtr<T, Deleter>::SharedPtr(T* ptr)//构造函数
:_ptr(ptr)
,_count(new long(1))
{}
template<class T, class Deleter = DefaultDelete>
SharedPtr<T, Deleter>::SharedPtr(T* ptr,Deleter del)//构造函数
: _ptr(ptr)
, _count(new long(1))
,_del(del)
{}
template<class T,class Deleter = DefaultDelete>
SharedPtr<T, Deleter>::SharedPtr(const SharedPtr<T, Deleter>& sp)//拷贝构造
:_ptr(sp._ptr)
,_count(sp._count)
,_del(sp._del)
{
++(*_count);
}
template<class T,class Deleter = DefaultDelete>
SharedPtr<T, Deleter>::~SharedPtr()//析构函数
{
if (--(*_count) == 0)
{
_del(_ptr);
delete _count;
}
}
template<class T, class Deleter = DefaultDelete>//赋值运算符的重载
SharedPtr<T, Deleter>& SharedPtr<T, Deleter>::operator=(SharedPtr<T, Deleter> sp)
{
std::swap(_ptr, sp._ptr);
std::swap(_count, sp._count);
std::swap(_del, sp._del);
return *this;
}
template<class T,class Deleter = DefaultDelete>
T& SharedPtr<T, Deleter>::operator*()//重载接引用
{
return *_ptr;
}
template<class T, class Deleter = DefaultDelete>
T& SharedPtr<T, Deleter>::operator*()const
{
return *_ptr;
}
template<class T, class Deleter = DefaultDelete>
T* SharedPtr<T, Deleter>::operator->()//重载间接运算符
{
return _ptr;
}
template<class T, class Deleter = DefaultDelete>
T* SharedPtr<T, Deleter>::operator->()const
{
return _ptr;
}
template<class T,class Deleter = DefaultDelete>
long SharedPtr<T, Deleter>::GetCount()const//获取引用计数
{
return *_count;
}
template<class T,class Deleter = DefaultDelete>
T* SharedPtr<T, Deleter>::GetPtr()const//获取对象指针
{
return _ptr;
}
int main()
{
SharedPtr<int> sp1(new int(1));//默认为delete
SharedPtr<FILE, Fclose> sp2(fopen("1.txt", "w+"), Fclose());//文件操作
SharedPtr<string, Free> sp3((string *)malloc(sizeof(string)), Free());//字符串操作
system("pause");
return 0;
}
智能指针总结
如果程序要使用多个指向同一个对象的指针
,应该选择shared_ptr
。这样的情况包括:有一个指针数组
,并使用一些辅助指针来表示特定的元素,如最大的元素和最小的元素;两个对象包含都指向第三个对象的指针;STL容器包含指针。
如果程序不需要多个指向同一个对象的指针
,则可使用scoped_ptr(unique_ptr)
。如果函数使用new分配内存,并返回指向该内存的指针,将其返回类型声明为scoped_ptr(unique_ptr)是不错的选择。这样,所有权将转让给接受返回值的unique_ptr
,而该智能指针将负责调用delete。
参考博客:
智能指针