C++|C++ 智能指针

目录
1. 产生的原因-内存泄漏
2. 智能指针简介
3. auto_ptr
4. unique_ptr
5. shared_ptr
6. weak_ptr
参考链接:智能指针、auto_ptr 参考、shared_ptr参考
1. 产生的原因-内存泄漏当我们当从堆中申请了内存后,如果不释放空间,就发生内存泄漏。内存泄漏的情景主要有几种:
* new和delete没有匹配。 * 没有正确清楚嵌套对象指针。 * 释放对象数组的时候没有使用方括号。 * 指向对象的指针数组不等同于对象数组(数组中的每个对象为指针)。 * 缺少拷贝构造函数或者没有重载赋值操作符,导致按值传递,两次释放相同的内存。 * 没有将基类的析构函数定义为虚函数。

然而,即便写出了清晰并且带有错误验证的代码,有时候仍会出现问题。比如和别人合作写代码的时候,合作者可能就会在完美的程序中增加一个提早返回的语句,导致申请的内存空间无法释放。针对以上原因,C++推出了智能指针。
这里顺便提一个概念,野指针。野指针表示指向被释放的或者访问受限内存的指针。使用野指针很容易导致内存泄漏。 在使用指针的时候除了避免内存泄漏,野指针也需要尽量避免的。产生原因主要有几个:
  • 指针变量没有初始化。
  • 指针被free或者delete后,没有置为NULL。
  • 指针操作超过变量作用范围。
2. 智能指针简述智能指针定义在memoery文件中,是一个RAII(Resource Acquisition is initialization)的类模型。智能指针类的构造函数中传入一个普通指针,析构函数中释放传入的指针。因为智能指针的类都是栈上的对象,所以当函数(或程序)结束时会自动被释放。
3. C++98 auto_ptrauto_ptr的出现,主要是为了解决“有异常抛出时发生内存泄漏”的问题,如下所示。
int* p = new int(100); try { doSomething(); cout << *p << endl; delete p; } catch(){}

当doSomething(); 部分抛出异常,将导致指针p所指向的空间得不到释放而导致内存泄露,因而引入auto_ptr。
3.1 auto_ptr的使用
在使用auto_ptr的时候,我们实际上是创建一个auto_ptr类型的局部对象。该局部对象析构时,会将自身所拥有的指针空间释放,从这个角度避免了内存泄露。另外,auto_ptr重载了’*‘和’->’,运算符,可以像普通指针那样进行提领操作。
#include #include #include class Test { public: Test(int a = 0 ) : m_a(a){ std::cout << "Calling constructor" << std::endl; } ~Test( ) { std::cout << "Calling destructor" << std::endl; } public: int m_a; }; //抛出异常 void Fun(int a, int b, int &c) { if( a == 0 ) { throw -1; } c = b / a; return; }//测试 int main( ) { try{ std::auto_ptr p(new Test(5)); int c = 0; Fun(0, 3, c); std::cout << p->m_a << std::endl; //提领操作 } catch(...){ std::cout << "catch()" << std::endl; } return 0; }/* Output:Calling constructor Calling destructor catch() */

使用auto_ptr的时候有几个问题需要注意:
(a) auto_ptr的构造函数为explicit,阻止了一般指针隐式类型转换为auto_ptr的构造,所以如下的创建方式是编译不过的。
int* p = new int(1); auto_ptr ap = p; //无法进行隐式变换

(b) 由于auto_ptr对象析构时会删除它所拥有的指针,所以使用时应避免像下例所示的多个auto_ptr对象管理同一个指针。
int* np = new int(1); auto_ptr p1(np); auto_ptr p2(np);

? auto_ptr的析构函数删除对象使用delete而不是delete[],所以auto_ptr不能用来管理数组指针。
int *p = new int[100]; auto_ptr ap(p); //仅仅释放第一个元素空间,造成内存泄漏

(d) C++中对一个空指针NULL执行delete操作是安全的,所以在auto_ptr的析构函数中无须判断它所拥有指针是否为空。
3.2 auto_ptr的拷贝构造和赋值
auto_ptr要求对所拥有的指针完全占有,也就是说,一个一般指针不能同时被两个auto_ptr所拥有。这也意味着auto_ptr在拷贝构造和赋值运算符重载时要做特殊处理。具体的做法是对所有权进行了完全转移,在拷贝和赋值时,剥夺原auto_ptr拥有权(置空),赋予当前auto_ptr对指针的拥有权。
由于会修改原对象,所以auto_ptr的拷贝构造函数以及赋值运算符重重载函数的参数是引用而不是常(const)引用。这时候就需要注意以下几个问题:
(a) auto_ptr对象被拷贝或者被赋值后,已经失去了对原指针的所有权,此时,对这个auto_ptr的读取操作是不安全的。尤其是当auto_ptr作为函数参数按值传递,传入的实参auto_ptr对指针的所有权会转移到函数临时的auto_ptr对象上。临时auto_ptr在函数退出时被析构,则当函数调用结束时,原实参所指向的对象已经被删除了。
void func(auto_ptr ap) { cout << *ap << endl; }auto_ptr ap(new int(1)); func(ap); cout << *ap1 << endl; //错误,函数调用结束后,ap1已经不再拥有任何对象了

因此,要避免使用auto_ptr对象作为函数参数按值传递,按引用传递在调用函数是不会发生所有权转移,但是无法预测函数体内的操作,有可能在函数体内进行了所有权的转移。因此按引用传递auto_ptr作为函数参数也是不安全的。如果不得不使用auto_ptr对象作为函数参数时,尽量使用const引用传递参数。
(b) auto_ptr支持所拥有的指针类型之间的隐式类型转换。
class base{}; class derived: public base{}; auto_ptr apbase = auto_ptr(new derived); //auto_ptr隐式转换到auto_ptr

? auto_ptr对象不能作为STL容器元素。C++的STL容器对于容器元素类型的要求是有值语义,即可以赋值和复制。但是auto_ptr在赋值和复制时都进行了特殊操作。
3.3 auto_ptr的相关操作
T* get(); //获得auto_ptr所拥有的指针。 T* release(); //释放auto_ptr的所有权,并将所有用指针返回。 void reset(T* ptr=0); // 接收所有权,接收之前拥有其它指针的话,必须先释放其空间。

4. C++11 unique_ptrunique_ptr和auto_ptr类似,都是同一时刻只能有一个unique_ptr指向给定对象。但是它禁止拷贝语义,只支持移动语义。
4.1 unique_ptr的使用方法 unique_ptr不能拷贝,也不能赋值,只能通过move()转换所有权或者通过reset()重置所有权。另外,它还可以通过release方法释放所有权。
#include #include class Test { public: Test(int a = 0 ) : m_a(a){ std::cout << "Calling constructor" << std::endl; } ~Test( ) { std::cout << "Calling destructor" << std::endl; } public: int m_a; }; //测试 int main() { std::unique_ptr uptr(new Test(10)); //绑定动态对象 //std::unique_ptr uptr2 = uptr; //不能赋值 //std::unique_ptr uptr2(uptr); //不能拷贝 std::cout << "uptr->ma: " << uptr->m_a << std::endl; std::unique_ptr uptr2 = std::move(uptr); //转换所有权 //std::cout << "uptr->ma: " << uptr->m_a << std::endl; std::cout << "uptr2->ma: " << uptr2->m_a << std::endl; //uptr2.release(); //释放所有权 return 0; }//离开作用域是自动析构/* Output:Calling constructor uptr->ma: 10 uptr2->ma: 10 Calling destructor */

上例需要注意的是,如果我们取消掉release()函数的注释,两个指针都失去了对象的所有权,这时候之前的Test对象就无法被销毁。
5. C++11 shared_ptrshared_ptr 是一个标准的共享所有权的智能指针, 允许多个指针指向同一个对象,主要是为了解决auto_ptr和unique_ptr在对象所有权上的局限性。因为加入了计数机制,也就产生了额外的开销:
  • shared_ptr对象除了包括一个所拥有对象的指针外, 还必须包括一个引用计数代理对象的指针。
  • 时间上的开销主要在初始化和拷贝操作上, *和->操作符重载的开销跟auto_ptr是一样。
5.1 何时需要shared_ptr?
  • 程序不知道自己需要使用多少对象. 如使用窗口类, 使用 shared_ptr 为了让多个对象能共享相同的底层数据.
  • 程序不知道所需对象的准确类型.
  • 程序需要在多个对象间共享数据.
5.2 shared_ptr的使用方法
每一个shared_ptr的拷贝都指向相同的内存。每拷贝一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。
#include #include int main() { { int a = 10; //使用make_shared函数初始化。 std::shared_ptr ptra = std::make_shared(a); //智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。 std::shared_ptr ptra2(ptra); std::cout << ptra.use_count() << std::endl; int b = 20; int *pb = &a; //std::shared_ptr ptrb = pb; //error std::shared_ptr ptrb = std::make_shared(b); //ptra2原来指向的对象引用计数减1(如果为0, 释放内存), ptrb指向的对象的引用计数加1 ptra2 = ptrb; //assign,拷贝构造 pb = ptrb.get(); //获取原始指针std::cout << ptra.use_count() << std::endl; std::cout << ptrb.use_count() << std::endl; } }//Output: 2 1 2

5.3 注意事项
a. 不能将指针直接赋值给一个智能指针,一个是类,一个是指针。
std::shared_ptr p4 = new int(1); //ERROR

b. 不要用一个原始指针初始化多个shared_ptr,否则会造成二次释放同一内存。
c. shared_ptr作为对象成员变量时,应避免循环引用。
假设a对象中含有一个shared_ptr指向对象b, 对象b中含有一个shared_ptr 指向对象a, 并且 a, b 对象都是堆中分配的。当m_spa被销毁时, 对象a的use_count从2变为1; 对象b同理。至此,a和b失去联系,但是却无法继续销毁自身,如下所示。
#include #include class CB; class CA; class CA{ public: CA() { } ~CA() { std::cout << "~CA()" << std::endl; }void Register(const std::shared_ptr& sp){ m_sp = sp; } private: std::shared_ptr m_sp; }; class CB{ public: CB() { }; ~CB() { std::cout << "~CB()" << std::endl; }; void Register(const std::shared_ptr& sp){ m_sp = sp; } private: std::shared_ptr m_sp; }; int main(){ std::shared_ptr spa(new CA()); std::shared_ptr spb(new CB()); spa->Register(spb); spb->Register(spa); std::cout << spb.use_count() << std::endl; //2 std::cout << spa.use_count() << std::endl; //2 } //两个对象都没有调用析构函数(都没有被销毁)

解决此方法是使用 weak_ptr 替换 shared_ptr,在weak_ptr小节介绍。
d. shared_ptr 不支持数组, 如果使用数组, 需要自定义删除器。
// 下例是一个利用 lambda 实现的删除器 std::shared_ptr sps(new int[10], [](int *p){delete[] p; }); //对于数组元素的访问, 需使要使用 get 方法取得内部元素的地址后, 再加上偏移量取得 for (size_t i = 0; i < 10; i++) *((int*)sps.get() + i) = 10 - i;

6. C++11 weak_ptr weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作。
【C++|C++ 智能指针】weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。
6.1 weak_ptr的使用
  • use_count():观测资源的引用计数
  • expired():等价于use_count()==0,表示被观测的资源(也就是shared_ptr的管理的资源)已经不复存在。
  • lock():从被观测的shared_ptr获得一个可用的shared_ptr对象,从而操作资源。
    当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr。
#include #include int main() { { std::shared_ptr sh_ptr = std::make_shared(10); std::cout << sh_ptr.use_count() << std::endl; //1std::weak_ptr wp(sh_ptr); std::cout << wp.use_count() << std::endl; //1if(!wp.expired()){//判断wp是否指向对象 std::shared_ptr sh_ptr2 = wp.lock(); //get another shared_ptr *sh_ptr = 100; std::cout << wp.use_count() << std::endl; //2 } } //delete memory }

6.2 利用weak_ptr规避循环引用
如果忘记了循环引用问题,可以到[shared_ptr[(#5)部分回顾,这里主要给出解决方案。
#include #include class CB; class CA; class CA{ public: CA() { } ~CA() { std::cout << "~CA()" << std::endl; }void Register(const std::shared_ptr& sp){ m_sp = sp; } private: std::weak_ptr m_sp; //将这里设置为weak_ptr }; class CB{ public: CB() { }; ~CB() { std::cout << "~CB()" << std::endl; }; void Register(const std::shared_ptr& sp){ m_sp = sp; } private: std::shared_ptr m_sp; }; int main(){ std::shared_ptr spa(new CA()); std::shared_ptr spb(new CB()); spa->Register(spb); spb->Register(spa); std::cout << spb.use_count() << std::endl; //1 std::cout << spa.use_count() << std::endl; //2 }/* 成功调用析构函数,销毁对象 */

    推荐阅读