当前位置: 首页 > article >正文

UE学习日志#25、26 C++笔记#11 智能指针

 注:本篇内容主要为《C++20高级编程》的学习笔记

         当智能指针离开作用域或被重置时,会自动释放所占用的资源。智能指针可用于管理在函数作用域内(或作为类的数据成员)动态分配的资源。也可以通过函数实参来传递动态分配的资源的所有权。

        1.可通过模板为任何指针类型编写类型安全的智能指针类

        2.可使用运算符重载为智能指针对象提供一个接口,使智能指针对象的使用和普通指针一样

1 unique_ptr

        unique_ptr拥有资源的唯一所有权。当unique_ptr被销毁或重置时,资源将自动释放。unique_ptr的一个优点是,内存和资源总会被释放,即使在执行return语句或抛出异常时也是如此。例如,当函数有多个返回语句时,这简化了编码,因为不必记住在每个return语句之前释放资源。
        作为经验法则,总是应该将动态分配的有唯一所有者的资源保存在unique_ptr的实例中。

1.1 创建 unique_ptr

        考虑下面的函数,这个函数在自由存储区上分配了一个 Simple 对象,但是不释放这个对象,故意产生内存泄漏。

void leaky(){
    Simple* mySimplePtr{ new Simple() };// BUG! 内存从未被释放!
    mySimplePtr->go();
}

        有时你可能认为,代码正确地释放了动态分配的内存。遗憾的是,这种想法几乎总是不正确的。看看下面的函数:

void couldBeLeaky(){
    Simple* mySimplePtr { new Simple{} };
    mySimplePtr->go();
    delete mySimplePtr;
}

        上面的函数动态分配一个 Simple 对象,使用该对象,然后正确地调用 delete。但是,这个例子仍然可能会产生内存泄漏!如果go方法抛出一个异常,将永远不会调用delete,导致内存泄漏。
        应该使用unique_ptr ,它可通过std:make_unique()辅助函数创建。unique_ptr 是一个泛型的智能指针,可以指向任何类型的内存。因此它是一个类模板,std:make_unique()是一个函数模板。两者都需要尖括号<>之间的模板参数,指定 unique_ptr需要指向的内存类型。
        下面的函数使用了 unique_ptr,而不是原始指针。Simple 对象不会显式释放,但 unique_ptr 实例离开作用域时(在函数的末尾,或者因为抛出了异常),就会在其析构函数中自动释放Simple 对象:

void notLeaky()
{
    auto mySimpleSmartPtr{ make_unique<Simple>() };
    mySimpleSmartPtr->go();
}

        如果Simple的构造需要参数,就把他们放在make_unique()调用的圆括号里

        C++20中引入的make_unique_for_overwrite()用于优化,基本类型不会初始化,而对象仍然默认构造。

        也可以通过构造函数创建:

unique_ptr<Simple> mySimpleSmartPtr{ new Simple() };

        但是也可能出现内存泄漏,所以应始终使用make_unique()创建unique_ptr.

1.2 使用 unique_ptr

        仍然可以通过*或->对智能指针解引用

mySimpleSmartPtr->go();
(*mySimpleSmartPtr).go();

        get()方法可以直接获取底层指针,可将指针传递给需要普通指针的函数:

void processData(Simple* simple){};
processData(mySimpleSmartPtr.get());

        reset(),可释放unique_ptr的底层指针,并根据需要将其改成另一个指针

mySimpleSmartPtr.reset();//释放资源并设为nullptr
mySimpleSmartPtr.reset(new Simple());//释放资源并设为一个新的Simple实例

        release()可断开于底层指针的连接,返回资源的底层指针,然后将智能指针设为nullptr,但因为智能指针失去对资源的所有权,需要在用完资源时释放资源。

Simple* simple{ mySimpleSmartPtr.release() };
delete simple;
simple = nullptr;

        std::move()工具函数可以用于显式移动unique_ptr的所有权(详细会补)

1.3 unique_ptr和C风格的数组

auto myVariableSizedArray { make_unique<int[]>(10) };
myVariableSizedArray[1] = 123;

1.4 自定义deleter

int* my_alloc(int value) { return new int { value }; }
void my_free(int *p){ delete p };

int main()
{
    unique_ptr<int ,decltype(&my_free)> myIntSmartPtr { my_alloc(42),my_free };
} 

        这个特性很有用,因为还可以管理其他类型的资源而不仅是内存。例如,当unique_ptr离开作用域时,可自动关闭文件或网络套接字以及其他资源。

        decltype(&my_free)用于返回my_free()的函数指针类型。

2 shared_ptr

        有时,多个对象或代码段需要同一指针的副本。由于unique_ptr无法复制,因此不能用于此类情况。相反,std::shared_ptr是一个可复制的支持共享所有权的智能指针。但是,如果有多个 shared_ptr实例引用同一资源,它们如何知道何时实际释放资源?这可以通过所谓的引用计数来解决,这正是“引用计数的必要性”一节的主题。但首先,让我们看看如何构造和使用 shared_ptr。

2.1 创建并使用 shared_ptr

        shared_ptr 的用法与 unique_ptr 类似。要创建 shared_ptr,可使用 make_shared(),它比直接创建shared_ptr 更高效。例如:

auto mySimpleSmartPtr { make_shared<Simple> () };

        总是使用 make_shared()创建 shared_ptr.
        与unique_ptr一样,类模板参数推断也不适用于 shared_ptr,所以必须指定模板类型。

        与unique_ptr类似,make_shared()使用了值初始化。如果你不需要的话,可以使用C++20 的
make_shared_for _overwrite()实现默认初始化,与 make_unique_for_overwrite()用法类似。


        从C++17开始,shared_ptr 就可以像 unique_ptr 一样可用于存储动态分配的C风格数组的指针。另外,从C++20开始,你可以使用 make_shared()来做这件事,正如你可以使用 make_unique()一样。


        但是,尽管这是可能的,仍建议使用标准库容器而非C风格数组


        与 unique_ptr一样,shared_ptr 也支持 ged()和 reset()方法。唯一的区别在于,当调用 reset()时,仅在最后的 shared_ptr 销毁或重置时,才释放底层资源。注意,shared_ptr 不支持 release()。可使用use_count()方法检索共享同一资源的shared_ptr 实例数量。


        与unique_ptr 类似,默认情况下,当存储C风格数组时,shared_ptr 使用标准的new 和 delete 运算符或使用 new[]和 delete[]分配和释放内存,可更改此行为,如下所示:

// Implementation of malloc_int () as before.
shared_ptr<int> myIntSmartPtr { malloc_int(42), my_free };

        可以看到,不必将自定义 deleter 的类型指定为模板类型参数,这比 unique_ptr 的自定义 deleter更简便。

2.2 引用计数的重要性

        这里只看原书中我觉得重要的一句话:唯一安全得到多个指向同一内存的shared_ptr实例的方法是创建shared_ptr的副本

2.3 强制转换 share_ptr

        正如某种类型的原始指针可以转换为另一种类型的指针一样,存储某种类型的 shared_ptr也可以转换为另一种类型的 shared_ptr。当然,对什么类型可以强制转换为什么类型有限制,并非所有强制转换都有效。可用于强制转换 shared _ptr 的函数有 const_pointer_cast()、dynamic_pointer_cast()、static_pointer_cast()和 reinterpret_pointer_cast()。它们的行为和工作方式类似于非智能指针转换函数const_cast()、dynamic_cast()、static_cast()和 reinterpret_cast(),之后会详细讨论这些方法。

2.4 别名

        shared_ptr支持所谓的别名。这允许一个 shared_ptr 与另一个 shared_ptr 共享一个指针(拥有的指针),但指向不同的对象(存储的指针)。例如,这可用于使用一个 shared_ptr 拥有一个对象本身的同时,指向该对象的成员。例如:

class Foo
{
    public:
        Foo(int value): m_data { value }{}
        int m_data;
};

auto foo{ make_shared<Foo>(42) };

auto aliasing { shared_ptr<int> { foo, &foo->m_data } };

        仅当两个 shared_ptr(foo 和 aliasing)都销毁时,才销毁Foo 对象。
        “拥有的指针”用于引用计数,对指针解引用或调用它的get()时,将返回“存储的指针”。
        警告:
        在现代C++代码中,只有在没有涉及所有权时才允许使用原始指针!如果涉及所有权,默认情况下使用 unique_ptr;如果需要共享所有权,则使用 shared_ptr. 此外,使用 make_ unique() 和 make _shared()创建这些智能指针。这样,几乎不需要直接调用new 运算符,也不需要调用 delete.

3 weak_ptr

        在C++中还有一个类与 shared_ptr 有关,那就是 weak_ptr。weak_ptr 可包含由 shared_ptr 管理的资源的引用。week _ptr 不拥有这个资源,所以不能阻止shared_ptr 释放资源。weak_ptr 销毁时(例如离开作用域时)不会销毁它指向的资源,然而,它可用于判断资源是否已经被关联的 shared_ptr 释放了。week_ptr的构造函数要求将一个 shared_ptr 或另一个weak_ptr 作为参数。为了访问weak_ptr 中保存的指针,需要将 weak_ptr 转换为 shared_ptr。这有两种方法:

  • 使用weak_ptr 实例的lock()方法,这个方法返回一个 shared_ptr。如果同时释放了与weak_ptr关联的 shared_ptr,返回的 shared_ptr 是 nullptr。
  • 创建一个新的 shared_ptr 实例,将 weak_ptr作为shared_ptr 构造函数的参数。如果释放了与weak_ptr 关联的 shared_ptr,将抛出 std:bad_weak_ptr 异常。

下例演示了 weak_ptr 的用法:

import <iostream>;
#include<limits>
#include<format>
#include<compare>
import Employee;
using namespace std;

class Simple {
public:
    int m_data{};
};

void useResource(weak_ptr<Simple>& weakSimple)
{
    auto resource{ weakSimple.lock() };
    if (resource)
    {
        cout << "Resource still alive." << endl;
    }
    else 
    {
        cout << "Resource has been freed!" << endl;
    }
}

int main()
{
    auto sharedSimple{ make_shared<Simple>() };
    weak_ptr<Simple> weakSimple{ sharedSimple };

    useResource(weakSimple);

    sharedSimple.reset();

    useResource(weakSimple);

}

运行结果: 

4 向函数传递参数 

        仅当涉及所有权转移或所有权共享时,接受指针作为其参数之一的函数才应该接受智能指针。要共享shared_ptr的所有权,只需要接受按值传递的shared_ptr作为参数。类似地,要转移unique_ptr的所有权,只需要接受按值传递的unique_ptr作为参数。后者需要使用移动语义。

        如果既不涉及所有权转移也不涉及所有权的共享,那么函数应该简单地使用 non-const 的引用或 const 引用作为参数;或者如果nullptr是参数的有效值,则应该使用原始指针作为参数。拥有诸如 const shared_ptr<T>& 或 const unique_ptr<T>&的参数类型没有多大意义。

5 从函数中返回

        从函数中返回智能指针是高效的。例如:

unique_ptr<Simple> create()
{
    auto ptr { make_unique<Simple>() };
    return ptr;
}
int main()
{
    unique_ptr<Simple> mySmartPtr1 {create()};
    auto mySmartPtr2{create()};
}

6 enable_shared_from_this

        std::enable_shared_from_this 派生的类允许对象调用方法,以安全地返回指向自己的shared_ptr或weak_ptr。如果没有这个基类,返回有效的shared_ptr 或 weak_ptr 的一种方法是将 weak_ptr 作为成员添加到类中,并返回它的副本或返回由它构造的shared_ptr。 enable_shared_from_this 类给派生类添加了以下两个方法:

  • shared_from_this()--返回一个 shared_ptr,它共享对象的所有权。
  • weak_from_ this()--返回一个 weak_ptr,它跟踪对象的所有权。

        这是一项高级功能,此处不做详述,下面的代码简单演示了它的用法。shared_from_this()和weak_fom_this()都是 public 方法。但是,你可能会觉得 public 接口中的 fom_this 部分令人困惑,因此作为演示,下面的Foo 类定义了自己的方法 getPointer()。

import <iostream>;
#include<limits>
#include<format>
#include<compare>
import Employee;
using namespace std;

class Foo : public enable_shared_from_this<Foo>
{
    public:
        shared_ptr<Foo> getPointer() {
            return shared_from_this();
        }
};

int main()
{
    auto ptr1{ make_shared<Foo>() };
    auto ptr2{ ptr1->getPointer() };
    cout <<"ptr1 adress=="<< ptr1 << endl;
    cout <<"ptr2 adress=="<< ptr2 << endl; 
    cout <<"ptr1.use_count()==" << ptr1.use_count() << endl;
    cout << "ptr2.use_count()==" << ptr2.use_count() << endl;
}

运行结果: 

        注意,仅当对象的指针已经存储在share_ptr时,才能使用对象上的 share_from_this()。否则,将会抛出 bad_weak_ptr异常。另外,调用weak_from_this()总是允许的,但当对象的指针还未存储在shared_ptr时,将会返回一个空的weak_ptr。

        不能直接return shared_ptr<Foo>(this);会导致双重释放。 

7 过时的auto_ptr,不要用


http://www.kler.cn/a/539740.html

相关文章:

  • DeepSeek从入门到精通教程PDF清华大学出版
  • C++ 中信号转异常机制:在磁盘 I/O 内存映射场景下的应用与解析
  • 【03】 区块链分布式网络
  • Linux内核中的软中断与NAPI机制:高效处理网络数据包
  • C语言的灵魂——指针(3)
  • Breakout靶场小试牛刀
  • Gemini 2.0模型更新:谷歌最新AI大模型全面开启智能时代
  • java数据结构_二叉树_5.3 (几道例题搞定二叉树的遍历)
  • BMC开发技术栈指南
  • SQL自学,mysql从入门到精通 --- 第 14天,主键、外键的使用
  • Gitlab中如何进行仓库迁移
  • XILINX硬件设计-(1)LVDS接口总结
  • 【Arxiv 大模型最新进展】TableRAG: 提高大语言模型在理解和推理大规模表格数据的效率和性能
  • oscp备考,oscp系列——VulnOSv2靶场,两种方法获取低权限shell
  • 三星OEM版SSD固态硬盘Model码对应关系
  • 【Spring Boot】Spring Boot解决循环依赖
  • c++计算机教程
  • 5G技术解析:从核心概念到关键技术
  • Java 中 ArrayList 和 LinkedList 有什么区别?
  • 【WB 深度学习实验管理】利用 Hugging Face 实现高效的自然语言处理实验跟踪与可视化
  • SQL自学,mysql从入门到精通 --- 第 5 天,对函数的处理
  • 神经网络|(九)概率论基础知识-泊松分布及python仿真
  • MySQL与钉钉数据融合,加速企业付款退款自动化进程
  • Spring AI -使用Spring快速开发ChatGPT应用
  • 鸿蒙NEXT API使用指导之文件压缩和邮件创建
  • 【Spring】Spring MVC入门(一)