c++智能指针

1,191 阅读3分钟

C++智能指针简介

STL一共给我们提供了四种智能指针:auto_ptrunique_ptrshared_ptrweak_ptrauto_ptr是C++98提供的解决方案,C+11已将将其摒弃。

🍉 摒弃auto_ptr的原因:避免潜在的内存崩溃问题。 auto_ptr可能出现访问空指针引起内存崩溃。

所有的智能指针类都有一个explicit构造函数,以指针作为参数。比如auto_ptr的类模板原型为:

templet<class T>
class auto_ptr {
  explicit auto_ptr(X* p = 0) ; 
  ...
};

因此意味着智能指针之间不允许隐式类型转换。另外:

// 不应将智能指针指向非堆内存变量,尤其是临时变量,运用delete时将导致未定义行为
string vacation("I wandered lonely as a cloud.");
shared_ptr<string> pvac(&vacation);   // No❗(尽管这不会报错)
  • 当程序试图将一个unique_ptr赋值给另一个时,如果原unique_ptr是个临时右值,编译器允许这么做;如果原unique_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
  • C++有一个标准库函数std::move(),让你能够将一个unique_ptr赋给另一个。该函数返回一个unique_ptr对象。使用move后,原来的指针仍转让所有权变成空指针,可以对其重新赋值:
unique_ptr<string> ps1, ps2;
ps1 = unique_ptr<string>(new string("hello")); 
ps2 = move(ps1);
ps1 = unique_ptr<string>(new string(" world"));
cout << *ps2 << *ps1 << endl;  // hello world

智能指针的选择

🍊 选择shared_ptr的情况包括:

  1. 有多个使用者共同使用同一个对象,而这个对象没有一个明确的拥有者

    • 这就好比教室里的电灯,大家都在使用这个电灯,但没有哪个人来专门负责这个电灯的开关。在这种场景下,我们就可以使用shared_ptr来管理这个电灯,通过shared_ptr,谁都可以使用这个电灯,但谁最后使用电灯谁就负责最后关灯。
  2. 某一个对象的复制操作很费时

    • 如果一个对象的复制操作很费时,同时我们又需要在函数间传递这个对象,我们往往会选择传递指向这个对象的指针来代替传递对象本身,以此来避免对象的复制操作。既然选择使用指针,那么使用shared_ptr是一个更好的选择,即起到了向函数传递对象的作用,又不用为释放对象操心。
  3. 要把指针存入标准库容器

    • STL容器包含指针。很多STL算法都支持复制和赋值操作,这些操作可用shared_ptr。不管容器中保存的是普通指针还是智能指针,在使用上,两者并无太大区别,使用智能指针的优越性主要体现在容器使用完毕后清空容器的操作上。如果容器中保存的是普通指针,当我们在清空某个容器时,先要释放容器中指针所指向的资源,然后才能清空这些指针本身。
  4. 当管理需要特殊清除方式的资源时,可以通过定制shared_ptr的删除器来实现

🍒 选择unique_ptr的情况:

如果程序不需要多个指向同一个对象的指针,则可使用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()
    ...
}

如果按值而不是按引用给show()传递对象,for_each()将非法,因为这将导致使用一个来自vp的非临时unique_ptr初始化pi,而这是不允许的。编译器将发现错误使用unique_ptr的企图。

unique_ptr为临时右值时,可将其赋给shared_ptr,这与将一个unique_ptr赋给一个需要满足的条件相同。与前面一样,在下面的代码中,make_int()的返回类型为unique_ptr

unique_ptr<int> pup(make_int(rand() % 1000));   // ok
shared_ptr<int> spp(pup);                       // not allowed, not temporary rvalue
shared_ptr<int> spr(make_int(rand() % 1000));   // ok

weak_ptr

C++11标准虽然将weak_ptr定位为智能指针的一种,但该类型指针通常不单独使用(没有实际用处),只能和shared_ptr类型指针搭配使用。甚至于,我们可以将weak_ptr 类型指针视为shared_ptr指针的一种辅助工具,借助weak_ptr类型指针,我们可以获取shared_ptr指针的一些状态信息,比如有多少指向相同的shared_ptr指针、shared_ptr指针指向的堆内存是否已经被释放等等。

需要注意的是,当weak_ptr类型指针的指向和某一shared_ptr指针相同时,weak_ptr指针并不会使所指堆内存的引用计数加1;同样,当weak_ptr指针被释放时,之前所指堆内存的引用计数也不会因此而减1。也就是说,weak_ptr类型指针并不会影响所指堆内存空间的引用计数。

除此之外,weak_ptr模板类中没有重载 *->运算符,这也就意味着,weak_ptr类型指针只能访问所指的堆内存,而无法修改它。

对于弱引用来说, 当引用的对象活着的时候弱引用不一定存在. 仅仅是当它存在的时候的一个引用, 弱引用并不修改该对象的引用计数, 这意味这弱引用它并不对对象的内存进行管理。

weak_ptr在功能上类似于普通指针, 然而一个比较大的区别是, 弱引用能检测到所管理的对象是否已经被释放, 从而避免访问非法内存。

注意: 虽然通过弱引用指针可以有效的解除循环引用, 但这种方式必须在程序员能预见会出现循环引用的情况下才能使用, 也可以是说这个仅仅是一种编译期的解决方案, 如果程序在运行过程中出现了循环引用, 还是会造成内存泄漏。