std::weak_ptr
的任何有用的用途。有人可以告诉我什么时候使用std::weak_ptr
是有用/必要的吗?std::weak_ptr
的任何有用的用途。有人可以告诉我什么时候使用std::weak_ptr
是有用/必要的吗?std::weak_ptr
是解决悬空指针问题的一种非常好的方式。仅使用原始指针无法确定引用的数据是否已被释放。相反,通过让std::shared_ptr
管理数据,并向数据的用户提供std::weak_ptr
,用户可以通过调用expired()
或lock()
来检查数据的有效性。
仅使用std::shared_ptr
无法做到这一点,因为所有std::shared_ptr
实例共享数据的所有权,在所有std::shared_ptr
实例被删除之前不会被移除。以下是使用lock()
检查悬空指针的示例:
#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
std::weak_ptr::lock
创建一个新的 std::shared_ptr
,共享所管理对象的所有权。 - Sahib Yarweak_ptr
的使用情况与基于引用的语言(如 Java)中的弱引用几乎相同,也就是说,非常少! - curiousguy另一个答案,希望更简单易懂。(供其他谷歌搜索者参考)
假设你有Team
和Member
对象。
很明显,这是一种关系: Team
对象将具有指向其Members
的指针。并且成员也很可能会有一个回指针指向他们的Team
对象。
然后,你就会出现依赖环。如果你使用shared_ptr
,当你放弃对它们的引用时,对象将不再自动释放,因为它们以循环方式相互引用。这就是内存泄漏。
你可以通过使用weak_ptr
来打破这种情况。 "所有者"通常使用shared_ptr
,而"被拥有者"则使用一个指向其父级别的weak_ptr
,并在需要访问其父级别时将其暂时转换为shared_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
shared_ptr
的整个意义在于共享所有权,因此没有人有特定的责任来释放内存,当不再使用时,内存会自动释放。除非存在循环引用......您可能有几个团队共享同一个球员(过去的团队?)。如果团队对象“拥有”成员,则首先无需使用 shared_ptr
。 - Offirmoshared_ptr
,那么它什么时候会被销毁呢?你所描述的是没有循环的情况。 - Offirmo以下是一个例子,由 @jleahy 给我提供:假设您有一组异步执行的任务,并由 std::shared_ptr<Task>
管理。 您可能想定期执行某些操作,因此计时器事件可能会遍历 std::vector<std::weak_ptr<Task>>
并为任务提供要做的事情。但是,同时任务可能已经决定不再需要并且终止了。计时器可以通过从弱指针创建共享指针并使用该共享指针(前提是它不是空的)来检查任务是否仍然存在。
weak_ptr
,通过它实例化一个临时的shared_ptr
,然后使用该临时的shared_ptr
。场景2:检查主shared_ptr
是否有效或为nullptr
/NULL
?难道你不觉得场景2更直接(而且可能更快)吗?这件事情让我感到很困惑,我仍然想知道为什么我们需要weak_ptr
?(除了解决循环依赖关系)。非常感谢!) - MilanSomeClass* ptrToSomeClass = new SomeClass();
]std::unique_ptr<SomeClass> uniquePtrToSomeClass ( new SomeClass() );
std::shared_ptr<SomeClass> sharedPtrToSomeClass ( new SomeClass() );
std::weak_ptr<SomeClass> weakPtrToSomeWeakOrSharedPtr ( weakOrSharedPtr );
在这个例子中,您拥有对会议B的弱指针。您不是会议B的“所有者”,因此它可以在没有您的情况下结束,并且除非您检查,否则您不知道它是否已经结束。如果它没有结束,您可以加入并参与,否则您就不能加入。这与拥有对会议B的共享指针不同,因为您将成为会议A和会议B的“所有者”(同时参加两个会议)。
该示例说明了弱指针的工作原理,并且在对象需要成为外部观察者但不想承担共享所有权的责任时非常有用。这在两个对象需要相互指向(即循环引用)的情况下特别有用。使用共享指针,两个对象都无法释放,因为它们仍然被对方强烈地指向。当其中一个指针是弱指针时,持有弱指针的对象仍然可以在需要时访问另一个对象,前提是它仍然存在。
当您在调用异步处理程序时不能保证目标对象仍然存在时,Boost.Asio
与 weak_ptr
结合使用非常有用。诀窍是将一个 weak_ptr
绑定到异步处理程序对象中,使用 std::bind
或 lambda 捕获。
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()
惯用法的变种,其中一个挂起的异步处理程序将不会延长目标对象的生命周期,但如果目标对象被删除,它仍然是安全的。
this
的捕获。 - Orwellophileself = shared_from_this()
習慣成自然。 - Emile Cormierweak_ptr
,通过它实例化一个临时的shared_ptr
,然后使用该临时shared_ptr
。场景2:检查主shared_ptr
是否有效或为nullptr
/NULL
?你不认为场景2更直接(可能更快)吗?这件事让我很困惑,我仍然在想为什么我们需要weak_ptr
(除了解决循环依赖)。非常感谢! - Milanstd::weak_ptr
将保证lock
是原子性的。因此,情况2可能会错过一些情况:在您检查它是否有效之后,您将将其分配给另一个变量;如果线程切换并拦截了分配,并且在原始std::shared_ptr
清空其资源之间发生了这种情况,那么您将在分配中获得一个空指针,但在代码块中将其视为有效,这可能会导致奇怪的崩溃。情况1不会出现这种情况,因为所有操作都是同时进行的(相当于)。 - o_oTurtleweak_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());
std::weak_ptr
在多线程环境中是一个非常好用的工具,因为:
std::shared_ptr
与 std::weak_ptr
结合使用可以安全地避免悬垂指针问题,与 std::unique_ptr
结合使用的原始指针相比更加可靠std::weak_ptr::lock()
是一个原子操作(参见:关于 weak_ptr 的线程安全性)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
字段之前停止所有的加载器线程。否则,即使它们已经过时,加载器仍将继续加载,直到所有图片都完成。在简化的示例中,这并不太难,但在实际环境中,事情可能会变得更加复杂。
这些线程可能是多个管理器使用的线程池的一部分,其中一些正在停止,一些没有,等等。简单的参数 imagesToLoad
将是一个被锁定的队列,这些管理器从不同的控制线程推送其图像请求,读取器以任意顺序在另一端弹出这些请求。因此,通信变得困难、缓慢和容易出错。在这种情况下避免任何额外的通信的一种非常优雅的方式是使用 std::shared_ptr
与 std::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) );
}
};
这种实现方式几乎和第一种一样简单,不需要任何额外的线程通信,并且可以成为线程池/队列中的一部分。由于跳过了过期的图像,处理未过期的图像,线程在正常运行期间永远不需要停止。您可以随时安全地更改路径或销毁管理器,因为读取器检查是否拥有的指针已过期。
我看到了很多有趣的答案,解释了引用计数等内容,但是缺少一个简单的示例,演示如何使用weak_ptr
来防止内存泄漏。在第一个示例中,我在循环引用的类中使用了shared_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
改为weak_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
}
A created
B created
B destroyed
A destroyed