1. 设计思想

智能指针是行为类似于指针的类对象,但这种对象还有其他功能。首先,看下面的函数:

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;
}

2. 使用智能指针

  STL一共给我们提供了四种智能指针:auto_ptr、unique_ptr、shared_ptr和weak_ptr(本文暂不讨论)。模板auto_ptr是C++98提供的解决方案,C+11已将将其摒弃,并提供了另外两种解决方案。然而,虽然auto_ptr被摒弃,但它已使用了好多年;同时,如果您的编译器不支持其他两种解决力案,auto_ptr将是唯一的选择。
要创建智能指针对象,

  • 必须包含头文件memory,其中,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运算符用于非堆内存,这是错误的。

3. 为何摒弃auto_ptr?

先来看下面的赋值语句:

auto_ptr< string> ps (new string ("I reigned lonely as a cloud.”);
auto_ptr<string> vocation;
vocation = ps;

上述赋值语句将完成什么工作呢?如果ps和vocation是常规指针,则两个指针将指向同一个string对象。但对于智能指针来说,这是不能接受的,因为程序将试图删除同一个对象两次——一次是ps过期时,另一次是vocation过期时。要避免这种问题,方法有多种:

  • 定义陚值运算符,使之执行深复制。这样两个指针将指向不同的对象,其中的一个对象是另一个对象的副本,缺点是浪费空间,所以智能指针都未采用此方案。
  • 建立所有权(ownership)概念。对于特定的对象,只能有一个智能指针可拥有,这样只有拥有对象的智能指针的构造函数会删除该对象。然后让赋值操作转让所有权。这就是用于auto_ptr和unique_ptr 的策略,但unique_ptr的策略更严格。
  • 创建更智能的指针,跟踪引用特定对象的智能指针数。这称为引用计数。例如,赋值时,计数将加1,而指针过期时,计数将减1。当减为0时才调用delete。这是shared_ptr采用的策略。

当然,同样的策略也适用于复制构造函数。
每种方法都有其用途。下面是不适合使用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:编译出错,与auto_ptr一样,unique_ptr也采用所有权模型,但在使用unique_ptr时,程序不会等到运行阶段崩溃,而在编译器因下述代码行出现错误:
unique_ptr<string> pwin;
pwin = films[2]; // films[2] loses ownership.
  • 使用shared_ptr:运行正常,因为shared_ptr采用引用计数,pwin和films[2]都指向同一块内存,在释放空间时因为事先要判断引用计数值的大小因此不会出现多次删除一个对象的错误。

错误的使用auto_ptr可能导致问题(这种代码的行为是不确定的,其行为可能随系统而异)。因此为了避免潜在的程序崩溃,要摒弃auto_ptr。

4. unique_ptr为何优于auto_ptr

  • unique_ptr比auto_ptr更安全,编译阶段错误比潜在的程序崩溃更安全
  • 相比于auto_ptr,unique_ptr还有一个可用于数组的变体。
std::unique_ptr<double> pda(new double(5));//will use delete[]
  new/delete new[]/delete[]
auto_ptr  
unique_ptr
shared_ptr  
  • 当程序试图将一个 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

其中#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;

5. 如何选择智能指针?

应使用哪种智能指针呢?
(1)如果程序要使用多个指向同一个对象的指针,应选择shared_ptr。这样的情况包括:

  • 有一个指针数组,并使用一些辅助指针来标示特定的元素,如最大的元素和最小的元素
  • 两个对象包含都指向第三个对象的指针
  • STL容器包含指针

很多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节 智能指针模板类

C++笔记(11) 智能指针的更多相关文章

  1. c++11 智能指针 unique_ptr、shared_ptr与weak_ptr

    c++11 智能指针 unique_ptr.shared_ptr与weak_ptr C++11中有unique_ptr.shared_ptr与weak_ptr等智能指针(smart pointer), ...

  2. C++11——智能指针

    1. 介绍 一般一个程序在内存中可以大体划分为三部分——静态内存(局部的static对象.类static数据成员以及所有定义在函数或者类之外的变量).栈内存(保存和定义在函数或者类内部的变量)和动态内 ...

  3. C++11智能指针之std::unique_ptr

    C++11智能指针之std::unique_ptr   uniqut_ptr是一种对资源具有排他性拥有权的智能指针,即一个对象资源只能同时被一个unique_ptr指向. 一.初始化方式 通过new云 ...

  4. 【C++11新特性】 C++11智能指针之weak_ptr

    如题,我们今天要讲的是C++11引入的三种智能指针中的最后一个:weak_ptr.在学习weak_ptr之前最好对shared_ptr有所了解.如果你还不知道shared_ptr是何物,可以看看我的另 ...

  5. 详解C++11智能指针

    前言 C++里面的四个智能指针: auto_ptr, unique_ptr,shared_ptr, weak_ptr 其中后三个是C++11支持,并且第一个已经被C++11弃用. C++11智能指针介 ...

  6. C++11智能指针读书笔记;

    智能指针是一个类对象,而非一个指针对象. 原始指针:通过new建立的*指针 智能指针:通过智能指针关键字(unique_ptr, shared_ptr ,weak_ptr)建立的指针 它的一种通用实现 ...

  7. C++11 智能指针

    C++ 11标准库引入了几种智能指针 unique_ptr shared_ptr weak_ptr C++内存管理机制是当一个变量或对象从作用域过期的时候就会从内存中将他干掉.但是如果变量只是一个指针 ...

  8. C++11智能指针

    今晚跟同学谈了一下智能指针,突然想要看一下C++11的智能指针的实现,因此下了这篇博文. 以下代码出自于VS2012 <memory> template<class _Ty> ...

  9. C++11智能指针的深度理解

    平时习惯使用cocos2d-x的Ref内存模式,回过头来在控制台项目中觉得c++的智能指针有点生疏,于是便重温一下.首先有请c++智能指针们登场: std::auto_ptr.std::unique_ ...

随机推荐

  1. python + mysql 实现表删除数据

    实例如下: import pymysqldef Delete_From(): #打开数据库链接 db = pymysql.connect("localhost","roo ...

  2. Leetcode8. 字符串转换整数 (atoi)

    > 简洁易懂讲清原理,讲不清你来打我~ 输入字符串,输出整数![在这里插入图片描述](https://img-blog.csdnimg.cn/4feb56d86fca437a98f1e7f18d ...

  3. 【超值分享】为何写服务器程序需要自己管理内存,从改造std::string字符串操作说起。。。

    服务器程序为何要进行内存管理,管中窥豹,让我们从string字符串的操作说起...... new/delete是用于c++中的动态内存管理函数,而malloc/free在c++和c中都可以使用,本质上 ...

  4. centos7下安装mycat中间件 笔记

    1. 下载 # wget http://dl.mycat.org.cn/1.6.7.4/Mycat-server-1.6.7.4-release/Mycat-server-1.6.7.4-releas ...

  5. pip3 pip 安装包 临时更换镜像地址

    在使用pip3或者pip安装某些第三方包的时候,可能会遇到网络原因导致的安装失败. 可以在安装第三方包的时候临时指定镜像地址. 命令: pip3 install 库名 -i 镜像地址 例如:# pip ...

  6. java.lang.RuntimeException: Cannot create a secure XMLInputFactory 解决

    客户端调用服务端cxf,服务端报 java.lang.RuntimeException: Cannot create a secure XMLInputFactory 我的cxf 版本 为  3.0. ...

  7. 八数码难题之 A* 算法

    人生第一个A*算法-好激动-- 八数码难题--又称八数码水题,首先要理解一些东西: 1.状态可以转化成整数,比如状态: 1 2 3 4 5 6 7 8 0 可以转化成:123456780这个整数 2. ...

  8. 从小白角度探索Android事件分发机制

    今早来上班时看到郭神这边文章超赞,剧情很好引人入胜,特此备份! https://mp.weixin.qq.com/s?__biz=MzA5MzI3NjE2MA==&mid=2650244386 ...

  9. TestNG注释@BeforeGroups与@AfterGroups不执行的处理

    在学习TestNG框架注解时发现在执行以下的代码 package com.groups; import org.testng.annotations.AfterGroups; import org.t ...

  10. mysql《一》

    一.启动和停止服务器 通过管理员权限打开cmd命令指示符 通过 net stop mysql(自己的服务器名字)  停止服务器 通过 net start mysql(自己的服务器名字)  启动服务器 ...