查看: 120|回复: 1

C++ 智能指针

[复制链接]

4

主题

5

帖子

13

积分

新手上路

Rank: 1

积分
13
发表于 2022-12-17 18:20:44 | 显示全部楼层 |阅读模式
unique_ptr

使用场景

unique_ptr 对其持有的堆内存具有唯一拥有权,也可以说该智能指针对资源(即其管理的堆内存)的引用计数永远是 1,unique_ptr 对象在销毁时会释放其持有的堆内存。
使用方式

创建
//方式一
std::unique_ptr<int> sp1(new int(123));
//方式二
std::unique_ptr<int> sp2;
sp2.reset(new int(123));
//方式三
std::unique_ptr<int> sp3=std::make_unique<int>(123);
推荐使用第三种创建方式,因为该方式考虑到了 异常安全 等(详细解释在注意事项)。make_unique 并未在 C++  版本小于 14 中进行实现,需要用户额外编写,下面提供一种简单的实现方式1(需要注意的是这种实现方式的构造不支持数组以及自定义 deleter):
template<typename T,typename... Ts>
std::unique_ptr<T> make_unique(T&& ..params)
{
    return std::unique<T>(new T(std::forward<Ts>(params)...));
}
特性
unique_ptr 有如下几个特性:

  • 无法拷贝&赋值,在其实现中将拷贝构造函数和赋值运算符标记为 delete;
  • 可以使用move语句来实现堆内存转移给其他对象;
  • 不仅可以持有一个堆对象,也可以持有一组堆;
  • 自定义资源释放函数,unique_ptr对象在析构;;时只会释放其持有的堆内存,但是假设这块堆内存代表的对象还对应一种需要回收的资源,我们则可以通过给智能指针自定义资源回收函数来实现资源回收。
示例
堆内存转移语句示例:
#include <iostream>
#include <memory>
int function(std::unique_ptr<int> qtr){
    std::cout<<*qtr<<std::endl;
    return 1;
}
int main(){
    std::unique_ptr<int> qtr=std::make_unique<int>(123);
    function(std::move(qtr));
    return 0;
}
堆数组示例:
//方式1
std::unique_ptr<int[]> sp1(new int[10]);
//方式2
std::unique_ptr<int[]> sp2;
sp2.reset(new int[10]);
//方式3
std::unique<int[]> sp3(std::make_unique<int[]>(10));
自定义资源回收示例:
auto deletor=[](Object* object){
    object->close();
    dolog("resrouce be closed");
    delete object;
}
std::unique_ptr<Object,void(*)(Object* obj)>(new Object(),deletor);
注意事项

优先使用make函数的理由
这里参照了《Effective Modern C++》有关的解释。
1.更少的代码量
auto upw1(std::make_unique<Widget>());//使用make函数
std::unique_ptr<Widget> upw2(new Widget);//不使用make函数
可以发现,使用make_unique的方法可以少写一个Widget。
2.异常安全
int computePriority();
processWidget(std::shared_ptr<Widget>(new Widget),computePriority())
为什么上面的代码可能会产生内存泄露?
上面代码产生的内存泄露和编译器将源代码翻译为 object code 有关 。在函数被调用前,函数的参数必须被推算出来,所以在processWidget 开始执行之前,下面的步骤必须要执行:

  • "new Widget"表达式必须被执行,即,一个Widget必须在堆上被创建;
  • 负责管理new所创建的指针的std::shared_ptr<Widget>的构造函数必须被执行;
  • computePriority 必须被执行。
C++标准并没有要求编译器生成对这些操作做到按顺序执行的代码,"new Widget"必须要在 shared_ptr 的构造函数被调用之前执行,因为 new 的结果作为该构造函数的一个参数。computePriority 则可能这两者之前、之后或者之间执行,若其在两者之间执行,会产生如下步骤:

  • 执行"new Widget";
  • 执行 computePriority ;
  • 执行std::shared_ptr<Widget> 的构造函数。
如果这样的代码在运行时被生成出来,computePriority 产生出了一个异常,那么在步骤 1 中动态分配的 Widget 可能会产生泄漏。因为它永远不会存储在步骤 3 中执行的本应负责管理它的 std::shared_ptr中。
make 函数如何避免内存泄露?
使用make函数实现代码如下:
processWidget(std::make_shared<Widget>(),computePriority);
在运行时,std::make_shared或者 computePriority 都有可能被第一次调用。如果是std::make_shared先被调用,被动态分配的 Widget 安全的存储在返回的std::shared_ptr中(在 computePriority 被调用之前)。如果 computePriority 产生了异常,std::shared_ptr的析构函数会负责把它所拥有的 Widget 回收。如果 computePriority 首先被调用并且产生出一个异常,std::make_shared不会被调用,因此也不必担心动态分配的 Widget 会产生泄漏的问题。
3.更高的效率
std::shared_ptr<Widget> spw(new Widget);
考虑上面直接使用 new 的方式,很明显的情况是代码只需一次内存分配,但实际上它执行了两次。一个 shared_ptr 都指向了一个包含被指向对象的引用计数的控制块,控制块的分配工作在 shared_ptr 的构造函数内部完成。直接使用 new,就需要一次为 Widget 分配内存,第二次需要为控制块分配内存。
如果使用的是 make_shared,则只需要一次内存分配。
4.不适用的场景

  • 使用 make_shared 没办法指定 deletor (自定义资源回收函数)。
  • make 函数中,完美转发使用的是括号(不使用 initializer_list 的构造函数)而非大括号(initializer_list 的构造函数)。因此使用 initializer_list 进行构造将不适用。
<hr/>shared_ptr

使用场景

shared_ptr 持有的资源可以在多个 shared_ptr 之间共享,每多一个 shared_ptr 对资源的引用,资源引用计数就会增加1,在每一个指向该资源的 shared_ptr 对象析构时,资源引用计数都会减少 1,最后一个 shared_ptr  对象析构时,若发现资源计数为 0,则将释放其持有的资源。多个线程之间递增和减少资源的引用计数都是安全的。
使用方式

创建
//方式1
std::shared_ptr<int> sp1(new int(123));
//方式2
std::shared_ptr<int> sp2;
sp2.reset(new int(123));
//方式3
std::shared_ptr<int> sp3;
sp3=std::make_shared<int>(123);
与 unique_ptr 相同,推荐使用第三种初始化方式。
特性

  • 通过使用 use_count 方法获取当前引用计数
  • 使用 reset 方法释放对象引用
  • 在类中返回包裹当前对象的一个 shared_ptr 对象给外部使用,可以通过继承enable_shared_from_this 模板对象实现。
示例
use_count & reset方法示例:
int main(){
    //初始化
    std::shared_ptr<int> sp1=std::make_shared<int>(123);
    std::cout<<"use count:"<<sp1.use_count()<<std::endl;
    //赋值
    std::shared_ptr<int> sp2=sp1;
    std::cout<<"use count:"<<sp1.use_count()<<std::endl;
    //作用域
    {
        //拷贝
        std::shared_ptr<int> sp3(sp1);
        std::cout<<"use count:"<<sp1.use_count()<<std::endl;
     }
    //出作用域,sp3将被析构         
    //sp2释放引用
    sp2.reset();        
    std::cout<<"use count:"<<sp1.use_count()<<std::endl;
}
enable_shared_from_this 示例:
class A:public std::enable_shared_from_this<A>
{
public:
    A(){std::cout<<"constructor\n";}   
    ~A()(std::cout<<"destructor\n";)
    std::shared_ptr<A> getSelfSharedPtr(){return shared_from_this();}
};
int main(){
    std::shared_ptr<A> sp1=std::make_shared<A>();
    std::shared_ptr<A> sp2=sp1->getSelfSharedPtr();
    std::cout<<sp1.use_count()<<std::endl;//输出为2
    return 0;
}
注意事项

优先使用make函数的理由
参照 unique_ptr 中的相关介绍。
enable_shared_from_this 注意事项
注意事项一:不允许栈对象的this指针给智能指针对象
class A:public std::enable_shared_from_this<A>
{
    //...
};
int main(){
    A a;
    std::shared_ptr<A> sp2=a.getSelfSharedPtr();
    std::cout<<sp1.use_count()<<std::endl;
    return 0;
}
上述程序无法运行,智能指针管理的是堆对象,栈对象会在函数调用结束后自行销毁。
注意事项二:循环引用问题
class A:public std::enable_shared_from_this<A>
{
public:
    A(){std::cout<<"constructor\n";}   
    ~A()(std::cout<<"destructor\n";)
    void func(){
        m_self_ptr=shared_from_this();   
    }
    std::shared_ptr<A> getSelfSharedPtr(){return shared_from_this();}
private:
    std::shared_ptr<A> m_self_ptr;
};
int main(){
    {
        std::shared_ptr<A> spa(new A());
        spa->func();
    }
    return 0;
}
上述程序只打印constructor ,并不打印destructor ,当前程序存在内存泄漏。原因是:
spa出了其作用域准备析构,在析构时其发现仍然有另一个 std::hared_ptr 对象即 A::m_self_ptr 引用了 A,因此 spa 只会将对 A 的引用计数递减为 1,然后销毁自身。
<hr/>weak_ptr

使用场景

weak_ptr 是一个不控制资源生命周期的智能指针,是对对象的一种弱引用,只提供了对其管理的资源的一个访问手段,引入它的目的是协助 shared_ptr 工作。常用的使用场景,如:解决互相引用死锁问题观察者模式等。
使用方式

创建
auto sp1=std::make_shared<int>(123);
//通过构造函数创建
std::weak_ptr<int> sp2(sp1);
//赋值
std::weak_ptr<int> sp3=sp2;
std::weak_ptr<int> sp4=sp2;
无论通过何种方式创建 weak_ptr,都不会增加资源的引用。
特性
相较于 shared_ptr ,weak_ptr 因其功能的原因有如下特性:

  • 可以通过expired判断资源是否失效;
  • weak_ptr 类没有重写 operator->和 operator* 方法,因此不能直接操作对象。一般的使用 lock 方法获取shared_ptr后进行对象操作;
示例
一般的使用方法:
auto sp1=std::make_shared<A>(123);
std::weak_ptr<int> s=sp1;
if(s.expired())
    return;
auto m_sp=s.lock();
m_sp->do_something();
注意事项

多线程场景下常见的安全隐患:
//线程1
void A::on_free(){
    //让引用资源失效
    s.reset();
}
//线程2
void A::do_something(){
    if(s.expired())
        return;
   auto m_sp=s.lock();
   m_sp->do_something();
}
在以上代码中,若线程 2 的 expired() 执行后  lock() 执行前,线程 1 执行了 on_free() 释放了 s 的引用资源,线程 2 接下来的逻辑将产生安全隐患。
回复

使用道具 举报

1

主题

13

帖子

21

积分

新手上路

Rank: 1

积分
21
发表于 2025-4-2 05:58:01 | 显示全部楼层
向楼主学习
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表