初识C++之智能指针

7 篇文章 0 订阅

目录

一、智能指针的概念

二、RAII

三、 智能指针的拷贝构造

1. 智能指针的拷贝构造问题

2. C++库中的智能指针

2.1 auto_ptr

2.2 unique_ptr

2.3 shared_pt

2.4 weak_ptr

四、shared_ptr的循环引用问题

五、 定制删除器


一、智能指针的概念

在了解智能指针的概念前,先写出如下程序:

在这个程序里面,Div函数会抛出一个异常,func函数会捕获这个异常并将这个异常继续抛出给main函数。注意,在这个程序里面的func函数new了一个空间,在catch中释放了这块空间。此时只new了一块空间。

但是,如果在这个程序中的func函数中再new一个空间。此时就需要释放两个空间。有人可能会认为多开一块空间没什么,只需要在catch中新增一条delete语句即可:

但是,这里大家忽略了一个问题,那就是new其实也是可能抛异常的。比如当new的空间过大,内存无法满足的情况下, new就会抛出一个异常。因此,在新创建了一个空间后,也需要对这块空间进行捕获:

这种写法首先看起来很难看。并且最重要的是,由于new可能抛异常,所以如果再增加一个p3,就需要再套一层try catch。随着new的空间越多,就需要套越多层的try catch。这种写法无疑是非常麻烦且难看的。

面对这种情况时,就可以使用智能指针。提供如下一个类:

 有了这个类后,就可以不再需要在捕获中释放空间了。修改func函数:

在这里,将p1和p2交给SmartPtr对象,让这个类中的_ptr指向对应的空间。这两个类在这里属于临时变量,一旦出了作用域就会结束。因此,当Div函数出现异常时,它会直接跳转到main函数的catch中,此时func函数的生命周期结束,也就带着这两个SmartPtr对象销毁了。而这两个对象在销毁时会调用析构函数用delete销毁new出来的空间

通过上面这种方式,就可以“将new出来的空间的生命周期与一个局部对象的生命周期相绑定”,实现自动释放的功能。也就无需再为了防止Div函数出错而使用catch捕获异常以便于在catch中释放对应的空间。

当然,为了方便,也可以将func中的代码简写:

既然这个类叫做只能指针,当然也需要有像指针一样的功能,所以在类中添加如下内容:

然后写下测试代码并运行测试:

运行正常。

上面的这种将资源生命周期与对象生命周期相绑定的方法,其实就是RAII。总结起来,智能指针的原理其实就是使用RAII的特性和重载了*与->,具有像指针一样的行为

二、RAII

RAII(Resource Acquisition is Initialization),翻译过来就是“资源申请即初始化”。就是一种“利用对象声明周期来控制程序资源”(如内存、文件句柄、网络连接、互斥量等)的简单技术。

简单来讲,RAII就是“在对象构造时获取资源”,控制资源的访问并使之在这个对象的声明周期内始终保持有效。最后“在对象析构时释放资源”。因此,这种技术实际上就是将一份资源交给了一个对象管理。

这种做法有两个好处:

(1)不需要显式地释放资源。因为创建出来的对象都是局部对象,出了作用域自动调用析构函数销毁,也就将管理的资源一并销毁了。

(2)这种方式,将申请的空间的生命周期与对象的生命周期绑定,便于用户更好的管理资源。

三、 智能指针的拷贝构造

1. 智能指针的拷贝构造问题

现在有如下一个我们自己写的智能指针:

写出如代码:

运行上面的代码:

此时就出现了报错。原因很简单。在这个智能指针的类中并没有写拷贝构造,因此编译器会自动生成一个进行浅拷贝的拷贝构造函数,此时sp1和sp2指向同一块空间,在程序结束析构的时候就析构了两次,导致程序错误。

其实智能指针中的RAII和“像指针一样”这两个特性都是非常简单的,并没有什么难度。智能指针真正的问题之一,就在于这个拷贝构造上。

智能指针的行为其实就是在模拟原生指针的行为,所以这里的拷贝构造其实就是要让两个指针指向同一个位置

2. C++库中的智能指针

2.1 auto_ptr

在C++98中,其实就已经提出了智能指针的概念。第一个智能指针是“auto_ptr”

 C++98中的auto_ptr针对拷贝构造提出的解决方案就是“资源管理权转移”。简单来讲就是在一个智能指针在拷贝另一个智能指针后,会将原指针指针的资源转移给新的智能指针,然后将自己置空。写如下代码进行测试:

运行该程序:

可以看到,sp1被置为了空,而sp1的资源被转移到了sp2中。这个智能指针的解决方案有一个很大的问题,就是会导致“悬空”问题:

例如如上代码,如果是一个不清楚这个特性,或者说在使用时没注意到这个问题的人就可能错误的使用sp1这个已经被置空的指针,进而出现错误。

如果我们想实现这一特性,也非常的简单,就是将资源交换然后将原指针置空即可:

一般来讲,在实际使用中是非常不推荐使用auto_ptr的

2.2 unique_ptr

unique_ptr其实是C++委员会从boost库中抄来的,包括下面的shared_ptr和weak_ptr也是如此。boost库可以看做C++标准库的一个预备库,是由C++委员会发起建立的,里面的很多内容在未来都可能进入C++标准库。

unique_ptr解决拷贝构造的方法就很粗暴,从名字“唯一指针”上就可以看出来,它的解决方案就是“禁止拷贝构造”。写入如下代码进行测试:

运行该程序后,可以看到如上报错。表示使用了已经删除的函数。这就可以证明,unique_ptr其实就是禁止了对智能指针的拷贝构造。实现方式也很简单,直接使用delete关键字即可:

带有这个关键字的类中的默认成员函数会被禁止生成和使用。

2.3 shared_pt

上面的unique_ptr是禁止拷贝,但如果我们就是想让两个不同的指针指针指向同一块空间呢?此时就可以使用shared_ptr。从名字“共享指针”就可以看出来,这个智能指针是允许不同的智能指针指向同一块空间的。写出如下代码测试:

 运行程序查看监视窗口:

 可以看到,sp1和sp2指向的是同一块空间。

shared_ptr对拷贝构造的解决方案就是“计数器”

shared_ptr通过计数器的方式记录某块空间有几个智能指针指向,每多一个就增加计数器,在析构时,先--计数器,如果计数器不为0,则不释放空间;如果计数器为0,则释放空间。

那么如何实现这个计数器呢?有的人可能就想,既然要让不同的智能指针看到同一块空间,就可以定义一个静态成员变量,这样就可以解决问题:

但是要知道,static成员是整个类(类所实例化的所有对象)共享的。这也就是说,确实指向同一块空间的智能指针能看到同一块空间。但是,指向不同空间的智能指针也是看到的同一个计数器。如果出现有三个智能指针指向同一块空间,此时计数器为3;但是此时又出现一个智能指针指向其他空间,由于看到的是同一个计数器,所以此时计数器++,变为4。很明显不满足需要。

因此,智能指针的计数器必须让指向同一个空间的智能指针看到同一个计数器;指向不同空间的智能指针看到不同的计数器。

要实现这一方法也很简单。首先定义一个计数器变量,在构造函数中单独为这个计数器new一块空间。此时这个变量的值就存在于堆上。不会因为某个对象结束而被销毁。当要进行拷贝构造时,首先++被拷贝对象的计数器。再让要拷贝的对象的计数器指向被拷贝的计数器,此时它们看到的就是同一个计数器。实现起来也非常简单:

实现了拷贝构造后,再来实现赋值。如果是指向同一块空间的指针赋值,就什么都不需要做。但如果是指向不同空间的指针赋值,首先就需要--原智能指针的计数器;如果计数器为0,还需要释放空间。如果不为0,就要将被赋值的智能指针指向的空间和计数器指向赋值的智能指针,最后再++被赋值智能指针的计数器: 

要实现起来,就比拷贝构造复杂一点:

2.4 weak_ptr

weak_ptr并不是单独使用的,它需要配合shared_ptr,主要用于解决shared_ptr的循环引用问题。这个智能指针主要用于提供对shared_ptr的拷贝构造,甚至不允许带参构造:

要实现起来也是比较简单的:

至于这个weak_ptr如何解决循环引用的问题, 就放在下面讲。

四、shared_ptr的循环引用问题

shared_ptr是一个支持多个智能指针指向同一块空间的类。这个智能指针的多方面都很好用,但有一个很严重的问题存在,就是“循环引用”问题。

写出如下程序:

该程序可以看成一个简化版的链表,每个数据块中只有两个链接上下数据块的节点。创建两个节点,让这两个节点链接起来。运行程序:

此时可以发现,当这个程序结束后,什么都没有打印。但是我们自己写的析构函数中是加了一句话的。既然这里没有打印,也就说明在这个程序结束后,没有调用析构函数释放空间。

我们屏蔽掉一个节点指向后再运行程序:

可以看到,当对一个节点指向屏蔽后,就可以正常调用析构函数了。但是,如果是向上面那样两个节点互相指向,却无法析构。

上面的代码中所用的是我们自己写的shared_ptr,那么这是不是我们自己写的代码有问题呢?换成库中的shared_ptr试试:

要注意,库中的构造函数是加了explicit关键字的,禁止隐式类型转换。所以这里不能使用=创建n1和n2。运行该程序:

可以看到,在两个节点互相指向的情况下,库中shared_ptr也无能为力,无法调用析构函数。同样的,隐藏一个节点指向后运行程序:

同样的,此时又可以正常调用析构函数了。

这种节点互相指向导致无法析构的情况,就叫做“循环引用”问题。

原理很简单,假设有n1和n2两个节点,这两个节点互相指向。而shared_ptr中是存在计数器的,这也就意味着当这两个节点互相指向的时候,n1和n2的计数器都会++变为2。当要析构时,首先析构n2,将n2的计数器--为1,但是此时并没有释放空间,因为n1中还有一个shared_ptr,即_next指向n2;于是接着释放n1,--n1的计数器为1,此时n1也没有释放,因为n2中的有一个shared_ptr,即_prev指向n1。此时就会出现要释放n1,就必须释放n2中的_prev;要释放n2,就要释放n1中的_next的情况。两个节点互相等待对方的释放,导致双方都无法释放。

那么如何解决这个问题呢?很简单,只需要在指向空间时不要++计数器即可。但是shared_ptr是无法自行做到这件事的,所以,库中便提供了weak_ptr来专门处理这种情况:

 修改程序如下:

可以看到,此时依然是存在两个指针互相指向的情况。运行该程序:

可以看到,程序可以正常析构。

至于这个weak_ptr如何模拟实现,在上文中已经讲解过,这里就不再赘述。换成我们自己写的weak_ptr来测试程序:

同样可以正常析构。当然,库中的实现还考虑了很多问题,实现的复杂程度要比我们自己实现的复杂的多,但单个智能指针的实现思想是一样的。

五、 定制删除器

大家知道,在C++中提供了new来申请空间。而new申请空间时,有两种申请方式。一种是不带[]申请,只有一块固定空间;带[],则可以指定申请对应大小的空间。这两种空间的删除方式并不一样。错误使用可能会带来严重后果

如果是内置类型,使用错误的删除方式可能还没有问题:

但如果是自定义类型,使用错误的方式就可能出现问题:

此时就有一个问题了,在智能指针中,如何得知应该使用哪种方式释放空间呢?

此时,就需要使用定制删除器,指定释放空间的方式。

库中的shared_ptr中也是有定制删除器的,其实就是提供仿函数:

例如下图:

在这里,不仅可以正常传仿函数,也可以传lambda表达式。这里大家可能就会比较奇怪了,在以前传仿函数时,都是在类型名处传仿函数名,为什么库中的却是在构造对象的地方,即构造函数中传可调用对象呢?这其实就和C++库中shared_ptr的底层实现有关。

在这里,我们是无法实现像库中这样实现定制删除器的。因为库中其实套了很多个类,通过这些类的嵌套来实现让shared_ptr拿到这个可调用对象。如果单单对构造函数进行修改是没有用的:

在构造函数中单独加一个模板,虽然可以将函数对象传进去,但是要知道,在这里我们并不是要让构造函数使用这个删除器,而是要让shared_ptr使用这个删除器。更准确点,是让shared_ptr的析构函数使用。如果单单给类中的构造函数加一个参数模板,如何让整个类拿到呢?很明显,是无法实现的。库中为了实现这一方法,就采用了多个类的嵌套实现这一操作。实现起来是非常复杂的,这里就不过多讲解。

但是我们要使用定制删除器也是有方法的,那就是给整个类加上一个参数模板即可。

通过传仿函数的方式,就可以让智能指针内部拿到对应的释放空间的方式。但是这种方式有一个缺点,那就是无法使用lambda表达式

因为lambda表达式是一个可调用对象,但是新增参数模板的方式是要在模板中填入类型,所以无法使用lambda表达式。有人可能就会想到使用decltype来声明这个表达式是一个类型,同样是无效的。因为decltype是运行时推导,而这里传入的类型是要在编译时就传入,所以decltype失效

当然,不仅shared_ptr是这样,unique_ptr其实也是一样的,都是经过多个类的嵌套实现了在构造函数中传入释放资源的方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
目录 第1篇初级篇 第1章 初识C++ 1.1 c++简介 1.2 C++与C的区别 1.3 学习c++之前需要先学C吗 1.4 c++与其他语言的区别 1.5 c++的版本以及安装问题 第2章 做一个最简短的C4-+程序 2.1 简单的屏幕输出小程序 2.2 输出语句的使用 2.3 std::介绍 2.4 iostream与iostream.h的区别 2.5 重名问题 2.6 注释 2.7 总结 第3章 初步了解函数 3.1 一个简单的函数 3.2 函数的传参 3.3 函数的返回值、参数与变量 3.4.函数的声明与定义 3.5 局部变量 3.6 全局变量 3.7 总结 第4章 C4-+数据类型 4.1 变量的定义 4.2 将变量及数据存储在内存中 4.3 布尔型变量 4.4 字符型变量 4.5 wchart双字符型变量 4.6 整型概述 4.7 整型变量的定义 4.8 浮点型变量 4.9 常量 4.10枚举型常量 第5章 if语句与运算符 5.1 语句的定义 5.2 块的定义 5.3 表达式的定义 5.4 运算符的定义 5.4.1 赋值运算符的定义 5.4.2 数学运算符的定义 5.4.3 赋值运算符与数学运算符的联合 5.5 自加与自减 5.5.1 前置 5.5.2 后置 5.6 表达式的优先级 5.7 关系运算符 5.8 if语句 5.8.1 else语句 5.8.2 elseif语句 5.8.3 if语句的嵌套 5.9 逻辑运算符及其使用 5.9.1 逻辑“与” 5.9.2 逻辑“或” 5.9.3 逻辑“非” 5.9.4 逻辑运算符的优先级 5.9.5 运算式的真假关系 5.1 0三目运算符 5.1 0.1 三目运算符的优先问题 5.1 0.2 三目运算符的使用问题 5.1 0.3 三目运算符的型别问题 5.1 0.4 三目运算符在字符型变量中的应用 5.1 1复杂嵌套的if语句 5.1 2总结 第6章 面向对象 6.1 面向对象程序语言的主要特征 6.2 类、对象和成员 6.3 类、对象和成员的使用方法及区别 6.3.1 声明一个类 6.3.2 命名习惯 6.3.3 定义一个对象 6.3.4 类与对象的区别 6.3.5 对象与成员的关系 6.3.6 不要给类赋值 6.3.7 对象只能调用类中存在的方法 6.4 公有 6.5 私有 6.6 成员函数的声明和定义 6.7 内联函数 6.7.1 普通内联函数 6.7.2 内联成员函数 6.8 头文件与源文件 6.9 const成员函数 6.10构造函数 6.11默认构造函数 6.12析构函数 6.13析构对象数组 6.14总结 第7章 循环语句 7.1 循环语句的前身——goto语句 7.2 慎用goto语句 7.3 while语句 7.3.1 带运算符的while语句 7.3.2 以字符为条件的while语句 7.3.3 限定while循环的次数 7.3.4 continue语句 7.3.5 break语句 7.3.6 永不休止的while循环 7.4. do……while循环 7.5 for循环 7.5.1 灵活的for循环 7.5.2 条件为空的for循环 7.5.3 执行为空的for循环 7.5.4 嵌套的for循环 7.6 switch语句 7.6.1 switch语句常见错误 7.6.2 switch的菜单功能 7.7 总结 第8章 指针 8.1 什么是地址 8.2 用指针来保存地址 8.2.1 空指针 8.2.2 指针与变量类型 8.2.3 用指针来访问值 8.2.4 指针地址、指针保存的地址和 该地址的值 8.2.5 指针对数值的操作 8.2.6 更换指针保存的地址 8.3 为什么使用指针 8.3.1 栈和堆 8.3.2 用指针创建堆中空间 8.3.3 用指针删除堆中空间 8.4 动态内存 8.4.1 内存泄漏 8.4.2 在堆中创建对象 8.4.3 在堆中删除对象 8.4.4 访问堆中的数据成员 8.4..5 在构造函数中开辟内存空间 8.4.6 对象在栈与堆中的不同 8.5 this指针 8.6 指针的常见错误 8.7 指针运算 8.7.1 指针的加减运算 8.7.2 指针的赋值运算 8.7 _3指针的相减运算 8.7.4 指针的比较运算 8.8 指针 8.8.1 常量指针 8.8.2 指向常量的指针 8.8.3 指向常量的常指针 8.9 总结 第9章 引用 9.1 什么是引用 9.1.1 引用的地址 9.1.2 引用就是别名常量 9.1.3 引用对象 9.1 4空引用 9.2 函数的参数传递 9.2.1 按值传递 9.2.2 按址传递 9.2.3 按别名传递 9.2.4 让函数返回多个值 9.3 传递对象 9.3.1 按值来传递对象 9.3.2 利用指针来传递对象 9.3.3 利用cost指针来传递对象 9.3.4 利用引用来传递对象 9.3.5 到底是使用引用还是指针 9.3.6 引用和指针可以一块用 9.4 引用应注意的问题 9.4.1 引用容易犯的错误 9.4.2 引用一个按值返回的堆中对象 9.4 -3引用一个按别名返回的堆中对象 9.4.4 在哪里创建,就在哪里释放 9.5 总结 第10章 深入函数 10.1 函数重载 10.1.1 普通函数的重载 10.1.2 成员函数的重载 10.2 函数的默认参数 10.3 重载构造函数 10.3.1 成员变量的赋值与初始化 10.3.2 成员变量的初始化与构造函数 10.3.3 复制构造函数 10.3.4 构造函数和new运算符 10.3.5 再谈默认构造函数 10.4.析构函数和delete运算符 10.4..1 默认析构函数 10.4.2 调用构造函数进行类型转换 10.5 浅层复制构造函数 10.6 深层复制构造函数 第11章 运算符重载 11.1 运算符重载 11.2 在成员函数中实现自加 11.3 重载前置自加运算符 11.4 创建临时对象 11.5 创建无名临时对象 11.6 取消创建临时对象 11.7 重载后置自加运算符 11.8 重载加法运算符函数operator+ 11.9 重载赋值运算符函数operator 11.10转换类型运算符 11.10.1 温习调用构造函数实现的类型转换 11.10.2 通过构造函数将变量转换为一个对象的成员变量 11.10.3 通过operator关键字进行转换 11.11什么可以重载,什么不可以重载 第12章 继承 12.1 什么是继承和派生 12.1.1 复杂的继承和派生 12.1.2 继承和派生如何在C++中实现 12.1.3 继承的种类及语法 12.1.4 单一继承 12.2 公有型、保护型和私有型 12.3 访问权限 12.4 多重继承 12.5 继承的构造与析构 12.6 合理利用基类构造函数 12.7 继承和重载的两义性问题 12.7.1 多重继承容易产生两义性 12.7.2 继承中的重载 12.7.3 两义性的归属问题 12.7.4 减少两义性产生的混淆问题 12.7.5 虚基类不会产生两义性 12.8 总结 第13章 虚函数 13.1 指向子对象的父指针 13.2 虚函数 13.3 拳击游戏 13.4 继承是否可以实现多态性 13.5 在编译时的静态联编 13.6 在运行时的静态联编 13.7 在运行时的动态联编 13.8 在编译时的动态联编 13.9 调用虚函数 13.9.1 在虚函数中调用成员函数 13.9.2 3种调用虚函数的方式比较 13.10被继承的虚函数仍然是虚函数 13.11系统是如何调用虚函数的 13.12在虚函数中使用成员名限定 13.13虚析构函数 13.14总结 第14章 数组 14.1 数组的基本用法 14.1.1 什么是数组 14.1.2数组元素 14.1.3数组下标越界 14.1.4 倒序输出 14.1.5 将数组的下标定义为常量 14.1.6 手动操作数组元素 14.1.7 数组的初始化 14.2 数组的用途 14.2.1 求平均考试成绩 14.2.2 兔子繁殖问题 14.2.3 数字排序问题 14.3 数组在内存中的分布 14.4.输出数组名 14.5 数组名与函数 14.6 传递与接收 14.7 数组与函数 14.7.1 函数传参实例一——求数组所有元素的和 14.7.2 函数传参实例二——用递增法查找数据 14.7.3 函数传参实例三——用二分算法查找数据 14.7.4 函数传参实例四——判断数组是否按照顺序排列 14.7.5 函数传参实例五——判断数组排列方式后执行不同的函数 14.8 数组在对象中的传参 14.9 对象数组 14.10 在对象数组中初始化成员变量 14.11 指针数组 14.12 枚举常量与数组 14.13 多维数组 14.14 多维数组的初始化 14.15 字符数组 14.16 重载数组下标操作符 14.17 总结 第15章 链表 15.1 声明链表结构 15.2 简单的图书链表 15.2.1 图书链表 15.2.2 类的链表 15.3 动态链表 15.3.1 动态链表的建立 15.3.2 解决输入字符造成死循环的问题 15.3.3 动态链表的显示 15.3.4 动态链表的删除 第16章 多态性 第17章 类的特殊成员 第2篇 高级篇 第19章 代码重用 第20篇 高级篇 第20章 友元类与嵌套类 第21章 流 第22章 命名空间 第23章 模板 第24章 异常和错误处理 第25章 补充知识 附录A ASCII码对照表 附录B C++的关键字 附录C C++常用头文件列表 附录D 运算符的优先级 后记
易学 C++ PDF 及源代码,原书配套,很全。 PDF高清有目录 第一篇 过程化的程序设计   第1章 良好的学习开端 1   1.1 软件与程序 1   1.2 程序设计要做什么 1   1.3 选好一种语言 2   1.4 C++能够做些什么 2   1.5 C语言、C++和VisualC++的关系 2   1.6 学习程序设计的方法和必要准备 3   1.7 总结 3   第2章 Hello,World 4   第3章 各种各样的“箱子”——变量 12   第4章 要走哪条路——条件语句 20   第5章 有个圈儿的程序——循环语句 36   第6章 好用的“工具”——函数 51   第7章 好大的“仓库”——数组 69   第8章 内存里的快捷方式——指针 84   第9章 自己设计的箱子——枚举和结构 98   第二篇 实战程序设计   第10章 高效阅读程序代码 119   第11章 调试程序代码技巧 127   第12章 编写程序技巧 150   第三篇 面向对象的程序设计   第13章 初识对象 163   第14章 再识对象 169   第15章 造物者与毁灭者——对象生灭 178   第16章 共有财产•好朋友•操作符 206   第17章父与子——继承 228   第18章 再谈输入与输出 273   第19章 万用的模板 285   第20章 异常的处理 297   附录A 常用保留字列表 305   附录B 常见编译错误和解决方法 307   附录C 参考答案 310   附录D 参考文献 356   ……

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值