当一个对象b
在内部引用并使用它不拥有的对象a
时,a
的死亡会使b
生病。这是一个简单的例子来说明这一点:
#include <iostream>
const int my_int = 5;
class A {
private:
int n_;
public:
int n() const { return n_; }
A(int);
};
A::A(int n__) : n_(n__) {}
class B {
private:
const A *ap_;
public:
int m() const { return 1 + ap_->n(); }
explicit B(const A *);
};
B::B(const A *const ap__) : ap_(ap__) {}
int main()
{
std::cout << "Will put an unnamed A on the heap.\n";
A *const p = new A(my_int);
std::cout << "Have put an unnamed A on the heap.\n";
std::cout << "p->n() == " << p->n() << "\n";
B b(p);
std::cout << "b. m() == " << b. m() << "\n";
std::cout << "Will delete the unnamed A from the heap.\n";
delete p;
std::cout << "Have deleted the unnamed A from the heap.\n";
std::cout << "b. m() == " << b. m() << "\n"; // error
return 0;
}
当然,可以通过让b
保留a
的副本而不是指向它的指针来解决这个问题,但假设b
不想保留副本(因为{ {1}}占用大量内存或其他原因)。假设a
仅仅指代现有的b
。当a
悄然死亡时,a
从未注意到。然后,当b
尝试使用b
时,会产生不可预测的行为。
在我的计算机上,示例的输出恰好是这样的:
a
但是,在您的计算机上,结果可能是段错误或谁知道什么。
我的例子的麻烦似乎在于,这个例子间接打破了Will put an unnamed A on the heap.
Have put an unnamed A on the heap.
p->n() == 5
b. m() == 6
Will delete the unnamed A from the heap.
Have deleted the unnamed A from the heap.
b. m() == 1
的封装,让程序员记住b
的持续有效性取决于{{1}的继续存在}}。当程序员忘记时,程序会中断。因此,受骚扰的程序员在b
工作时必须记住a
,即使类型A不关心类型B 。如您所知,面向对象的程序员如果能够帮助它们,则不必考虑这些琐事。
我在编程时偶尔会遇到这个问题。我今天又见过它了。有人认为,不知何故,应该存在一个优雅的设计模式来维持b
的正确封装,从程序员转移到编译器,负责记住a
对b
的依赖。存在 - 并且该模式从根本上应该包含比智能指针和完整引用计数更精细的东西。但是,也许我错了。也许这正是他们的引用计数的智能指针。无论哪种方式,我都不知道适用于问题的正确模式,也不知道修复代码的最佳方法。
如果你知道,你会告诉它吗?
Here是我在Stackoverflow上已经注意到的最接近相关的答案;但是,除了使用一两个我不理解的单词之外,无论如何,这个答案似乎都没有回答这个问题。
(我的编译器仍然不能很好地支持C ++ 11,但如果C ++ 11带来了一个专门用于解决我的问题的功能,那么当然我应该有兴趣了解它。但是,不可否认,我的问题主要涉及OO /范围基础。问题是对底层设计模式比对最新编译器的这个或那个新特性更感兴趣。)
给读者注意
一些好的答案使这个问题更加突出。在Stackoverflow上,如您所知,提问者有责任接受最佳答案,以便(当您在几个月或几年后阅读时),您将不必搜索它。
然而,这是两个答案的组合,最能回答这个问题。你应该读到两个:
答案 0 :(得分:4)
这取决于第一个对象被删除的原因。如果它是 只是因为有人认为没有人使用它,那么你可能会 能够避免智能指针的问题 - 你仍然 必须注意周期,但如果使用的原因 动态分配只是为了避免过于昂贵的副本, 对象可能只是数据,并没有任何数据 指针,所以你很安全。
当然,更常见的是对象存在的原因
删除是因为程序逻辑需要它,并推迟
通过某种智能指针删除会破坏
程序逻辑。在这种情况下,类具有指针
必须通知对象该对象已被删除,a
因此行事。在这种情况下,观察者模式是
标准解决方案您的对象b
会在A
注册
对象,被告知其消亡,并做任何必须做的事情
结果。如果我们采用您的B
类的简单情况
是Session
,A
类是Connection
。如果是实际的
连接丢失,Connection
课程将被通知,并且
自毁。这样做(在它的析构函数中),它会
通知所有注册的对象。如果你在
服务器,Session
类可能会记录问题和
自我毁灭本身 - 掉线连接终止了
会话。但是,在客户端中,Session
类可能会尝试
创建一个新的Connection
,并且只在中止会话
失败。
答案 1 :(得分:3)
智能解决方案是使用智能指针,如果你真的需要只保留一个对象,而不是多个副本!
C ++ 11为您提供了multiple choices可供选择:
std::unique_ptr
std::shared_ptr
std::weak_ptr
在你的情况下,我认为你需要第二个:std::shared_ptr
。
答案 2 :(得分:3)
我可以想到两个特殊的解决方案:
在共享所有权解决方案中,a
的生命周期由所有者的数量的计数器确定,只有当没有所有者时才会a
'一生终于结束了。这通常使用std::shared_ptr<A>
实现。
在观察者解决方案中,当a
传递给b
时,a
会记住b
拥有对它的引用。然后,当a
死亡时,它会立即通知b
或留下“{token}”后面的b
,以便在下一次访问尝试时得到通知。
直接通知通常通过维护当前引用者的列表并在销毁时调用每个引用来处理(因此可以删除它们的引用)。这是典型的观察者模式。
间接通知通常通过让代理对象进入a
来处理。当a
死亡时,通知代理(O(1)),当b
尝试访问a
时,它必须通过代理。自行实现这一点有很多困难,因此更好的方法是使用标准设施:std::shared_ptr
这次加上std::weak_ptr
。
最后,这些解决方案并不相同。
a
在b
生存期间不会死亡,而观察员计划允许a
先死亡b
立即对a
的死亡做出反应,但同时可能会使程序更加脆弱(在a
执行析构函数期间,你不应该抛出任何例外)选择你自己的毒药:)
答案 3 :(得分:1)
您正在寻找的模式是“如果您拥有一个对象,请不要放弃对它的非拥有引用,然后销毁它。”我不认为它有更好的名字,但它真的只是很好的C ++礼仪。如果你放弃一个指向一个可以保持它的对象的指针,你需要知道该对象可能依赖指针保持有效。在这种情况下,您只需确保对象的生命周期超过非拥有对象的生命周期。
如果你正在传入一个函数,并且你可以假设该函数没有使用全局状态,那么我不会担心它 - 一旦函数返回,你应该假设它是用指针完成的
正如Nawaz所说,停止担心这种情况的真正方法是使用具有适当所有权语义的智能指针。如果要将所有权转移到其他对象,请使用std::unique_ptr
。如果您想共享所有权,请使用std::shared_ptr
。如果你想使用一个非拥有的原始指针,我不会说不,但至少知道它可能导致的问题。
为了避免过早地意外删除对象,可以执行以下操作:
const std::unique_ptr<A> p(new A(my_int));
B b(p.get());
// the A object will be destroyed at the end of the current scope
这里你没有std::shared_ptr
的(非常小的!)开销。我不确定我是否真的推荐它。全智能而不是半智能更为可取。 const std::unique_ptr
具有与boost::scoped_ptr
类似的所有权语义:它表示“我拥有这个而且没有其他人会这样做。”不同之处在于,您无法像对reset
那样在此const std::unique_ptr
上致电boost::scoped_ptr
。