平时写代码一直避免使用指针,但在某些场景下指针的使用还是有必要的。最近在项目中简单使用了一下智能指针(shared_ptr),结果踩了不少坑,差点就爬不出来了。痛定思痛抱着《Cpp Primer》啃了两天,看书的时候才发现自己的理解和实践很浅薄,真的是有种后背发凉的感觉。。。特地记录下这些坑点,且警后人(指后来的自己=。=).


写在前面……

本次实验基于的数据结构定义如下:

基类Polygon的成员_points是一个shared_ptr,指向动态分配的vector<Point>,这样实现了在Polygon对象的多个拷贝之间共享相同的vector<Point>。基于Polygon实现了RectCircle两个子类。

#include <vector>
#include <string>
#include <memory>
#include <cassert>using namespace std;static constexpr double PI = 3.14;using coord_t = double;struct Point { coord_t x, y; };class Polygon {public:Polygon(const vector<Point> &points) :_points(make_shared<const vector<Point>>(points)) {}virtual string shape() const = 0;virtual coord_t area() const = 0;public:const shared_ptr<const vector<Point>> _points;
};class Rect final : public Polygon {public:Rect(const vector<Point> &points, coord_t width, coord_t height) :Polygon(points), _width(width), _height(height) {assert(points.size() == 4);}string shape() const { return "Rect"; }coord_t area() const { return _width * _height; }private:const coord_t _width;const coord_t _height;
};class Circle final : public Polygon {public:Circle(const vector<Point> &points, coord_t radius) :Polygon(points), _center(points.front()), _radius(radius) {assert(points.size() == 1);}string shape() const { return "Circle"; }coord_t area() const { return PI * _radius * _radius; }private:const Point _center;const coord_t _radius;
};using polygon_ptr = shared_ptr<Polygon>;using rect_ptr = shared_ptr<Rect>;using circle_ptr = shared_ptr<Circle>;// 定义一个边长为5的矩形和一个半径为5的圆.
static vector<Point> r_points{ {0,0},{0,5},{5,5},{5,0} };
static coord_t r_width = 5, r_height = 5;
static vector<Point> c_points{ {0,0} };
static coord_t c_radius = 5;

从正确定义智能指针开始……

在项目中采用智能指针的初衷是为了实现多个对象之间共享数据,避免拷贝造成的开销。然而在使用的时候,我竟然连定义一个智能指针都能制造出五花八门的错误。。。下面分别整理了正确和错误的用法。

1. make_shared函数:最安全的分配和使用动态内存的方法

类似顺序容器的emplace成员,make_shared用其参数来构造给定类型的对象。可以是一般的构造函数:

shared_ptr<Rect> p1 = make_shared<Rect>(r_points, r_width, r_height);

也可以是拷贝构造函数:

Rect rect_2(r_points, r_width, r_height);
shared_ptr<Rect> p2 = make_shared<Rect>(rect_2);

Ps:需要说明的一点是,由于p2指向的对象(即*p2)是rect_2的拷贝,所以它们的_points成员指向相同的内存,共享相同的vector<Point>。这个vector<Point>r_points的一份拷贝,保存在动态内存中。

2. shared_ptrnew结合使用

可以用new返回的指针来初始化智能指针:

shared_ptr<Rect> p3(new Rect(r_points, r_width, r_height));

或者将一个shared_ptr绑定到一个已经定义的普通指针:

Rect *x = new Rect(r_points, r_width, r_height);
shared_ptr<Rect> p4(x);
x = nullptr;

Ps:这是一种不建议的写法。原则上当p4绑定到x时,内存管理的责任就交给了p4,就不应该再使用x来访问p4指向的内存了。因此建议在完成绑定之后立刻将x置为空指针nullptr,避免在后续代码中使用delete x释放p4所指的内存,或者又将其他智能指针绑定到x上,这都会造成同一块内存多次释放的错误。
但这就出现一个尴尬的情况:程序员要时刻记得一个已经存在的变量不能使用,这要求实在是高了点。。。最理想的还是不要制造出x,或者说x的存在就没有意义。

3. 【错误1】试图从raw指针隐式转换到智能指针

shared_ptr<Rect> p5 = new Rect(r_points, r_width, r_height); // !!!

【修改】接受指针参数的智能指针构造函数是explicit的,必须使用直接初始化形式:

shared_ptr<Rect> p5(new Rect(r_points, r_width, r_height));

4. 【错误2】将非动态分配的内存托管给智能指针

Rect rect_6(r_points, r_width, r_height);
shared_ptr<Rect> p6(&rect_6); // !!!

这种写法将p6指向一块栈内存,相当于局部变量rect_6p6管理了同一内存空间,而栈内存中的对象是编译器负责创建和销毁的,而且不能析构一个指向非动态分配的内存的智能指针,因此是不合理的。

【修改】创建智能指针时传递一个空的删除器函数或者直接使用raw指针,详见stackoverflow。正如回答中说的:There is not much point in using a shared_ptr for an automatically allocated object.

Rect rect_6(r_points, r_width, r_height);
shared_ptr<Rect> p6(&rect_6, [](Rect*) {});

5. 【错误3】将同一份动态内存托管给多个智能指针

Rect *xx = new Rect(r_points, r_width, r_height);
shared_ptr<Rect> p7(xx);
{shared_ptr<Rect> p8(xx); // !!!shared_ptr<Rect> p9(p7.get()); // !!!
}
xx = nullptr;
Rect rect_7 = *p7;

p7p8p9指向了相同的动态内存,但由于它们是相互独立创建的,因此各自的引用计数都是1,即相互不知道对方的存在,认为自己是这块内存的唯一管理者。当p8p9所在程序块结束时,内存被释放,从而导致p7变为空悬指针,意味着当试图使用p7时将发生未定义的行为;而且也存在同一内存多次释放的危险。

Ps:在测试中还发现这种多个智能指针托管同一动态内存的情况与上文智能指针指向栈内存的情况,二者报错信息并不相同。

【修改】与错误用法2类似,在创建智能指针时传递一个空的删除器函数即可。

Rect *xx = new Rect(r_points, r_width, r_height);
shared_ptr<Rect> p7(xx);
{shared_ptr<Rect> p8(xx, [](Rect*) {});shared_ptr<Rect> p9(p7.get(), [](Rect*) {});
}
xx = nullptr;
Rect rect_7 = *p7;

小结:本质上4和5属于同一类型的错误,即同一块内存由多个管理者托管,但它们彼此之间又不知道对方的存在,这样就导致在它们各自生命周期结束时都会释放这块内存的错误。个人认为,5的正确写法在某种程度上还是可以接受的,但4是一种完全不合理的智能指针使用方式,这种情况就应该直接使用raw指针,“只有将指向动态分配的对象的指针交给shared_ptr托管才是有意义的”。
往往这种错误在编译期间没有问题,但运行时会报错,因此不易排查。为了避免这种错误,应该养成良好的编程意识,《Cpp Primer》中提到几条基本规范,建议严格遵循:
1. 不使用相同的raw指针初始化(或reset)多个智能指针。
2. 不delete get()返回的指针。
3. 不使用get()初始化或reset另一个智能指针。
4. 如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。
5. 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。

智能指针的使用场景

《Cpp Primer》中提到程序使用动态内存出于以下三种原因之一:

1. 程序不知道自己需要使用多少对象
2. 程序不知道所需对象的准确类型
3. 程序需要在多个对象间共享数据

容器类是出于第一种原因而使用动态内存的典型例子,而2和3的需求可以使用(智能)指针很好地满足。

智能指针成员

基类Polygon中的_points成员是一个shared_ptr智能指针,依靠它实现了Polygon对象的不同拷贝之间共享相同的vector<Point>,并且此成员将记录有多少个对象共享了相同的vector<Point>,并且能在最后一个使用者被销毁时释放该内存。

Rect rect_1(r_points, r_width, r_height);
cout << "rect_1 points成员地址: " << rect_1._points.get() << endl;
cout << "rect_1 points引用计数: " << rect_1._points.use_count() << endl;Rect rect_2 = rect_1;
cout << "rect_2 points成员地址: " << rect_2._points.get() << endl;
cout << "rect_2 points引用计数: " << rect_2._points.use_count() << endl;

上述代码的运行结果:

程序需要在多个对象间共享数据 →(智能)指针成员

容器与继承

当我们使用容器存放继承体系中的对象时,因为不允许直接在容器中保存不同类型的元素,通常必须采取间接存储的方式,即我们实际上存放的是基类的(智能)指针,这些指针所指的对象可以是基类对象,也可以是派生类对象。当要使用具体的对象时,要利用多态性将基类指针下行转换为派生类指针。

vector<polygon_ptr> polygon_ptrs;
polygon_ptrs.push_back(make_shared<Rect>(r_points, r_width, r_height));
polygon_ptrs.push_back(make_shared<Circle>(c_points, c_radius));//auto rect = dynamic_cast<Rect*>(polygon_ptrs.front()); // compile error
//auto rect = dynamic_cast<rect_ptr>(polygon_ptrs.front()); // compile error
auto rect = dynamic_pointer_cast<Rect>(polygon_ptrs.front()); // compile success
cout << "polygon_ptrs.front() shape: " << rect->shape() << " area: " << rect->area() << endl;
auto circle = dynamic_pointer_cast<Circle>(polygon_ptrs.back());
cout << "polygon_ptrs.back() shape: " << circle->shape() << " area: " << circle->area() << endl;

上述代码的运行结果:

程序不知道所需对象的准确类型 → 容器中放置(智能)指针而非对象

智能指针的下行转换
1. 必须使用dynamic_pointer_cast,而不是dynamic_cast。这是因为父子两种智能指针并非继承关系,而是完全不同的类型。
2. 基类必须是多态类型(包含虚函数)。

[Github] 代码

项目实例均在vs2017上测试,并上传至GitHub。

[Reference] 参考

Stack Overflow: Set shared_ptr to point existing object​stackoverflow.comC++11 shared_ptr(智能指针)详解​www.cnblogs.com

get方法报空指针_智能指针shared_ptr踩坑笔记相关推荐

  1. java get方法报空指针_面试的哪些事儿之JAVA程序员面试笔试题(一)

    前言 在一个技术微信群看一个网友最近在一家公司做笔试的题目,然后我就整理了一下,供大家参考一下,希望能够帮助到大家. 笔试内容 1.假设有一个mysql实例,相关信息如下: schema名为test用 ...

  2. C++_指针的定义使用_指针所占内存空间_空指针_野指针---C++语言工作笔记023

    然后我们再来学这个指针,指针是c系列语言中的,重要的部分,在其他的语言里没有, 所以也难一点. 可以看到通过 int * p; 定义一个指针,指针就是指向一个地址. 然后有个 int a =10; 可 ...

  3. 智能指针 shared_ptr 解析

    近期正在进行<Effective C++>的第二遍阅读,书里面多个条款涉及到了shared_ptr智能指针,介绍的太分散,学习起来麻烦.写篇blog整理一下. LinJM   @HQU s ...

  4. C++智能指针shared_ptr、unique_ptr以及weak_ptr

    目录 shared_ptr类 shared_ptr和unique_ptr都支持的操作 shared_ptr独有的操作 make_shared函数 shared_ptr自动销毁所管理的对象 由普通指针管 ...

  5. 智能指针(shared_ptr、unique_ptr、weak_ptr)的使用

    智能指针的使用 一.shared_ptr 1.创建一个shared_ptr 2.shared_ptr的常用成员函数 reset成员函数的使用 3.==注意事项== 二.unique_ptr 1.uni ...

  6. 智能指针shared_ptr的原理、用法和注意事项

    智能指针shared_ptr的原理.用法和注意事项 1 前言 2 shared_ptr原理 3 shared_ptr的基本用法 3.1 初始化 3.2 获取原始指针 4 智能指针和动态数组 4.1 c ...

  7. 智能指针shared_ptr的用法

    智能指针shared_ptr的用法 2016-12-03 15:39 by jiayayao, 360 阅读, 0 评论, 收藏, 编辑 为了解决C++内存泄漏的问题,C++11引入了智能指针(Sma ...

  8. 智能指针shared_ptr

    如果有可能就使用unique_ptr,然后很多时候对象是需要共享的,因此shared_ptr也就会用得很多.shared_ptr允许多个指向同一个对象,当指向对象的最后一个shared_ptr销毁时, ...

  9. 智能指针shared_ptr的几个例子

    #include <string> #include <iostream> #include <memory> //智能指针定义在头文件memory中,例如shar ...

最新文章

  1. 联想小新air13pro重装系统_联想 小新Air 13 ProU盘装系统win7教程
  2. leetcode valid number
  3. 数学老师必备工具,你的最爱!
  4. 数据结构与算法笔记 —— 十大经典排序及算法的稳定性
  5. 手机KG音乐怎么下载竖屏MV
  6. 2021第六届数维杯大学生数学建模竞赛赛题_B 中小城市地铁运营与建设优化设计
  7. windows下cmd命令提示符下让程序后台运行命令
  8. RabbitMQ入门(三)-Publish/Subscribe(发布/订阅)
  9. php如何解释xml,PHP – 如何解析这个xml?
  10. centos7下cups + samba共打印服务
  11. jQuery Mobile中选择select的data-*选项
  12. java 获取当前时分_java实现获取当前年、月、日 、小时 、分钟、 秒、 毫秒
  13. 电大计算机阅读英语作文,(2017年电大)电大英语作文整理20篇.doc
  14. Npm更新移除包的规则
  15. PHP array(递归)转xml,xml转array
  16. 【天梯选拔月赛】经典算法之过河问题+(倒水问题--见链接)
  17. 日期:将格林尼治时间(GMT)转化为北京时间
  18. “熊猫烧香”主犯:毒王?黑客英雄?
  19. 联想微型计算机设置从u盘启动,联想笔记本设置u盘为第一启动项教程
  20. 基于SSM技术的医院在线预约诊疗系统设计与实现 毕业设计-附源码011130

热门文章

  1. 论文《Attention Is All You Need》及Transformer模型
  2. 【数据库复习】第二章关系数据库
  3. buu 凯撒?替换?呵呵!
  4. GPTEE中的Storage API的使用
  5. TEEC_Context和TEEC_InitializeContext介绍
  6. [register]-05-ARMv8中常用系统寄存器详解
  7. Nginx的基本介绍反向代理
  8. pr如何处理音效_Pr基础全通关:从0到1,进阶剪辑大神
  9. Win10 EPROCESS 断链
  10. python获取IP位置信息