我开始研究 C++11 的智能指针,但我没有看到std::weak_ptr
. 有人可以告诉我什么时候std::weak_ptr
有用/必要吗?
14 回答
std::weak_ptr
是解决悬空指针问题的一个很好的方法。通过仅使用原始指针,不可能知道引用的数据是否已被释放。相反,通过让 astd::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 << *tmp << '\n';
else
std::cout << "weak1 is expired\n";
// weak2 points to new data (5)
if(auto tmp = weak2.lock())
std::cout << *tmp << '\n';
else
std::cout << "weak2 is expired\n";
}
输出
weak1 is expired
5
一个很好的例子是缓存。
对于最近访问的对象,您希望将它们保存在内存中,因此您持有指向它们的强指针。您会定期扫描缓存并确定哪些对象最近未被访问。您不需要将它们保存在内存中,因此您摆脱了强指针。
但是,如果该对象正在使用中并且其他一些代码拥有指向它的强指针怎么办?如果缓存摆脱了它唯一指向对象的指针,它就再也找不到它了。因此,缓存会保留一个指向对象的弱指针,如果它们碰巧留在内存中,则需要查找这些对象。
这正是弱指针所做的——它允许您在对象仍然存在时定位它,但如果没有其他需要它,则不会保留它。
另一个答案,希望更简单。(对于谷歌同事)
假设你有Team
和Member
对象。
显然这是一种关系:Team
对象将具有指向其Members
. 并且成员可能还会有一个指向其Team
对象的反向指针。
然后你有一个依赖循环。如果你使用shared_ptr
,当你放弃对它们的引用时,对象将不再被自动释放,因为它们以循环的方式相互引用。这是内存泄漏。
您可以使用weak_ptr
. “所有者”通常使用shared_ptr
和“拥有”使用 aweak_ptr
到其父级,并在需要访问其父级时将其临时转换为。shared_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
这是@jleahy 给我的一个例子:假设你有一组任务,异步执行,并由std::shared_ptr<Task>
. 您可能希望定期对这些任务执行某些操作,因此计时器事件可能会遍历 astd::vector<std::weak_ptr<Task>>
并给任务一些可做的事情。然而,同时一个任务可能同时决定不再需要它并终止。因此,计时器可以通过从弱指针创建共享指针并使用该共享指针来检查任务是否仍然存在,前提是它不为空。
当您不能保证在调用异步处理程序时目标对象仍然存在时,它们对 Boost.Asio 很有用。诀窍是使用或 lambda 捕获将 a 绑定weak_ptr
到异步处理程序对象中。std::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";
}
} );
}
这是self = shared_from_this()
Boost.Asio 示例中常见的惯用语的变体,其中挂起的异步处理程序不会延长目标对象的生命周期,但如果目标对象被删除,仍然是安全的。
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());
使用指针时,重要的是要了解可用的不同类型的指针以及何时使用每种指针有意义。指针有四种类型,分为以下两类:
- 原始指针:
- 原始指针 [ ie
SomeClass* ptrToSomeClass = new SomeClass();
]
- 原始指针 [ ie
- 智能指针:
- 唯一指针 [ ie
std::unique_ptr<SomeClass> uniquePtrToSomeClass ( new SomeClass() );
] - 共享指针 [ ie
std::shared_ptr<SomeClass> sharedPtrToSomeClass ( new SomeClass() );
] - 弱指针 [ ie
std::weak_ptr<SomeClass> weakPtrToSomeWeakOrSharedPtr ( weakOrSharedPtr );
]
- 唯一指针 [ ie
原始指针(有时称为“遗留指针”或“C 指针”)提供“基本”指针行为,并且是错误和内存泄漏的常见来源。原始指针无法提供跟踪资源所有权的方法,开发人员必须手动调用“删除”以确保他们不会造成内存泄漏。如果资源是共享的,这会变得很困难,因为很难知道是否有任何对象仍然指向该资源。由于这些原因,通常应避免使用原始指针,并且只在代码的性能关键部分使用有限的范围。
唯一指针是一个基本的智能指针,它“拥有”指向资源的底层原始指针,并负责在“拥有”唯一指针的对象超出范围时调用删除并释放分配的内存。“唯一”这个名称是指在给定时间点只有一个对象可以“拥有”唯一指针这一事实。所有权可以通过 move 命令转移到另一个对象,但永远不能复制或共享唯一指针。由于这些原因,在给定时间只有一个对象需要指针的情况下,唯一指针是原始指针的一个很好的替代方案,这使开发人员无需在拥有对象的生命周期结束时释放内存。
共享指针是另一种类型的智能指针,类似于唯一指针,但允许许多对象拥有共享指针的所有权。与唯一指针一样,共享指针负责在所有对象都指向资源后释放分配的内存。它通过一种称为引用计数的技术来实现这一点。每次新对象获得共享指针的所有权时,引用计数都会增加 1。同样,当对象超出范围或停止指向资源时,引用计数减一。当引用计数达到零时,分配的内存被释放。由于这些原因,共享指针是一种非常强大的智能指针,应该在多个对象需要指向同一个资源的任何时候使用。
最后,弱指针是另一种类型的智能指针,它不是直接指向资源,而是指向另一个指针(弱指针或共享指针)。弱指针不能直接访问对象,但可以判断对象是否仍然存在或是否已过期。弱指针可以临时转换为共享指针来访问指向的对象(前提是它仍然存在)。为了说明,请考虑以下示例:
- 您很忙并且有重叠的会议:会议 A 和会议 B
- 您决定参加会议 A,而您的同事参加会议 B
- 您告诉您的同事,如果会议 A 结束后会议 B 仍在进行,您将加入
- 可能会出现以下两种情况:
- 会议 A 结束,会议 B 仍在进行,所以你加入
- 会议 A 结束,会议 B 也结束,因此您无法加入
在示例中,您有一个指向会议 B 的弱指针。您不是会议 B 中的“所有者”,因此它可以在没有您的情况下结束,并且除非您检查,否则您不知道它是否结束。如果还没有结束,你可以加入并参与,否则,你不能。这与拥有指向会议 B 的共享指针不同,因为您将成为会议 A 和会议 B 的“所有者”(同时参与两者)。
该示例说明了弱指针是如何工作的,并且在对象需要成为外部观察者但不希望承担共享所有权的责任时很有用。这在两个对象需要相互指向(也称为循环引用)的情况下特别有用。使用共享指针,两个对象都不能被释放,因为它们仍然被另一个对象“强烈”指向。当其中一个指针是弱指针时,持有弱指针的对象仍然可以在需要时访问另一个对象,只要它仍然存在。
除了其他已经提到的有效用例之外,std::weak_ptr
在多线程环境中是一个很棒的工具,因为
- 它不拥有该对象,因此不能阻止在不同线程中删除
std::shared_ptr
与 结合std::weak_ptr
对悬空指针是安全的 -std::unique_ptr
与与原始指针结合相反std::weak_ptr::lock()
是一个原子操作(另请参见关于 weak_ptr 的线程安全)
考虑一项将目录的所有图像(~10.000)同时加载到内存中的任务(例如,作为缩略图缓存)。显然,最好的方法是处理和管理图像的控制线程和加载图像的多个工作线程。现在这是一项简单的任务。这是一个非常简化的实现(join()
省略了 etc,在实际实现中必须以不同方式处理线程等)
// 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) );
}
};
这个实现几乎和第一个一样简单,不需要任何额外的线程通信,并且可以成为实际实现中线程池/队列的一部分。由于过期的图像被跳过,而未过期的图像被处理,线程在正常操作期间永远不必停止。您始终可以安全地更改路径或销毁您的管理器,因为阅读器 fn 会检查是否拥有指针未过期。
我将std::weak_ptr<T>
其视为 a 的句柄: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 有一个精彩的演讲,解释了语言功能的最佳使用(在这种情况下是智能指针),以确保默认情况下的泄漏自由 (意思是:一切都通过构造点击到位;你几乎不能搞砸)。这是必须观看的。
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 的循环引用。
我看到很多有趣的答案来解释引用计数等,但我错过了一个简单的示例来演示如何使用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's
改为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
}
这一次,在使用时weak_ptr
我们看到了正确的类销毁:
A created
B created
B destroyed
A destroyed
共享指针有个缺点:shared_pointer 不能处理父子循环依赖。表示如果父类使用共享指针使用子类的对象,如果子类使用父类的对象,则在同一个文件中。共享指针将无法销毁所有对象,即使共享指针在循环依赖场景中根本没有调用析构函数。基本上共享指针不支持引用计数机制。
我们可以使用weak_pointer 克服这个缺点。
当我们不想拥有该对象时:
前任:
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 超出 od 范围时,计数仍保持为 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 更有意义。