c++之make_shared特性

概念介绍

c++11版本引入了智能指针shared_ptr/unique_ptr等,本文重点讲解share_ptr相关。由于引入了shared_ptr,根据shared_ptr的定义可以知晓shared_ptr一个模板类,支持基本数据类型,自定义数据类型的共享指针的构造。但是直接使用shared_ptr可能会引入一些问题,例如内存泄露。请看下面的例子:

class MyClass
{
    private:
    int value;
    public:
        Myclass(int x)
        {
            value = x;
        }
}
void process_value()
{
    ...
    throw std::runtime_error("无效值");
}
void fun()
{
    //如果process_value抛出异常,可能会会发生内存泄露
    //造成共享指针变量p未完成构造,那么MyClass对象就会发生泄露
    std::shared_ptr p = new MyClass(process_value());
    //调用顺序1.new Myclass构造函数-->2.process_value函数-->3.shared_ptr构造
    //调用顺序1.process_value函数-->new MyClass构造函数-->3.shared_ptr构造
    //如果在调用顺序是第一种情况,那么就会发生内存泄露
    ...
    return;
}

上面的例子说明如果shared_ptr使用不当可能会导致内存泄露。要解决这个问题,有两种方法,一种是待Myclass构造成功后再释放资源,请看下面的例子:

class MyClass
{
    public:
        Myclass(int x)
        {
            if(x < 0){throw std::runtime_error("无效值");}
        }
}
//正确的实现
void fun()
{
    int value = process();

    //第一种情况
    MyClass* cls = new MyClass(value);
    std::shared_ptr p(cls);

    //第二种情况
    std::shared_ptrp = new MyClass(value);
    //如果此时value是<0的数值,那么Myclass函数抛出异常后,new操作符会析构MyClass对象
}

上面的这种解决方法能够有效的避免内存泄露,原理如下:

  • new操作符能够保证当widget构造函数抛出异常时,自动析构该对象
  • 明确的调用顺序,不给编译器自己选择的空间

make_shared解决方法

class MyClass
{
    private:
    int value;
    public:
        Myclass(int x)
        {
            value = x;
        }
}
void process_value()
{
    ...
    throw std::runtime_error("无效值");
}
void fun()
{
    std::shared_ptr p = std::make_shared(process_value());
    ...
    return;
}

为什么make_shared方式是安全的呢?这与它的实现方式有关:

// make_shared 的简化实现原理
template
shared_ptr make_shared(Args&&... args) {
    // 在一个原子操作中完成:
    // 1. 分配内存
    // 2. 构造对象
    // 3. 构造 shared_ptr
    // 如果任何步骤失败,所有资源都会被正确清理
}

注意make_shared是在堆上实现的对象的创建。 

 make_shared的优点

  1. 单次分配性:共享指针shared_ptr在内存构成上分成两部分,对象块和控制块,因为有了控制块才能实现共享指针的管理,当共享指针对象的计数变为0时,析构该对象。使用make_shared可以一次性将对象块和控制块内存一次申请完成。使用shared_ptr是分开两次完成的。
  2. 原子操作性:整个make_shared操作是一个原子操作。
  3. 无中间状态:整个构造过程要么成功,要么失败。
  4. 内存泄露性:保证不发生内存泄露

make_shared的限制

make_shared有其自身的优点,也有自身的限制:

  • make_shared在c++20版本之前都不支持自定义的分配器
  • make_shared不支持自定义删除器,shared_ptr是支持自定义分配器和自定义删除器

make_shared基本使用方法

make_shared支持基本类型和自定义类型对象的创建,它本质是一个创建并返回shared_ptr的工厂函数。在c++17版本引入了支持数组类类型对象的创建。具体使用实例如下所示:

//基本数据类型
std::shared_ptr ptr1 = std::make_shared(10);
auto ptr2 = std::make_shared("hello");
auto ptr3 = std::make_shared(1.1);

//自定义类型
class Myclass
{
    private:
        int value = x;
    public:
        Myclass(int x){value = x;}
    ...
}

auto ptr4 = std::make_shared(22);

//c++17版本支持数组类型对象,之前版本不支持

auto ptr4 = std::make_shared(20); //创建一个数组长度为20的整形数组

shared_ptr与make_shared内存管理差异

 shared_ptr两次分配

举一个简单的例子:

shared_ptr ptr(new Widget(42));

这种方式会有两次内存分配:

// 示意图:两块独立的内存
+-------------------+     +------------------+
|    Widget对象  |      | 控制块(control|
|                        |      | block)              |
|                        |      | - 引用计数       |
|                        |      | - 弱引用计数    |
|                        |      | - 删除器等信息  |
+-------------------+     +------------------+ 

执行过程:

  1. new Widget(42) - 第一次分配,用于对象本身
  2. shared_ptr 构造 - 第二次分配,用于控制块

 make_shared的一次分配

auto ptr = make_shared(42);

这种方式只有一次内存分配:

// 示意图:一块连续的内存
+-------------------+-----------------------+
|    Widget对象   |  控制块               |
|                         |  - 引用计数          |
|                         |  - 弱引用计数      |
|                         |  - 删除器等信息   |
+-------------------+------------------------+
 

为什么一次分配更好?

  •  性能优势:减少了内存分配器的调用次数;减少了内存管理开销;减少了系统调用
  • 空间局限性:对象块和控制块在物理上相邻
  • 异常安全性:整个操作是一个原子操作,要么成功,要么失败,没有中间状态
  • 内存碎片:减少了内存碎片,更高效的内存使用

使用建议

  1. 始终优先使用make_shared创建shared_ptr对象
  2. 如果需要使用自定义删除器,那么需要使用shared_ptr直接构造,shared_ptr存在类似的构造函数可以自定义内存分配器和自定义删除器函数对象
  3. c++17后对数组优先使用make_shared

你可能感兴趣的:(c++,c++,开发语言)