智能指针
引言
智能指针和普通指针的区别在于智能指针实际上是对普通指针加上了一层封装机制,这样的一层封装机制的目的是为了使智能指针可以方便的管理一个对象的生命期。
在C++中,我们知道,入伏哦使用普通的指针来创建一个指向某个对象的指针,那么在使用完这个对象之后我们需要自己删除它,例如:
ObjectType* temp_ptr = new ObjectType(); temp_ptr->foo(); delete temp_ptr;
指出如果程序员忘记在调用完temp_ptr之后删除temp_ptr,那么会造成哟个指针悬挂(dangling pointer),也就是说这个指针现在指向的内存区域程序员无法把控和控制,也可能会造成内存泄漏。
可是事实上,不只是“忘记”,在上述的一段程序中,如果foo()在运行时抛出异常,那么temp_ptr所指向的对象仍不会被安全删除。
在这个时候,智能指针的出现实际上就是为了可以方便的控制对象的生命期,在智能指针中,一个对象什么时候和在什么条件下要被析构或者是删除是受智能指针本身所决定的,用户并不需要管理。
智能指针的使用
智能指针是在<memory>头文件中的std空间中定义的,它们对RAII或“获取资源即初始化”编程惯用法至关重要。此习惯用法的主要目的是确保资源获取与对象初始化同时发生,从而能够创建该对象的所有资源并在某行代码中准备就绪。实际上,RAII的主要原则是为将任何堆分配资源(例如,动态分配内存或系统对象句柄)的所有权提供给其析构函数,这包含用于删除或释放资源的代码以及任何相关请理代码的堆栈分配对象。
大多数情况下,当初始化原始指针或资源句柄以指向实际资源时,会立即将指针传递给智能指针。在现代C++中,原始指针仅用于范围有限的小代码块、循环或者性能至关重要且不会混淆所有权的Helper函数中。
下面的示例将原始指针声明和智能指针声明进行了比较:
void UseRawPointer() { // Using a raw pointer -- not recommended. Song* pSong = new Song(L"Nothing on You", L"Bruno Mars"); // Use pSong... // Don't forget to delete! delete pSong; } void UseSmartPointer() { // Declare a smart pointer on stack and pass it the raw pointer. unique_ptr<Song> song2(new Song(L"Nothing on You", L"Bruno Mars")); // Use song2... wstring s = song2->duration_; //... } // song2 is deleted automatically here.
如示例所示,智能指针是我们在堆栈上声明的类模板,并可通过使用某个堆分配的对象的原始指针进行初始化。在初始化智能指针之后,它将拥有原始的指针。这意味着智能指针负责删除原始指定的内存。智能指针析构函数包括要删除的调用,并且由于在堆栈上声明了智能指针,当智能指针超出范围时将调用其析构函数,尽管堆栈上的某处将进一步引发异常。
通过使用熟悉的指针运算符(->
和 *
)访问封装指针,智能指针类将重载这些运算符以返回封装的原始指针。
[注意]:请始终在单独的代码行上创建智能指针,而绝不在参数列表中创建智能指针,这样就不会由于某些参数列表分配规则而产生轻微泄露资源的情况。
下面的示例演示了如何使用标准模板库中的 unique_ptr
智能指针类型将指针封装到大型对象。
class LargeObject { public: void DoSomething(){} }; void ProcessLargeObject(const LargeObject& lo){} void SmartPointerDemo() { // Create the object and pass it to a smart pointer std::unique_ptr<LargeObject> pLarge(new LargeObject()); //Call a method on the object pLarge->DoSomething(); // Pass a reference to a method. ProcessLargeObject(*pLarge); } //pLarge is deleted automatically when function block goes out of scope.
此示例演示如何使用智能指针执行以下关键步骤:
- 将智能指针声明为一个自动(全局)变量(不要对智能指针本身使用new或malloc表达式)
- 在类型参数中,指定封装指针的指向类型
- 在智能指针构造函数中将原始指针传递给new对象.(某些实用工具函数或智能指针构造函数可为你执行此操作)
- 使用重载的
->
和*
运算符访问对象。 - 允许智能指针删除对象。
智能指针的设计原则是在内存和性能上尽可能高效,它具有通过使用“点”表示法访问的成员函数,通常会提供直接访问其原始指针的方法,STL 智能指针拥有一个用于此目的的 get
成员函数,CComPtr
拥有一个公共的 p
类成员。 通过提供对基础指针的直接访问,你可以使用智能指针管理你自己的代码中的内存,还能将原始指针传递给不支持智能指针的代码。
智能指针的类型
STL一共给我们提供了四种智能指针:auto_ptr、unique_ptr、shared_ptr和weak_ptr
auto_ptr 是C++98提供的方案,C++11已经其摒弃。使用这些智能指针作为将指针封装为纯旧 C++ 对象 (POCO) 的首选项。
auto_ptr
C++11已经将其摒弃,这里我们只是简单提一下。
class Test { public: Test(string s) { str = s; cout<<"Test creat\n"; } ~Test() { cout<<"Test delete:"<<str<<endl; } string& getStr() { return str; } void setStr(string s) { str = s; } void print() { cout<<str<<endl; } private: string str; }; int main() { auto_ptr<Test> ptest(new Test("123")); ptest->setStr("hello "); ptest->print(); ptest.get()->print(); ptest->getStr() += "world !"; (*ptest).print(); ptest.reset(new Test("123")); ptest->print(); return 0; }
![image](https://images0.cnblogs.com/blog/517264/201312/03230705-9573a380081e4bc18b7407847822ec16.png)
如上面的代码:智能指针可以像类的原始指针一样访问类的public成员,成员函数get()返回一个原始的指针,成员函数reset()重新绑定指向的对象,而原来的对象则会被释放。注意我们访问auto_ptr的成员函数时用的是“.”,访问指向对象的成员时用的是“->”。我们也可用声明一个空智能指针auto_ptr<Test>ptest();
当我们对智能指针进行赋值时,如ptest2 = ptest,ptest2会接管ptest原来的内存管理权,ptest会变为空指针,如果ptest2原来不为空,则它会释放原来的资源,基于这个原因,应该避免把auto_ptr放到容器中,因为算法对容器操作时,很难避免STL内部对容器实现了赋值传递操作,这样会使容器中很多元素被置为NULL。判断一个智能指针是否为空不能使用if(ptest == NULL),应该使用if(ptest.get() == NULL),如下代码
int main() { auto_ptr<Test> ptest(new Test("123")); auto_ptr<Test> ptest2(new Test("456")); ptest2 = ptest; ptest2->print(); if(ptest.get() == NULL)cout<<"ptest = NULL\n"; return 0; }
还有一个值得我们注意的成员函数是release,这个函数只是把智能指针赋值为空,但是它原来指向的内存并没有被释放,相当于它只是释放了对资源的所有权,从下面的代码执行结果可以看出,析构函数没有被调用。
int main() { auto_ptr<Test> ptest(new Test("123")); ptest.release(); return 0; }
那么当我们想要在中途释放资源,而不是等到智能指针被析构时才释放,我们可以使用ptest.reset(); 语句。
unique_ptr
unique_ptr
是对auto_ptr
的替换,unique_ptr
遵循着独占语义。在任何时间点,资源只能唯一地被一个unique_ptr
占有。当unique_ptr
离开作用域,所包含的资源被释放。如果资源被其它资源重写了,之前拥有的资源将被释放。所以它保证了它所关联的资源总是能被释放。
unique_ptr 是一个独享所有权的智能指针,它提供了严格意义上的所有权,包括:
- 拥有它指向的对象
- 无法进行复制构造,无法进行复制赋值操作。即无法使两个unique_ptr指向同一个对象。但是可以进行移动构造和移动赋值操作
- 保存指向某个对象的指针,当它本身被删除释放的时候,会使用给定的删除器释放它指向的对象
unique_ptr 可以实现如下功能:
- 为动态申请的内存提供异常安全
- 讲动态申请的内存所有权传递给某函数
- 从某个函数返回动态申请内存的所有权
- 在容器中保存指针
- auto_ptr 应该具有的功能
头文件:<memory>
。 有关更多信息,请参见如何:创建和使用 unique_ptr 实例和unique_ptr 类。
创建
unique_ptr不提供复制语义(拷贝复制和拷贝构造都不可以),只支持移动语义(move semantics).也就是说unique_ptr 不共享它的指针。它无法复制到其他 unique_ptr
,无法通过值传递到函数,也无法用于需要副本的任何标准模板库 (STL) 算法。 只能移动 unique_ptr
。 这意味着,当内存资源所有权将转移到另一 unique_ptr时
,原始 unique_ptr
不再拥有此资源。 我们建议将对象限制为由一个所有者所有,因为多个所有权会使程序逻辑变得复杂。 因此,当需要智能指针用于纯 C++ 对象时,可使用 unique_ptr
,而当构造 unique_ptr
时,可使用 make_unique Helper 函数。
下图演示了两个unique_ptr实例之间的所有权转换
以下示例演示unique_ptr的基本使用
unique_ptr<Test> fun() { return unique_ptr<Test>(new Test("789")); } int main() { unique_ptr<Test> ptest(new Test("123")); unique_ptr<Test> ptest2(new Test("456")); ptest->print(); ptest2 = std::move(ptest);//不能直接ptest2 = ptest if(ptest == NULL)
cout<<"ptest = NULL\n"; Test* p = ptest2.release(); p->print(); ptest.reset(p); ptest->print(); ptest2 = fun(); //这里可以用=,因为使用了移动构造函数 ptest2->print(); return 0; }
以下示例演示如何创建unique_ptr实例并在函数之间传递这些实例
unique_ptr<Song> SongFactory(const std::wstring& artist, const std::wstring& title) { // Implicit move operation into the variable that stores the result. return make_unique<Song>(artist, title); } void MakeSongs() { // Create a new unique_ptr with a new object. auto song = make_unique<Song>(L"Mr. Children", L"Namonaki Uta"); // Use the unique_ptr. vector<wstring> titles = { song->title }; // Move raw pointer from one unique_ptr to another. unique_ptr<Song> song2 = std::move(song); // Obtain unique_ptr from function that returns by value. auto song3 = SongFactory(L"Michael Jackson", L"Beat It"); }
这些示例就说明了unique_ptr的基本特征:可移动,但不可复制,“移动”将所有权转移到新的unique_ptr并重置旧的unique_ptr。
share_ptr
从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。所有实例指向同一个对象,并共享访问一个“控制块”,即每当一个新的 shared_ptr
被添加时,递增和递减引用计数,超出范围,则复位。当引用计数到达零时,控制块删除内存资源和自身。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。具体的成员函数解释可以参考 here。
下图显示了指向一个内存位置的几个shared_ptr示例
以下示例演示share_ptr的基本使用
int main() { shared_ptr<Test> ptest(new Test("123")); shared_ptr<Test> ptest2(new Test("456")); cout<<ptest2->getStr()<<endl; cout<<ptest2.use_count()<<endl; ptest = ptest2;//"456"引用次数加1,“123”销毁 ptest->print(); cout<<ptest2.use_count()<<endl;//2 cout<<ptest.use_count()<<endl;//2 ptest.reset(); ptest2.reset();//此时“456”销毁 cout<<"done !\n"; return 0; }
有关更多信息,请参见如何:创建和使用 shared_ptr 实例
weak_ptr
weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。
以下示例演示weak_ptr的基本使用
class B; class A { public: shared_ptr<B> pb_; ~A() { cout<<"A delete\n"; } }; class B { public: shared_ptr<A> pa_; ~B() { cout<<"B delete\n"; } }; void fun() { shared_ptr<B> pb(new B()); shared_ptr<A> pa(new A()); pb->pa_ = pa; pa->pb_ = pb; cout<<pb.use_count()<<endl; cout<<pa.use_count()<<endl; } int main() { fun(); return 0; }
![image](https://images0.cnblogs.com/blog/517264/201312/03230710-de79f5e19be14c90ba695f87d8d7608d.png)
class B; class A { public: weak_ptr<B> pb_; //这里有变动 ~A() { cout<<"A delete\n"; } }; class B { public: weak_ptr<A> pa_; //这里有变动 ~B() { cout<<"B delete\n"; } }; void fun() { shared_ptr<B> pb(new B()); shared_ptr<A> pa(new A()); pb->pa_ = pa; pa->pb_ = pb; cout<<pb.use_count()<<endl; cout<<pa.use_count()<<endl; } int main() { fun(); return 0; }
注意的是我们不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样访问,pa->pb_->print(); 英文pb_是一个weak_ptr,应该先把它转化为shared_ptr,如:shared_ptr<B> p = pa->pb_.lock(); p->print();
参考文献:
https://www.zhihu.com/question/20368881
https://msdn.microsoft.com/zh-cn/library/hh279674.aspx