我最近正在研究C++ STL,我将讨论一个使用
deque
的例子。请注意,deque
只是一个示例,我们还可以使用vector
或其他。
最小的可重现示例是一个简单的多队列生产者-消费者问题。考虑一个可以自己洗衣服的智能洗衣机,以及一个可以从洗衣机中取出衣服并烘干的智能烘干机。洗衣机可以被认为是多件未洗衣服的队列,而烘干机可以被认为是已洗衣服的队列。
令
class Clothes
为衣服单位。生产者将不断生成 Clothes
并将它们添加到洗衣机队列中。当洗衣机完成一件Clothes
后,它会将其移除并将其添加到烘干机队列中。
如果我们用 C 语言来做这件事,使用指针 (
malloc()
) 和链表会很简单。通过移动指针,我们可以保证不会分配重复的 Clothes
。当我们完成后,我们可以简单地调用 free()
。
我们可以在 C++ STL 中做同样的事情吗?实现这一目标的推荐方法是什么?
我运行了一些测试,发现 C++ STL 自己处理内存分配和释放,这让我很困惑。
例如,(假设我们在这种情况下使用
deque
):
std::deque<Clothes> washer = {...}; // only 1 object
std::deque<Clothes> dryer; // empty
// case 1: copy constructor is invoked, 2 memory locations occupied
Clothes c = washer.front();
washer.pop_front(); // destructor is invoked, I think this corresponds to the original Clothes
上面调用了复制构造函数并产生 1 个重复的副本。操作
pop_front()
隐式调用 Clothes
对象的析构函数。但它不会影响c
,因为它是由复制构造函数构造的。现在,Clothes
的数量仍然是一个。
// case 2: copy constructor is not invoked
Clothes &c = washer.front();
washer.pop_front(); // destructor is invoked again, but there is only one Clothes object
上面的代码通过引用获取
washer
中的第一个对象,并没有调用复制构造函数。我假设现在仍然只有一个 Clothes
对象。然而,当我们调用 pop_front()
时,会调用 Clothes
的析构函数。我尝试通过 Clothes
访问 c.val
对象中的一些字段(假设 val
是 Clothes
中的字段),我发现我仍然可以访问它,这意味着 Clothes c
没有被破坏。这就是为什么我很困惑。为什么调用析构函数,哪个 Clothes
对象被析构?
我认为情况1可能是正确的方法,但情况2似乎也是正确的,尽管发生了一些意想不到的事情。
代码中的问题:
您是正确的,在情况 1 中创建了
Cloths
的副本。
您也是正确的,在情况 2 c
中没有复制。c
是对已破坏对象的悬空引用,访问它会导致 未定义的行为。这意味着任何事情都可能发生,包括“工作”代码的出现。
解决方案:
std::shared_ptr<Clothes>
作为容器中的元素。这将避免复制 Clothes
对象并产生悬空引用。std::shared_ptr
自动管理对象(在本例中为 Clothes
类型)的生命周期。当不再需要时它将被释放(这是通过为每个对象保留引用计数来完成的)。shared_ptr
的推荐方法是通过 std::make_shared
。
#include <deque>
#include <memory>
class Clothes
{
// ...
};
int main_()
{
std::deque<std::shared_ptr<Clothes>> washer = { std::make_shared<Clothes>() };
std::deque<std::shared_ptr<Clothes>> dryer;
auto c = washer.front();
washer.pop_front();
dryer.push_back(c);
}
std::unique_ptr
,它支持单个所有者,并且不能复制(只能移动)。然而,在这种情况下,在我看来,std::shared_ptr
使用起来更直接。