type
date
status
slug
summary
tags
category
icon
password
网址
C++11智能指针的原理和使用

1.基本概念

1.什么是内存泄漏

内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释 放,导致程序运行速度减慢甚至系统崩溃等严重后果。
内存泄漏分为以下两类:
(1)堆内存泄漏:我们经常说的内存泄漏就是堆内存泄漏,在堆上申请了资源,在结束使用的时候,没有释放归还给OS,从而导致该块内存不会被再次使用
(2)资源泄漏:通常指的是系统资源,比如socket,文件描述符等,因为这些在系统中都是有限制的,如果创建了而不归还,久而久之,就会耗尽资源,导致其他程序不可用。

2.什么是智能指针

C#和Java中都有自动垃圾回收机制,GC 可以管理分配的堆内存,在对象失去引用时自动回收,因此,在 C#和Java中,内存管理不是大问题。 C++语言没有垃圾回收机制,必须自己去释放动态分配的堆内存,否则就会内存泄露。相信大部分C++开发人员都遇到过内存泄露的问题,而查找内存泄露的问题往往要花大量的精力。 实例: 解决这个问题最有效的方法就是使用智能指针(Smart Pointer)。在智能指针对象中有一个指针, 此指针存储的是动态创建对象的地址,用于生存期控制,能够确保智能指针对象离开所在作用域时,自动正确地销毁动态创建的对象,防止内存泄露。 只要我们能够正确使用智能指针就不会担心内存泄露的问题,因为智能指针可以自动释放分配的内存。 智能指针和裸指针的用法类似,只是不需要手动释放内存,而是通过智能指针自己管理内存的释放,这样就不用担心忘记释放内存从而导致内存泄露了。

3.RAII

RAII(Resource Acquisition Is Initialization)是由c++之父Bjarne Stroustrup提出的,中文翻译为"资源获取即初始化",使用局部对象来管理资源的技术称为资源获取即初始化; RAII,它充分的利用了C++语言局部对象自动销毁的特性来控制资源的生命周期
这里的资源主要是指操作系统中有限的东西如内存(heap)、网络套接字,互斥量,文件句柄等等,局部对象是指存储在栈中的对象,它的生命周期是由操作系统来管理的,无需人工介入。
整个RAII过程总结四个步骤:
  1. 设计一个类封装资源
  1. 在构造函数中初始化
  1. 在析构函数中执行销毁操作
  1. 使用时定义一个该类的局部对象
给一个简单的例子来看下局部对象的自动销毁的特性:
从Object类型可以看出,当我们在fun函数中声明一个局部对象的时候,会自动调用构造函数进行对象的初始化,当整个fun函数执行完成后,自动调用析构函数来销毁对象,整个过程无需人工介入,由操作系统自动完成;于是,很自然联想到,当我们在使用资源的时候,在构造函数中进行初始化,在 析构函数中进行销毁。

4.使用裸指针存在的问题

  1. 使用完指针之后无法判断是否应该销毁指针,因为无法判断指针是否“拥有”指向的对象;
  1. 在已经确定需要销毁指针的情况下,也无法确定是用delete关键字删除,还是有其他特殊的销毁机 制,例如通过将指针传入某个特定的销毁函数来销毁指针所指资源;
  1. 即便已经确定了销毁指针的方法,由于1的原因,仍然无法确定到底是用delete(销毁单个对象)还是 delete[] 销毁一组对象;
  1. 假设上述的问题都解决了,也很难保证在代码的所有路径中(分支结构,异常导致的跳转),有且仅 有一次销毁指针操作;任何一条路径遗漏都可能导致内存泄露,而销毁多次则会导致未定义行为;
  1. 理论上没有方法来分辨一个指针是否处于悬挂状态;
C++11提供了4种智能指针∶auto_ptr ,unique_ptr,shared_ptr和weak_ptr; 使用时需要引用头文件。

2.auto_ptr的剖析

在C98 标准中没有右值引用,也没有move,forward 语义。
1.auto_ptr 的源码

2.auto_ptr 使用分析

1 .构造函数与析构函数 auto_ptr在构造时获取对某个对象的所有权(ownership),在析构时释放该对象。 我们可以这样使用auto_ptr来提高代码安全性:
从此我们不必关心应该何时释放Int对象,也不用担心发生异常会有内存泄漏。 2 .辅助函数 1 . get 返回指向被管理对象的指针 2 . 重载 operator * (取值) , operator-> (成员访问) ,访问被管理对象 3 . reset 替换被管理对象 4 . release 释放被管理对象的所有权
3 .拷贝构造与赋值 C98 还没加入右值引用,也没有move语义. auto_ptr 的拷贝构造和赋值重载陷入困境: 1 . 浅拷贝和浅赋值,指针重复释放; 2. 深拷贝和深复制,语义矛盾。最终选择所有权的转移; auto_ptr要求其对“裸”指针的完全占有性。也就是说一个“裸”指针不能同时被两个以上的auto_ptr所拥有。那么,在拷贝构造或赋值操作时,我们必须作特殊的处理来保证这个特性。auto_ptr的做法是“所有权转移”,即拷贝或赋值的源对象将失去对“裸”指针的所有权,所以,与一般拷贝构造函数,赋值函数不同, auto_ptr的拷贝构造函数,赋值函数的参数为引用而不是常引用(const reference)。 值语义(value sematics)指的是对象的拷贝与原对象无关,就像拷贝 int 一样。C++ 的内置类型 (bool/int/double/char)都是值语义,标准库里的 complex<> 、pair<>、vector<>、map<>、string 等 等类型也都是值语意,拷贝之后就与原对象脱离关系。Java 语言的 primitive types 也是值语义。 与值语义对应的是“对象语义/object sematics”,或者叫做引用语义(reference sematics),由于“引用”一词在 C++ 里有特殊含义,所以我在本文中使用“对象语义”这个术语。对象语义指的是面向对象意义下的对象,对象拷贝是禁止的。 当然,一个auto_ptr也不能同时拥有两个以上的“裸”指针,所以,拷贝或赋值的目标对象将先释放 其原来所拥有的对象。
4 .拷贝构造函数和赋值重载的语义不明确 因为一个auto_ptr被拷贝或被赋值后,其已经失去对原对象的所有权,这个时候,对这个auto_ptr的提领(dereference)操作是不安全的。如下: 这种情况较为隐蔽的情形出现在将auto_ptr作为函数参数按值传递,因为在函数调用过程中在函数的作用域中会产生一个局部对象来接收传入的auto_ptr(拷贝构造),这样,传入的实参auto_ptr就失去了其对原对象的所有权,而该对象会在函数退出时被局部auto_ptr删除。如下:
因为这种情况太隐蔽,太容易出错了. 5 .auto_ptr的析构函数的问题 析构函数删除指针用的是delete,而不是delete [],所以auto_ptr不能管理一个数组指针。 6 .总结: auto_ptr 主要有三大问题: 复制和赋值会改变资源的所有权,不符合人的直觉。 在 STL 容器中使用auto_ptr存在重大风险,因为容器内的元素必需支持可复制(copy constructable)和可赋值(assignable)。 不支持对象数组的操作。 C++11中已经舍弃auto_ptr; 3 C++11 中的智能指针 C++11 依据使用场景的不同创建了二个新的智能指针: unique_ptr,shared_ptr和weak_ptr; 1.unique_ptr 分析和使用 unique_ptr: 是“独占” 对象所有权语义的智能指针。 unique_ptr实现了独享被管理对象指针的概念,这意味这它可确保一个对象和其对应的资源同一时间只被一个unique_ptr对象拥有。一旦拥有者被销毁或者变成empty或者开始拥有另一个对象的地址,先前拥有的那个对象就会被销毁,其任何相应资源亦会被释放。
1.特点 :
1 .基于排他所有权模式:两个指针不能指向同一个资源。 2 .由于独占对象的拥有权,所以不提供拷贝构造函数和左值赋值函数重载。 3 .提供移动构造和移动赋值函数。 4 .为了实现单个对象和一组对象的管理,添加了删除器类型。 5 .在容器保存指针是安全。 6 .unique_ptr具有->和运算符重载符,因此它可以像普通指针一样使用。
2 .unique_ptr 源码
1 int func() unique_ptr 实现代码 4 .辅助函数
1 . get 返回指向被管理对象的指针 2 . reset 替换被管理对象 3 . release 释放被管理对象的所有权 4 . swap 交换被管理对象 5 . operator bool() const 检查是否有关联的被管理对象 6 . 重载 operator *, operator-> ,访问被管理对象
5 .不能拷贝构造和左值赋值 unique_ptr 实现代码 6 .可以移动构造和移动赋值
unique_ptr 代码实现
7 .管理动态数组 标准库提供了一个可以管理动态数组的unique_ptr版本。 unique_ptr 删除器仿写
8 .make_unique 函数 代码实现
9 .非heap内存的管理
10 .unique_ptr 不要与hash_map 结合
11 . 使用 unique_ptr总结 unique_ptr在要表达“专属所有权”的语义时使用,即unique_ptr指针永远“拥有”其指向的对象,所 以unique_ptr是一个move-only类型,一个unique_ptr指针是无法被复制的,只能将“所有权”在两个 unique_ptr指针之间转移,转移完成后原来的unique_ptr将被设为null;
1.语义简单,即当你确定使用的指针是不是被共享所有权的时候,认选unique_ptr独占式所有权,当 确定要被共享的时候可以转换成shared_ptr; 2.unique_ptr效率比shared_ptr高,不需要维护引用计数控制块; 3.在需要时可以将unique_pt转换成shared_ptr。
unique_ptr的场景:
1 .对象内部使用 实例: 2 .函数中使用
void func() 函数结束,Int 对象自动删除
3 .unique_ptr 和工厂方法模式
  1. 传统的实现方式
  1. 使用unique_ptr
 
4 . 对象的构造通过特殊的函数构造时,unique_ptr 使用。
智能指针是比原始指针更智能的类,解决悬空(dangling)指针或多次删除被指向对象,以及资源 泄露问题,通常用来确保指针的寿命和其指向对象的寿命一致。智能指针虽然很智能,但容易被误用, 智能也是有代价的。 2.shared_ptr 分析和使用 做出一个象Java中垃圾回收器,并且可以运用到所有资源,heap内存和系统资源都可以使用的系 统。 std::shared_ptr 就是C++11为了达到上述目标推出的方式。 shared_ptr实现了共享所有权(shared ownership)方式来管理资源对象,这意味没有一个特定的 std::shared_ptr 拥有资源对象。相反,这些指向同一个资源对象的 std::shared_ptr 相互协作来确保该 资源对象在不需要的时候被析构。 1.特点 :
1 .基于共享所有权模式:多个指针能够同时指向同一个资源。 2 . 基于共享所有权,使用引用计数控制块管理资源对象的生命期。 3 .提供拷贝构造函数和赋值重载函数; 提供移动构造和移动赋值函数。 4 .为了实现单个对象和一组对象的管理,添加了删除器类型。 5 .在容器保存shared_ptr对象是安全。 6 .shared_ptr重载了operator->和operator*运算符,因此它可以像普通指针一样使用。
2 .引用计数器的作用:
1、当新的 shared_ptr 对象与资源对象的地址关联时,则在其构造函数中,将与此资源对象关联的 引用计数增加1。 2、当任何 shared_ptr 对象超出作用域时,则在其析构函数中,将与资源对象关联的引用计数减1。 如果引用计数变为0,则表示没有其他 shared_ptr 对象与此资源对象关联,在这种情况下,它使用 deleter删除器删除该资源对象。
3 . 创建shared_ptr实例: 最安全和高效的方法是调用make_shared库函数,该函数会在堆中分配一个对象并初始化,最后返 回指向此对象的share_ptr实例。如果你不想使用make_shared,也可以先明确new出一个对象,然后 把其原始指针传递给share_ptr的构造函数。 示例如下: 4 . 引用计数
从上面这段代码中,我们对shared_ptr指针有了一些直观的了解。 一方面,跟STL中大多数容器类型一样,shared_ptr也是模板类,因此在创建shared_ptr时需要指 定其指向的类型。另一方面,正如其名一样,shared_ptr指针允许让多个该类型的指针共享同一堆分配 对象。同时shared_ptr使用经典的“引用计数”方法来管理对象资源,每个shared_ptr对象关联一个共享 的引用计数。 对于shared_ptr在拷贝和赋值时的行为是,每个shared_ptr都有一个关联的计数值,通常称为引用计 数。无论何时我们拷贝一个shared_ptr,计数器都会递增。 例如,当用一个shared_ptr初始化另一个shred_ptr,或将它当做参数传递给一个函数以及作为函数 的返回值时,它所关联的计数器就会递增。 当我们给shared_ptr赋予一个新值或是shared_ptr被销毁(例如一个局部的 shared_ptr离开其作用 域)时,计数器就会递减。 shared_ptr对象的计数器变为0,它就会自动释放自己所管理的对象。 对比我们上面的代码可以看到:当我们将一个指向Int对象的指针交给pInta管理后,其关联的引用计 数为1。接下来,我们用pInta初始化pIntb,两者关联的引用计数值增加为2。随后,函数结束,pInta 和pIntb相继离开函数作用域,相应的引用计数值分别自减1最后变为0,于是Object对象被自动释放 (调用其析构函数)。 5 .shared_ptr 代码仿写
6 .辅助函数
1 . get 返回指向被管理对象的指针,不要使用。 2 . reset 替换被管理对象。 3 . shared_ptr 不提供 release 释放被管理对象的所有权的函数。 4 . swap 交换被管理对象 5 . operator bool() const 检查是否有关联的管理对象 6 . 重载 operator *, operator-> ,访问被管理对象 7 . bool unique() const 函数 检查所管理对象是否仅由当前 shared_ptr 的实例管理
C++20移除
7 .可以移动构造和移动赋值
实现函数 8 .管理动态数组 9 . sharep_ptr 与容器的结合 示例1:
示例4: 问题:
10.多态与std::shared_ptr
1 . 上行转换
// 上行转换 void func(std::shared_ptr<Animal> pa) { cout << typeid(pa).name() << endl; pa->eat(); pa->talk(); pa->walk(); pa->PrintInfo(); } int main() { std::shared_ptr<Cat> pc(new Cat("yhping", "bsm")); std::shared_ptr<Dog> pd(new Dog("yhp", "hsq")); func(pc); func(pd); return 0; } int main() { std::shared_ptr<Cat> pc(new Cat("yhping", "bsm")); std::shared_ptr<Dog> pd(new Dog("yhp", "hsq")); // func(pc); // func(pd); func(std::static_pointer_cast<Animal>(pc)); func(std::static_pointer_cast<Animal>(pd)); return 0; } 2 .下行转换 11 工厂方法模式与shared_ptr
12 . shared_ptr与hash_map 结合
3.weak_ptr 分析和使用 弱引用指针 weak_ptr是用来监视 shared_ptr的生命周期,是shared_ptr的一个助手。weak_ptr没 有重载操作符和->,因为它不与shared_ptr 共享指针,不能操作资源,主要是通过shared_ptr获得资 源的监测权,它的构造不会增加引用计数,它的析构也不会减少引用计数,纯粹只是作为一个旁观者来 监视shared_ptr中管理的资源是否存在。weak_ptr还可以用来返回 this 指针和解决循环引用的问题。 1 . weak_ptr 构建 当我们创建一个weak_ptr时,需要用一个 shared_ptr 实例来初始化 weak_ptr,由于是弱共享, weak_ptr 的创建并不会影响 shared_ptr 的引用计数值。可以通过use_count() 方法来获得当前观测资 源的引用计数。 2 .通过 expired()方法来判断所观测的资源是否已经被释放。 3 . 通过lock方法来获取所监视的shared_ptr。 weak_ptr并没有重载 operator->和 operator 操作符,因此不可直接通过weak_ptr使用对象,典 型的用法是调用其lock函数来获得shared_ptr实例,进而访问对象。
4 . 理解weak_ptr 工作原理 4 . shared_ptr 和 weak_ptr 的仿写 类图
1.shared_ptr 的仿写 1 .删除器和类型声明 2 . My_Ref_count_base 看到 My_Ref_count_base类是一个抽象类: 1 .有两个计数器变量: _Uses 供shared指针计数用, _Weaks 供weak指针计数用; 2 .保证线程安全: _Uses 和 _Weaks 加1减1 使用原子操作; 3 . 如果_Uses 值为0,删除资源对象;
4 . 如果 _Weaks 的值为0 , 删除引用计数器对象,也就是删除自己; 5 . 而个纯虚函数: _Destroy() : 删除资源对象; _Delete_this() : 删除引用计数器对象本身; 3 .My_Ref_count类型 无删除器的引用计数器类型; My_Ref_count_base的第一个派生类是My_Ref_count,它是能够实例化对象的引用计数器类型; 1 . 数据成员 _Ptr,指向资源对象的地址。
  1. 有两个指针成员:分别是 _Ptr 和 _Rep ,类型分别是element_type * 和 My_Ref_count_base*;
  1. _Ptr指向资源对象的地址, _Rep指向引用计数器类型对象的指针;
  1. My_shared_ptr模板类没有自己的数据成员,而是只提供了一些供调用的接口;
  1. 版本不同的构造函数,对应不同实参来构造My_shared_ptr 对象;
  1. 辅助函数:
  1. get 返回指向被管理对象的指针,不要使用。
  1. reset 替换被管理对象。
  1. shared_ptr 不提供 release 释放被管理对象的所有权的函数。
  1. swap 交换被管理对象 。
  1. operator bool() const 检查是否有关联的管理对象
  1. 重载 operator *, operator-> ,访问被管理对象
  1. bool unique() const 函数 检查所管理对象是否仅由当前 shared_ptr 的实例管理,