std::weak_ptr什么时候有用?

When is std::weak_ptr useful?

提问人: 提问时间:8/20/2012 最后编辑:Jan Schultke 更新时间:9/24/2023 访问量:203978

问:

我开始研究 C++11 的智能指针,但我没有看到任何有用的用处。有人可以告诉我什么时候有用/必要吗?std::weak_ptrstd::weak_ptr

C ++11 共享 PTR 弱 PTR C++-FAQ

答:

292赞 David Schwartz 8/20/2012 #1

一个很好的例子是缓存。

对于最近访问的对象,您希望将它们保留在内存中,以便持有指向它们的强指针。定期扫描缓存并确定最近未访问的对象。你不需要把它们保留在内存中,所以你去掉了强指针。

但是,如果该对象正在使用中,并且其他一些代码包含指向它的强指针,该怎么办?如果缓存删除了指向对象的唯一指针,则永远无法再次找到它。因此,缓存会保留一个指向对象的弱指针,如果它们碰巧留在内存中,则需要查找这些对象。

这正是弱指针的作用——它允许你找到一个对象,如果它还在,但如果没有其他东西需要它,它就不能保留它。

评论

17赞 8/20/2012
所以 std::wake_ptr 只能指向另一个指针指向的位置,并且当指向的对象被任何其他指针删除/不再指向时,它指向 nullptr?
41赞 David Schwartz 8/20/2012
@R.M.:基本上是的。当您有一个弱指针时,您可以尝试将其提升为强指针。如果该对象仍然存在(因为至少有一个指向它的强指针仍然存在),则该操作将成功,并为您提供指向该对象的强指针。如果该对象不存在(因为所有强指针都消失了),则该操作将失败(通常,您通过丢弃弱指针来做出反应)。
25赞 The Vivandiere 9/20/2016
当强指针使对象保持活动状态时,weak_ptr可以查看它......不会破坏物体的使用寿命。
5赞 Jason C 1/27/2017
另一个例子,我至少用过几次,是当实现观察者时,有时让主题维护一个薄弱指针列表并执行自己的列表清理会变得很方便。当观察者被删除时,它节省了一点精力,更重要的是,在销毁观察者时,您不必获得有关主题的信息,这通常会简化很多事情。
5赞 rubenvb 8/9/2018
等等,保存shared_ptr的缓存在应该从内存中清除时将其从列表中删除有什么问题?任何用户都将持有一个shared_ptr,并且一旦所有用户都用完缓存的资源,缓存的资源就会被清除。
31赞 Kerrek SB 8/20/2012 #2

下面是 @jleahy 给我的一个例子:假设你有一个任务集合,异步执行,并由 .您可能希望定期对这些任务执行某些操作,因此计时器事件可能会遍历 a 并为任务提供要执行的操作。然而,与此同时,一项任务可能同时决定不再需要它并死亡。因此,计时器可以通过从弱指针创建共享指针并使用该共享指针来检查任务是否仍处于活动状态,前提是它不是 null。std::shared_ptr<Task>std::vector<std::weak_ptr<Task>>

评论

7赞 RoundPi 10/17/2012
:听起来是一个很好的例子,但你能再详细说明一下你的例子吗?我在想当一个任务完成时,它应该已经从 std::vector<std::weak_ptr<Task>>中删除,而无需定期检查。所以不确定 std::vector<std::weak_ptr<>> 在这里是否很有帮助。
0赞 uuu777 1/23/2014
与队列类似的注释:假设您有对象,并且您将它们排队等待某些资源,对象可能会在等待时被删除。因此,如果您排队weak_ptrs则不必费心从队列中删除条目。Weak_ptrs将被作废,然后在被淘汰时被丢弃。
2赞 Kerrek SB 1/23/2014
@zzz777:使对象失效的逻辑甚至可能不知道观察者队列或向量的存在。因此,观察者对弱指针执行一个单独的循环,作用于仍然活着的指针,并从容器中删除死指针......
1赞 uuu777 1/23/2014
@KerekSB:是的,在排队的情况下,您甚至不必进行单独的循环 - 然后资源可用,您可以丢弃过期的weak_ptrs(如果有),直到获得有效的(如果有的话)。
1赞 curiousguy 12/9/2017
您也可以让线程从集合中删除自身,但这会创建依赖关系并需要锁定。
180赞 Offirmo 7/3/2013 #3

另一个答案,希望更简单。(面向其他 Google 员工)

假设你有 和 对象。TeamMember

显然,这是一种关系:对象将具有指向其 .而且,成员可能还会有一个指向其对象的后退指针。TeamMembersTeam

然后你就有一个依赖周期。如果使用 ,当您放弃对对象的引用时,对象将不再自动释放,因为它们以循环方式相互引用。这是内存泄漏。shared_ptr

您可以使用 来破坏此设置。“所有者”通常使用“a”,而“拥有者”则使用a到其父级,并在需要访问其父级时临时将其转换为。weak_ptrshared_ptrweak_ptrshared_ptr

存储一个弱的 ptr :

weak_ptr<Parent> parentWeakPtr_ = parentSharedPtr; // automatic conversion to weak from shared

然后在需要时使用它

shared_ptr<Parent> tempParentSharedPtr = parentWeakPtr_.lock(); // on the stack, from the weak ptr
if( !tempParentSharedPtr ) {
  // yes, it may fail if the parent was freed since we stored weak_ptr
} else {
  // do stuff
}
// tempParentSharedPtr is released when it goes out of scope

评论

2赞 paulm 3/16/2014
这是怎么回事?如果团队被破坏,它将破坏其成员,因此shared_ptr引用计数将为 0 并且也被破坏?
10赞 Offirmo 3/16/2014
@paulm团队不会摧毁“其”成员。重点是共享所有权,因此没有人有释放内存的特殊责任,当不再使用时会自动释放内存。除非有循环......你可能有几支球队共用同一个球员(过去的球队?如果团队对象“拥有”成员,则无需首先使用 a。shared_ptrshared_ptr
2赞 Offirmo 3/16/2014
@paulm 你是对的。但是,既然在这个例子中,团队也被其“团队成员”引用,那么它什么时候会被销毁呢?你所描述的是一个没有循环的情况。shared_ptr
15赞 Mazyod 8/9/2015
我想,这还不错。如果一个成员可以属于多个团队,则使用引用将不起作用。
2赞 Rajesh 1/7/2019
在调用“lock”获取shared_ptr之前,还可以调用“expired”方法来确保对象是否处于活动状态
391赞 sunefred 2/19/2014 #4

std::weak_ptr是解决悬空指针问题的一个很好的方法。仅使用原始指针,无法知道引用的数据是否已被释放。相反,通过让 a 管理数据并向用户提供数据,用户可以通过调用 或 来检查数据的有效性。std::shared_ptrstd::weak_ptrexpired()lock()

您无法单独执行此操作,因为所有实例共享数据的所有权,在删除所有实例之前不会删除该数据。下面是如何检查悬空指针的示例:std::shared_ptrstd::shared_ptrstd::shared_ptrlock()

#include <iostream>
#include <memory>

int main()
{
    // OLD, problem with dangling pointer
    // PROBLEM: ref will point to undefined data!

    int* ptr = new int(10);
    int* ref = ptr;
    delete ptr;

    // NEW
    // SOLUTION: check expired() or lock() to determine if pointer is valid

    // empty definition
    std::shared_ptr<int> sptr;

    // takes ownership of pointer
    sptr.reset(new int);
    *sptr = 10;

    // get pointer to data without taking ownership
    std::weak_ptr<int> weak1 = sptr;

    // deletes managed object, acquires new pointer
    sptr.reset(new int);
    *sptr = 5;

    // get pointer to new data without taking ownership
    std::weak_ptr<int> weak2 = sptr;

    // weak1 is expired!
    if(auto tmp = weak1.lock())
        std::cout << "weak1 value is " << *tmp << '\n';
    else
        std::cout << "weak1 is expired\n";
    
    // weak2 points to new data (5)
    if(auto tmp = weak2.lock())
        std::cout << "weak2 value is " << *tmp << '\n';
    else
        std::cout << "weak2 is expired\n";
}

输出

weak1 is expired
weak2 value is 5

评论

1赞 user 12/25/2016
好的,就好像您在本地将(拥有)指针设置为 null(删除内存),则指向同一内存的所有其他(弱)指针也设置为 null
1赞 Sahib Yar 5/14/2019
std::weak_ptr::lock创建一个共享托管对象所有权的新对象。std::shared_ptr
0赞 Fennekin 1/24/2023
理解了这个概念。但什么是实际用例?有什么例子吗?
0赞 curiousguy 2/26/2023
@Fennekin 在基于引用的语言(如 Java)中,弱引用的用例几乎相同,也就是说,非常少!weak_ptr
2赞 MYLOGOS 5/12/2014 #5

http://en.cppreference.com/w/cpp/memory/weak_ptr std::weak_ptr 是一个智能指针,它保存对由 std::shared_ptr 管理的对象的非所有权(“弱”)引用。必须将其转换为 std::shared_ptr 才能访问引用的对象。

std::weak_ptr 模型临时所有权:当一个对象仅当它存在时才需要访问它,并且它可能随时被其他人删除时,std::weak_ptr 用于跟踪该对象,并将其转换为 std::shared_ptr 以承担临时所有权。如果此时销毁了原始 std::shared_ptr,则对象的生存期将延长,直到临时 std::shared_ptr 也被销毁。

此外,std::weak_ptr 用于断开 std::shared_ptr 的循环引用。

评论

0赞 curiousguy 6/15/2016
"打破循环引用“如何?
0赞 Milan 4/14/2022
@curiousguy Rainer Grimm 在解释这些循环引用以及如何使用以下方法打破它们方面做得很好:“Back to Basics: Smart Pointers - Rainer Grimm - CppCon 2020” -- youtu.be/sQCSX7vmmKY?t=2496 (出于某种原因,如果视频从头开始,则跳到 41:36std::weak_ptr)
0赞 underscore_d 3/24/2023
这只是链接的复制/粘贴,人们搜索时已经可以找到它。它什么也没增加。
18赞 Cookie 6/5/2014 #6

weak_ptr检查对象是否正确删除也很好 - 尤其是在单元测试中。典型用例可能如下所示:

std::weak_ptr<X> weak_x{ shared_x };
shared_x.reset();
BOOST_CHECK(weak_x.lock());
... //do something that should remove all other copies of shared_x and hence destroy x
BOOST_CHECK(!weak_x.lock());
22赞 Emile Cormier 12/16/2015 #7

当不能保证在调用异步处理程序时目标对象仍然存在时,它们对 Boost.Asio 很有用。诀窍是使用 或 lambda 捕获将 a 绑定到异步处理程序对象中。weak_ptrstd::bind

void MyClass::startTimer()
{
    std::weak_ptr<MyClass> weak = shared_from_this();
    timer_.async_wait( [weak](const boost::system::error_code& ec)
    {
        auto self = weak.lock();
        if (self)
        {
            self->handleTimeout();
        }
        else
        {
            std::cout << "Target object no longer exists!\n";
        }
    } );
}

这是 Boost.Asio 示例中常见的惯用语的变体,其中挂起的异步处理程序不会延长目标对象的生存期,但如果删除目标对象,它仍然是安全的。self = shared_from_this()

评论

0赞 Orwellophile 8/8/2018
为什么花了这么长时间才找到这个答案......P.S. 你没有使用你的捕获this
0赞 Emile Cormier 8/9/2018
@Orwellophile固定的。当处理程序调用同一类中的方法时,使用惯用语时的习惯力。self = shared_from_this()
2赞 ashutosh 5/19/2016 #8

共享指针有一个缺点: shared_pointer无法处理父子周期依赖关系。表示如果父类使用共享指针的子类的对象,则在同一文件中,如果子类使用父类的对象。共享指针将无法析构所有对象,甚至在循环依赖场景中,共享指针根本没有调用析构函数。基本上共享指针不支持引用计数机制。

这个缺点我们可以用weak_pointer来克服。

评论

0赞 curiousguy 6/11/2016
弱引用如何处理循环依赖关系?
1赞 Shelby Moore III 1/14/2018
@curiousguy,子项使用对父项的弱引用,则当没有指向父项的共享(强)引用时,可以解除分配父项。因此,当通过子项访问父项时,必须测试弱引用以查看父项是否仍然可用。或者,为了避免这种额外的情况,当对父项和子项的唯一共享引用相互引用时,循环引用跟踪机制(标记扫描或探测引用计数递减,这两种方法都具有较差的渐近性能)可以中断循环共享引用。
0赞 curiousguy 1/18/2018
@ShelbyMooreIII“必须测试以查看父项是否仍然可用”是的,并且您必须能够对不可用的情况做出正确的反应!这不会发生在真正的(即强)参考中。这意味着弱引用不是替换的下降:它需要改变逻辑。
2赞 Shelby Moore III 1/20/2018
@curiousguy你没有问“如何在不改变程序逻辑的情况下处理循环依赖关系作为直接替代品?weak_ptrshared_ptr
20赞 Saurav Sahu 10/21/2016 #9

shared_ptr :保存真实对象。

weak_ptr :用于连接到真正的所有者或否则返回 NULL。lockshared_ptr

PTR弱

粗略地说,角色类似于房屋中介的角色。如果没有中介,要想租到房子,我们可能不得不在城市里随机检查房子。代理商确保我们只访问那些仍然可以进入和出租的房屋。weak_ptr

评论

0赞 Milan 4/14/2022
(场景 1:通过它实例化临时,然后使用该临时shared_ptr。(场景 2:检查 main 是否有效或 /?你不认为场景 2 更直接(而且可能更快)吗?这件事让我很困惑,它仍然让我想知道我们为什么需要?(处理循环依赖关系除外)。提前非常感谢您!weak_ptrshared_ptrshared_ptrnullptrNULLweak_ptr
1赞 o_oTurtle 7/21/2022
@Milan 就我而言,将保证这是以原子方式完成的。因此,场景 2 可能会遗漏一些情况:在您检查它是否有效后,您将将其分配给另一个;如果线程切换并且赋值被截获,并且原始线程清空其资源,该怎么办?您将在赋值中获得一个空指针,但将其视为代码块中的有效指针,这可能会导致奇怪的崩溃。方案 1 不会,因为所有操作都“同时”关闭(相当于)。std::weak_ptrlockstd::shared_ptr
24赞 Jeremy 12/8/2017 #10

使用指针时,请务必了解可用的不同类型的指针,以及何时使用每个指针。在以下两个类别中,有四种类型的指针:

  • 原始指针:
    • 原始指针 [ 即SomeClass* ptrToSomeClass = new SomeClass(); ]
  • 智能指针:
    • 唯一指针 [ 即
      std::unique_ptr<SomeClass> uniquePtrToSomeClass ( new SomeClass() );
      ]
    • 共享指针 [ 即
      std::shared_ptr<SomeClass> sharedPtrToSomeClass ( new SomeClass() );
      ]
    • 弱指针 [ 即
      std::weak_ptr<SomeClass> weakPtrToSomeWeakOrSharedPtr ( weakOrSharedPtr );
      ]

原始指针(有时称为“旧指针”或“C 指针”)提供“基本”指针行为,是 bug 和内存泄漏的常见来源。原始指针无法跟踪资源的所有权,开发人员必须手动调用“delete”以确保它们不会造成内存泄漏。如果资源是共享的,这将变得很困难,因为要知道是否有任何对象仍指向该资源可能具有挑战性。出于这些原因,通常应避免使用原始指针,并且仅在范围有限的代码中对性能至关重要的部分使用。

唯一指针是一个基本的智能指针,它“拥有”指向资源的基础原始指针,并负责在“拥有”唯一指针的对象超出范围时调用、删除和释放分配的内存。名称“唯一”是指在给定时间点只有一个对象可以“拥有”唯一指针。所有权可以通过移动命令转移到另一个对象,但绝不能复制或共享唯一指针。由于这些原因,在给定时间只有一个对象需要指针的情况下,唯一指针是原始指针的一个很好的替代方法,这减轻了开发人员在拥有对象生命周期结束时释放内存的需要。

共享指针是另一种类型的智能指针,它们类似于唯一指针,但允许许多对象对共享指针拥有所有权。与唯一指针一样,共享指针负责在完成指向资源的所有对象后释放分配的内存。它通过一种称为引用计数的技术来实现这一点。每当新对象获得共享指针的所有权时,引用计数就会递增 1。同样,当对象超出范围或停止指向资源时,引用计数将递减 1。当引用计数达到零时,将释放分配的内存。由于这些原因,共享指针是一种非常强大的智能指针类型,每当多个对象需要指向同一资源时,都应使用该指针。

最后,弱指针是另一种类型的智能指针,它们不是直接指向资源,而是指向另一个指针(弱指针或共享指针)。弱指针不能直接访问对象,但它们可以判断对象是否仍然存在或是否已过期。弱指针可以临时转换为共享指针,以访问指向对象(前提是它仍然存在)。为了说明这一点,请考虑以下示例:

  • 您很忙,并且有重叠的会议:会议 A 和会议 B
  • 您决定参加会议 A,而您的同事参加会议 B
  • 你告诉你的同事,如果会议 A 结束后会议 B 仍在进行,你将加入
  • 以下两种情况可能会发生:
    • 会议 A 结束,会议 B 仍在进行,因此您加入
    • 会议 A 结束,会议 B 也已结束,因此您无法加入

在此示例中,您有一个指向会议 B 的薄弱指针。您不是会议 B 中的“所有者”,因此它可以在没有您的情况下结束,除非您检查,否则您不知道它是否结束。如果它还没有结束,你可以加入并参与,否则,你不能。这与具有指向会议 B 的共享指针不同,因为这样您将成为会议 A 和会议 B 中的“所有者”(同时参与两者)。

该示例说明了弱指针的工作原理,当对象需要成为外部观察者但不希望共享所有权的责任时,该指针很有用。这在两个对象需要相互指向(也称为循环引用)的情况下特别有用。使用共享指针时,两个对象都不能被释放,因为它们仍然被另一个对象“强烈”指向。当其中一个指针是弱指针时,持有弱指针的对象仍可以在需要时访问另一个对象,前提是该对象仍然存在。

评论

0赞 Cris Luengo 7/22/2023
不应避免使用原始指针。应避免通过原始指针拥有对象,但是在引用某些内容而不拥有它的情况下,原始指针非常有用。例如,指向父级的对象。
2赞 Swapnil 8/12/2018 #11

当我们不想拥有该对象时:

前任:

class A
{
    shared_ptr<int> sPtr1;
    weak_ptr<int> wPtr1;
}

在上面的类中,wPtr1 不拥有 wPtr1 指向的资源。如果资源被删除,则 wPtr1 已过期。

要避免循环依赖,请执行以下操作:

shard_ptr<A> <----| shared_ptr<B> <------
    ^             |          ^          |
    |             |          |          |
    |             |          |          |
    |             |          |          |
    |             |          |          |
class A           |     class B         |
    |             |          |          |
    |             ------------          |
    |                                   |
    -------------------------------------

现在,如果我们对类 B 和 A 进行shared_ptr,则两个指针的use_count均为 2。

当 shared_ptr超出范围时,计数仍保持 1,因此 A 和 B 对象不会被删除。

class B;

class A
{
    shared_ptr<B> sP1; // use weak_ptr instead to avoid CD

public:
    A() {  cout << "A()" << endl; }
    ~A() { cout << "~A()" << endl; }

    void setShared(shared_ptr<B>& p)
    {
        sP1 = p;
    }
};

class B
{
    shared_ptr<A> sP1;

public:
    B() {  cout << "B()" << endl; }
    ~B() { cout << "~B()" << endl; }

    void setShared(shared_ptr<A>& p)
    {
        sP1 = p;
    }
};

int main()
{
    shared_ptr<A> aPtr(new A);
    shared_ptr<B> bPtr(new B);

    aPtr->setShared(bPtr);
    bPtr->setShared(aPtr);

    return 0;  
}

输出:

A()
B()

从输出中我们可以看出,A 和 B 指针永远不会被删除,因此内存泄漏。

为避免此类问题,只需在 A 类中使用 weak_ptr 而不是 shared_ptr这更有意义。

9赞 user2328447 10/28/2018 #12

除了已经提到的其他有效用例之外,在多线程环境中是一个很棒的工具,因为std::weak_ptr

  • 它不拥有该对象,因此不能阻止在不同的线程中删除
  • std::shared_ptr与 is safe against dangling 指针 - 与 in with raw 指针相反std::weak_ptrstd::unique_ptr
  • std::weak_ptr::lock()是一个原子操作(另请参阅 关于 weak_ptr 的线程安全)

考虑一项任务,将目录(~10.000)的所有图像同时加载到内存中(例如,作为缩略图缓存)。显然,最好的方法是处理和管理图像的控制线程,以及加载图像的多个工作线程。现在这是一项简单的任务。这是一个非常简化的实现(省略了等,在实际实现中必须以不同的方式处理线程等)join()

// a simplified class to hold the thumbnail and data
struct ImageData {
  std::string path;
  std::unique_ptr<YourFavoriteImageLibData> image;
};

// a simplified reader fn
void read( std::vector<std::shared_ptr<ImageData>> imagesToLoad ) {
   for( auto& imageData : imagesToLoad )
     imageData->image = YourFavoriteImageLib::load( imageData->path );
}

// a simplified manager
class Manager {
   std::vector<std::shared_ptr<ImageData>> m_imageDatas;
   std::vector<std::unique_ptr<std::thread>> m_threads;
public:
   void load( const std::string& folderPath ) {
      std::vector<std::string> imagePaths = readFolder( folderPath );
      m_imageDatas = createImageDatas( imagePaths );
      const unsigned numThreads = std::thread::hardware_concurrency();
      std::vector<std::vector<std::shared_ptr<ImageData>>> splitDatas = 
        splitImageDatas( m_imageDatas, numThreads );
      for( auto& dataRangeToLoad : splitDatas )
        m_threads.push_back( std::make_unique<std::thread>(read, dataRangeToLoad) );
   }
};

但是,如果您想中断图像的加载,例如,因为用户选择了不同的目录,则会变得更加复杂。或者即使你想摧毁经理。

您需要线程通信,并且必须停止所有加载器线程,然后才能更改字段。否则,加载器将继续加载,直到所有图像都完成 - 即使它们已经过时。在简化的例子中,这并不难,但在实际环境中,事情可能会复杂得多。m_imageDatas

这些线程可能是多个管理器使用的线程池的一部分,其中一些正在停止,而另一些则没有。简单的参数将是一个锁定的队列,这些管理器将他们的图像请求从不同的控制线程推送到该队列中,读取器在另一端以任意顺序弹出请求。因此,沟通变得困难、缓慢且容易出错。在这种情况下,避免任何额外通信的一种非常优雅的方法是与 结合使用。imagesToLoadstd::shared_ptrstd::weak_ptr

// a simplified reader fn
void read( std::vector<std::weak_ptr<ImageData>> imagesToLoad ) {
   for( auto& imageDataWeak : imagesToLoad ) {
     std::shared_ptr<ImageData> imageData = imageDataWeak.lock();
     if( !imageData )
        continue;
     imageData->image = YourFavoriteImageLib::load( imageData->path );
   }
}

// a simplified manager
class Manager {
   std::vector<std::shared_ptr<ImageData>> m_imageDatas;
   std::vector<std::unique_ptr<std::thread>> m_threads;
public:
   void load( const std::string& folderPath ) {
      std::vector<std::string> imagePaths = readFolder( folderPath );
      m_imageDatas = createImageDatas( imagePaths );
      const unsigned numThreads = std::thread::hardware_concurrency();
      std::vector<std::vector<std::weak_ptr<ImageData>>> splitDatas = 
        splitImageDatasToWeak( m_imageDatas, numThreads );
      for( auto& dataRangeToLoad : splitDatas )
        m_threads.push_back( std::make_unique<std::thread>(read, dataRangeToLoad) );
   }
};

此实现几乎与第一个实现一样简单,不需要任何额外的线程通信,并且可以成为实际实现中线程池/队列的一部分。由于会跳过过期的映像,并处理未过期的映像,因此在正常操作期间永远不必停止线程。 你总是可以安全地改变路径或销毁你的管理器,因为如果拥有的指针没有过期,读取器 fn 会检查。

6赞 Escualo 10/28/2018 #13

我看作一个句柄:它允许我 获取 如果它仍然存在,但它不会扩展其 辈子。在以下几种情况下,这种观点是有用的:std::weak_ptr<T>std::shared_ptr<T>std::shared_ptr<T>

// Some sort of image; very expensive to create.
std::shared_ptr< Texture > texture;

// A Widget should be able to quickly get a handle to a Texture. On the
// other hand, I don't want to keep Textures around just because a widget
// may need it.

struct Widget {
    std::weak_ptr< Texture > texture_handle;
    void render() {
        if (auto texture = texture_handle.get(); texture) {
            // do stuff with texture. Warning: `texture`
            // is now extending the lifetime because it
            // is a std::shared_ptr< Texture >.
        } else {
            // gracefully degrade; there's no texture.
        }
    }
};

另一个重要的场景是打破数据结构中的循环。

// Asking for trouble because a node owns the next node, and the next node owns
// the previous node: memory leak; no destructors automatically called.
struct Node {
    std::shared_ptr< Node > next;
    std::shared_ptr< Node > prev;
};

// Asking for trouble because a parent owns its children and children own their
// parents: memory leak; no destructors automatically called.
struct Node {
    std::shared_ptr< Node > parent;
    std::shared_ptr< Node > left_child;
    std::shared_ptr< Node > right_child;
};

// Better: break dependencies using a std::weak_ptr (but not best way to do it;
// see Herb Sutter's talk).
struct Node {
    std::shared_ptr< Node > next;
    std::weak_ptr< Node > prev;
};

// Better: break dependencies using a std::weak_ptr (but not best way to do it;
// see Herb Sutter's talk).
struct Node {
    std::weak_ptr< Node > parent;
    std::shared_ptr< Node > left_child;
    std::shared_ptr< Node > right_child;
};

赫伯·萨特(Herb Sutter)进行了精彩的演讲,解释了语言的最佳使用 功能(在本例中为智能指针)以确保默认无泄漏(意思是:一切都通过结构卡入到位;你几乎无法搞砸它 向上)。这是必看的。

8赞 Andrushenko Alexander 10/7/2020 #14

我看到很多有趣的答案解释了引用计数等,但我缺少一个简单的示例来演示如何使用 防止内存泄漏。在第一个示例中,我使用循环引用的类。当类超出范围时,它们不会被销毁。weak_ptrshared_ptr

#include<iostream>
#include<memory>
using namespace std;

class B;

class A
{
public:
    shared_ptr<B>bptr;
    A() {
        cout << "A created" << endl;
    }
    ~A() {
        cout << "A destroyed" << endl;
    }
};

class B
{
public:
    shared_ptr<A>aptr;
    B() {
        cout << "B created" << endl;
    }
    ~B() {
        cout << "B destroyed" << endl;
    }
};

int main()
{
    {
        shared_ptr<A> a = make_shared<A>();
        shared_ptr<B> b = make_shared<B>();
        a->bptr = b;
        b->aptr = a;
    }
  // put breakpoint here
}

如果运行代码片段,您将看到类已创建,但未销毁:

A created
B created

现在我们改为:shared_ptr'sweak_ptr

class B;
class A
{
public:
    weak_ptr<B>bptr;

    A() {
        cout << "A created" << endl;
    }
    ~A() {
        cout << "A destroyed" << endl;
    }
};

class B
{
public:
    weak_ptr<A>aptr;

    B() {
        cout << "B created" << endl;
    }
    ~B() {
        cout << "B destroyed" << endl;
    }
};

    int main()
    {
        {
            shared_ptr<A> a = make_shared<A>();
            shared_ptr<B> b = make_shared<B>();
            a->bptr = b;
            b->aptr = a;
        }
      // put breakpoint here
    }

这一次,在使用时,我们看到适当的类破坏:weak_ptr

A created
B created
B destroyed
A destroyed

评论

2赞 daparic 12/16/2022
像这样的工作示例胜过千言万语。谢谢。
2赞 A. Mashreghi 3/24/2022 #15

受 @offirmo 响应的启发,我编写了以下代码,然后运行 Visual Studio 诊断工具:

#include <iostream>
#include <vector>
#include <memory>

using namespace std;

struct Member;
struct Team;

struct Member {
    int x = 0;

    Member(int xArg) {
        x = xArg;
    }

    shared_ptr<Team> teamPointer;
};

struct Team {
    vector<shared_ptr<Member>> members;
};

void foo() {
    auto t1 = make_shared<Team>();
    for (int i = 0; i < 1000000; i++) {
        t1->members.push_back(make_shared<Member>(i));
        t1->members.back()->teamPointer = t1;
    }
}

int main() {
    foo();

    while (1);

    return 0;
}

当指向团队的成员指针shared_ptr teamPointer 时,foo() 完成后内存不是空闲的,即它保持在 150 MB 左右。

但是,如果在诊断工具中将其更改为 weak_ptr teamPointer,您将看到一个峰值,然后内存使用率恢复到大约 2MB。