1 shared_ptr简介

shared_ptr内容特别多,我希望可以讲清楚,但是并不能遍布所有。先看看思维导图,
作为最智能的内存管理成员–shared_ptr,shared_ptr早已经加入C++11的标准,下面的内容在Boost和C++标准中都是一致的。
本文分如下要点:

1. 创建:shared_ptr支持所有能想到的操作,不管是拷贝构造、赋值、显示构造、别名构造等。对比scoped_ptr、unique_ptr是不支持拷贝构造、赋值的(unique_ptr有一个右值赋值)
2. 指针操作:支持*、->、获取原始指针get()。对比weak_ptr不支持*和->
3. 比较操作:支持==、<、<<(流操作),注意这里的==是任意两个shared_ptr比较操作。对比scoped_ptr、unique_ptr只支持和空指针比较,就只有shared_ptr有<比较操作----应用在关联容器
4. 简单的工厂函数,目的更好的使用shared_ptr
5. 最重要的是,应用案例,也是本文的核心。

在这里插入图片描述

2 shared_ptr案例

下面是基础理解,一些简单的API应用

class shared
{
private:
    boost::shared_ptr<int> m_p;

public:
    // 拷贝方式
    shared(boost::shared_ptr<int> p) : m_p(p){};
    ~shared(){};

    void print()
    {
        std::cout << "count: " << m_p.use_count() << " v= " << *m_p << std::endl;
    }
};

// 拷贝方式 又拷贝了一次,紧接着又释放了
void print_fun(boost::shared_ptr<int> p)
{
    std::cout << "count: " << p.use_count() << " v= " << *p << std::endl;
}

{
	// main
    boost::shared_ptr<int> p(new int(100));
    shared s1(p), s2(p);		// 拷贝了两次

    s1.print();
    s2.print();

    *p = 20;
    print_fun(p);				

    s1.print();
    s2.print();
}

答案:

count: 3 v= 100
count: 3 v= 100
count: 4 v= 20
count: 3 v= 20
count: 3 v= 20

从结果,可以看出,不要做一些无畏的拷贝,const &!!!
任何一个参数,先把const &加上,问一下两个问题

  1. 我需要拷贝吗?需要就去掉&
  2. 我需要修改值吗?需要去掉const

2.1 reset()的理解

reset()也是一族函数,本意是即将管理另外一个指针p,放弃当前的。
如果p为空,管理另外一个指针p(管理空指针),放弃当前的。相当于置0。
注意:

  1. p只是原始指针,并不是智能指针!!!
  2. 从原理上讲,放弃原来的—计数变量会减1;管理新的—又会加1,有时候会发现不变,抵消了
void reset() BOOST_SP_NOEXCEPT
template<class Y> void reset( Y * p )
template<class Y, class D> void reset( Y * p, D d )
...

分析下面的代码

    {
        auto sp = boost::make_shared<std::string>("make_shared123");
        auto sp1 = new std::string("xxxx");
        std::cout << "use_count: " << sp.use_count() << std::endl;
        sp.reset(sp1);
        std::cout << "use_count: " << sp.use_count() << std::endl;
        std::cout << "*sp: " << *sp << std::endl;
    }

输出:
在这里插入图片描述

  1. sp管理”make_shared123“,引用计数加1
  2. 之后sp管理新的”xxxx“,相当于原来的已经释放了,计数器先减1,后增加1,计数器值保持不变

2.2 make_shared的理解

通常make_shared()函数比直接创建shared_ptr对象要高效,且只分配一次内存

  1. 方式一,直接使用make_shared效率更高,不仅快而且高效,分配一次内存
  2. 方式二,就不要使用了,要动用两次动态内存分配
// 方式一
auto sp1 = boost::make_shared<std::string>("make_shared");

// 方式二
boost::shared_ptr<std::string> sp2(new std::string("make_shared"));

2.3 shared_ptr中的指针转型问题

对于shared_ptr专门提供了三个指针转型,为什么么?
一个很简单的想法:shared_ptr指针转型后还应该是shared_ptr
但是标准库中四种转型,结果就不是shared_ptr了
嘿嘿,总有人会绕后门,绝不可写出下面这种危险的代码,除非你清楚知道你在干啥!!!

static_cast<T*>(p.get());
  1. static_pointer_cast<T> -------------- 向上转,派生类到基类
  2. dynamic_pointer_cast<T>----------- 向下转,基类到派生类
  3. const_pointer_cast<T> --------------去除const属性

这种代码一般在多态的时候才会遇到,就第一、第二简单的举例

boost::shared_ptr<base> sp(new derived);
auto sp1 = dynamic_pointer_cast<derived>(sp);	// 下转
auto sp2 = static_pointer_cast<base>(sp1);		// 上转

2.4 shared_ptr在容器中的应用

shared_ptr搭配容器有两种典型的方式

1. shared_ptr<容器> <------> shared_ptr<vector<int>>
2. 容器<shared_ptr> <------> vector<shared_ptr<int>>

这两种方式还是很好理解的,下面举一个简单的例子
eg: 一个容器里面装的是智能指针,而智能指针管理的是int

	typedef std::vector<boost::shared_ptr<int>> vs;
	vs v(10);
	// pos是容器的迭代器指针,正如下面需要两次解引用
	for (auto pos = v.begin(); pos != v.end(); ++pos)
	{
	    (*pos) = boost::make_shared<int>(++i); // 工厂函数进行赋值,更加高效
	    std::cout << **pos << ", ";            // 第一次获取数组中地址,第二次获取值
	}
	
    // Method2 我一般用这种,,,
    for (auto &e : v)
    {
        e = boost::make_shared<int>(++i);
        std::cout << *e << ", ";
    }

2.5 定制删除器D

对于一般的简单数据类型,类我们使用默认的删除器就好了, 当然如果我们处理的是资源类,使用自定义的删除器会发挥shared_ptr的真实作用,是真正意义上的智能

常见的资源有

  1. 数据库、文件句柄、socket、自定义资源类等等

下面举两个例子

2.5.1 shared_ptr管理FILE文件指针

将打开的文件句柄交给shared_ptr管理,如果发生任何不当行为,shared_ptr会帮助我们自动释放资源,程序员也不用自己显式写出fclose。
如果文件不存在,那么shared_ptr管理的是空指针,这样会在fclose发生异常,为了避免,先打开后判断是否为空哈

boost::shared_ptr<FILE> fp(fopen("/home/topeet/myBoost/chap3_ptr/text.txt", "r"), fclose);

2.5.2 shared_ptr管理socket

一个自定义类管理套接字资源,自定义一个合适的资源释放函数,更加高效!!!

	class socket_t { };
	socket_t *open_socket() {
	    std::cout << "open_socket" << std::endl;
	    return new socket_t;
	}
	void close_socket(socket_t * s) {		// 资源释放函数
	    std::cout << "close_socket" << std::endl;
	    delete s;
	}
	
	socket_t *s = open_socket();
	if (s != nullptr)						// 其实这里可以不判断,delete 空指针不会异常,但是这不符合代码严谨性
	    boost::shared_ptr<socket_t> p(s, close_socket);
	else
	    return -1;

2.5.3 shared_ptr 高级用法

注意,我们假用一个空指针,在其退出作用域时,自动调用任意的函数!!!

void any_fun(void* p) {....}
boost::shared_ptr<void> p(nullptr, any_func); //在其退出作用域时,自动调用任意的函数!!!

2.6 关于桥接方式–减少文件的依赖关系,减少编译时间

关于这个主题,很复杂,我写了一篇博客
C++ 如何缩短编译时间(Effective C++ 条款31:将文件间的编译依存关系降至最低)
https://blog.csdn.net/weixin_39956356/article/details/110404940
有两种技术

  1. 技术1:运用Handle Class技术降低接口和实现的耦合性
  2. 技术2:运用Interface Class技术降低接口和实现的耦合性

对于技术1,shared_ptr主要用在private中—提供接口类中
对于技术2,shared_ptr主要用在create上,很有意思
具体细节,请浏览上面的链接

2.7 借shared_ptr我们分享下别名构造函数(alias constructor)

之后待续…

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐