智能指针是行为类似于指针的类对象,但这种对象还有其他功能。首先,看下面的函数:
void remodel(std::string & str) { std::string * ps = new std::string(str); ... if (weird_thing()) throw exception(); str = *ps; delete ps; return; }
当出现异常时(weird_thing()返回true),delete将不被执行,因此将导致内存泄露。可以用上一章介绍的方式修复这种问题:
void remodel(std::string & str) { std::string * ps = new std::string(str); ... try{ if (weird_thing()) throw exception(); } catch(exception &ex){ delete ps; throw; } str = *ps; delete ps; return; }
然而这将增加疏忽和产生其他错误的机会。
我们需要的是,当remodel函数中止时(不管是正常中止还是异常中止),本地变量都将从栈内存中删除,即指针ps占据的内存将被释放,同时ps指向的内存也被释放。
如果ps有一个析构函数,该析构函数在ps过期时释放它指向的内存。但问题在于,ps只是一个常规指针,不是有析构凼数的类对象。如果它是对象,则可以在对象过期时,让它的析构函数删除指向的内存。这正是智能指针背后的思想。
(我的理解是,将指针封装成为类,其析构函数可以释放指针占用的内存,在析构函数中增加delete释放它指向的内存。)
下面是使用智能指针auto_ptr修改该函数的结果:
# include <memory> //1.包含头义件memory(智能指针所在的头文件) void remodel (std::string & str) { std::auto_ptr<std::string> ps (new std::string(str));//2.将指向string的指针替换为指向string的智能指针对象 ...//智能指针模板在名称空间std中 if (weird_thing ()) throw exception(); str = *ps; // delete ps; 3. 删除delete语句 return; }
STL一共给我们提供了四种智能指针:auto_ptr、unique_ptr、shared_ptr和weak_ptr(本文暂不讨论)。模板auto_ptr是C++98提供的解决方案,C+11已将将其摒弃,并提供了另外两种解决方案。然而,虽然auto_ptr被摒弃,但它已使用了好多年;同时,如果您的编译器不支持其他两种解决力案,auto_ptr将是唯一的选择。
要创建智能指针对象,
template <class _Ty> class auto_ptr { // wrap an object pointer to ensure destruction public: using element_type = _Ty; explicit auto_ptr(_Ty* _Ptr = nullptr) noexcept : _Myptr(_Ptr) {} ... }
auto_ptr<string> pa1(new string("auto"));
1. 所有的智能指针类都有一个explicit构造函数,以指针作为参数。因此不能自动将指针转换为智能指针对象,必须显式调用:
shared_ptr<double> pd; double *p_reg = new double; pd = p_reg; // not allowed (implicit conversion) pd = shared_ptr<double>(p_reg); // allowed (explicit conversion) shared_ptr<double> pshared = p_reg; // not allowed (implicit conversion) shared_ptr<double> pshared(p_reg); // allowed (explicit conversion)
2. 智能指针类的析构函数中的delete,只能用于堆内存中动态建立(new)的对象。
string vacation("I wandered lonely as a cloud."); shared_ptr<string> pvac(&vacation); // No
pvac过期时,程序将把delete运算符用于非堆内存,这是错误的。
先来看下面的赋值语句:
auto_ptr< string> ps (new string ("I reigned lonely as a cloud.”); auto_ptr<string> vocation; vocation = ps;
上述赋值语句将完成什么工作呢?如果ps和vocation是常规指针,则两个指针将指向同一个string对象。但对于智能指针来说,这是不能接受的,因为程序将试图删除同一个对象两次——一次是ps过期时,另一次是vocation过期时。要避免这种问题,方法有多种:
当然,同样的策略也适用于复制构造函数。
每种方法都有其用途。下面是不适合使用auto_ptr的示例。
#include <iostream> #include <string> #include <memory> using namespace std; int main() { auto_ptr<string> films[5] = { auto_ptr<string> (new string("Fowl Balls")), auto_ptr<string> (new string("Duck Walks")), auto_ptr<string> (new string("Chicken Runs")), auto_ptr<string> (new string("Turkey Errors")), auto_ptr<string> (new string("Goose Eggs")) }; auto_ptr<string> pwin; pwin = films[2]; // films[2] loses ownership. cout << "The nominees for best avian baseballl film are\n"; for(int i = 0; i < 5; ++i) cout << *films[i] << endl; cout << "The winner is " << *pwin << endl; cin.get(); return 0; }
运行下发现程序崩溃了,这里的问题在于,
pwin = films[2];
所有权从films[2]转让给pwin,此时films[2]不再引用该字符串从而变成空指针,下面输出访问空指针导致程序崩溃了。如果上述代码,
unique_ptr<string> pwin; pwin = films[2]; // films[2] loses ownership.
错误的使用auto_ptr可能导致问题(这种代码的行为是不确定的,其行为可能随系统而异)。因此为了避免潜在的程序崩溃,要摒弃auto_ptr。
std::unique_ptr<double> pda(new double(5));//will use delete[]
new/delete | new[]/delete[] | |
auto_ptr | ✔ | |
unique_ptr | ✔ | ✔ |
shared_ptr | ✔ |
unique_ptr<string> pu1(new string ("hello world")); unique_ptr<string> pu2; pu2 = pu1; // #1 not allowed unique_ptr<string> pu3; pu3 = unique_ptr<string>(new string ("You")); // #2 allowed
其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。
当然,您可能确实想执行类似于#1的操作,仅当以非智能的方式使用摒弃的智能指针时(如解除引用时),这种赋值才不安全。要安全的重用这种指针,可给它赋新值。C++有一个标准库函数std::move(),让你能够将一个unique_ptr赋给另一个。下面是一个使用前述demo()函数的例子,该函数返回一个unique_ptr<string>对象:
使用move后,原来的指针仍转让所有权变成空指针,可以对其重新赋值。
unique_ptr<string> ps1, ps2; ps1 = demo("hello"); ps2 = move(ps1); ps1 = demo("alexia"); cout << *ps2 << *ps1 << endl;
应使用哪种智能指针呢?
(1)如果程序要使用多个指向同一个对象的指针,应选择shared_ptr。这样的情况包括:
很多STL算法都支持复制和赋值操作,这些操作可用于shared_ptr,但不能用于unique_ptr(编译器发出warning)和auto_ptr(行为不确定)。如果你的编译器没有提供shared_ptr,可使用Boost库提供的shared_ptr。
(2)如果程序不需要多个指向同一个对象的指针,则可使用unique_ptr。如果函数使用new分配内存,并返还指向该内存的指针,将其返回类型声明为unique_ptr是不错的选择。这样,所有权转让给接受返回值的unique_ptr,而该智能指针将负责调用delete。可将unique_ptr存储到STL容器在那个,只要不调用将一个unique_ptr复制或赋给另一个算法(如sort())。例如,可在程序中使用类似于下面的代码段。
unique_ptr<int> make_int(int n) { return unique_ptr<int>(new int(n)); } void show(unique_ptr<int> &p1) { cout << *a << ' '; } int main() { ... vector<unique_ptr<int> > vp(size); for(int i = 0; i < vp.size(); i++) vp[i] = make_int(rand() % 1000); // copy temporary unique_ptr vp.push_back(make_int(rand() % 1000)); // ok because arg is temporary for_each(vp.begin(), vp.end(), show); // use for_each() ... }
其中push_back调用没有问题,因为它返回一个临时unique_ptr,该unique_ptr被赋给vp中的一个unique_ptr。另外,如果按值而不是按引用给show()传递对象,for_each()将非法,因为这将导致使用一个来自vp的非临时unique_ptr初始化pi,而这是不允许的。前面说过,编译器将发现错误使用unique_ptr的企图。
在unique_ptr为右值时,可将其赋给shared_ptr,这与将一个unique_ptr赋给一个需要满足的条件相同。与前面一样,在下面的代码中,make_int()的返回类型为unique_ptr<int>:
unique_ptr<int> pup(make_int(rand() % 1000)); // ok shared_ptr<int> spp(pup); // not allowed, pup as lvalue shared_ptr<int> spr(make_int(rand() % 1000)); // ok
模板shared_ptr包含一个显式构造函数,可用于将右值unique_ptr转换为shared_ptr。shared_ptr将接管原来归unique_ptr所有的对象。
在满足unique_ptr要求的条件时,也可使用auto_ptr,但unique_ptr是更好的选择。如果你的编译器没有unique_ptr,可考虑使用Boost库提供的scoped_ptr,它与unique_ptr类似。
源自:《C++ Primer Plus》16.2节 智能指针模板类