好的,我知道有关此错误的大量帖子,但我找不到一个简单的例子和对问题的明确解释。免责声明:我发誓这不是一个任务:)我只是在玩智能指针,想出来,这就发生了。
shared_ptr<string> a = make_shared<string>("This is a string.");
shared_ptr<string> b = make_shared<string>("This is b string.");
cout << a.use_count() << " " << b.use_count() << endl;
a.reset(b.get());
cout << a.use_count() << " " << b.use_count() << endl;
在这段代码之后,我希望a指向b - 就是这种情况 - 并且b的使用次数为2.相反,我得到以下输出:
1 1
1 1
很公平,我想。毕竟,我正在传递一个原始指针,编译器/运行时应该知道它的相关控制块是什么,对吧?对?! (好吧,我真的希望编译器能够解决这个问题,但我认为这是一个简单的案例,如果不这样做,必须有充分的理由)
继续,我有以下代码
cout << a << " " << *a << endl;
cout << b << " " << *b << endl;
很好地打印出来
0x7f8f10403288 This is b string.
0x7f8f10403288 This is b string.
好的,这正是我的预期:现在指向b。但是,紧接着,我有一个返回0并且我离开了main(),而输出则喊出以下
tests(62775,0x7fff7940d000) malloc: *** error for object 0x7f8f10403288: pointer being freed was not allocated
*** set a breakpoint in malloc_error_break to debug
现在我很困惑:正在释放哪个指针被释放?我的钱会在a或b上,因为它们指向同一个东西,自然它们超出范围,因此它们的删除在同一个指针上被调用两次。我对吗?我错过了什么吗?还有其他我绝对应该知道的吗?
编辑我尝试了相当于哑指针的
string* a = new string("This is a string");
string* b = new string("This is b string");
a = b;
cout << *a << endl << *b << endl;
这次代码并没有窒息。那么,智能指针周围是否有一些特殊的事情发生?
答案 0 :(得分:7)
你永远不应该从智能指针中get
一个原始指针,并将其传递给另一个智能指针类型(使用a.reset(b.get());
)。
发生的事情是智能指针a
和b
的“所有者组”都在尝试管理相同的原始指针。当您将原始指针传递给reset
函数时,shared_ptr a
会假定它正在获取一个非拥有指针,因此它将管理原始指针的生命周期。删除所有权组中的最后一个shared_ptr时,将取消分配关联的内存。
问题是b
删除了字符串,然后a
尝试在销毁时再次删除该字符串。你永远不应该尝试两次释放相同的内存。否则行为是未定义的,您可能会遇到异常,或者您可能会覆盖您不应该覆盖的内存(因为它已被重新分配),这可能会导致程序中不相关部分出现各种错误。
当然,这发生在return语句之后,因为那时智能指针a
和b
超出范围(在堆栈上)并被破坏。
要分配共享指针,您应该简单地说:
a = b;
这将自动解除分配a
指向的任何内容,并将其设置为b
,但智能指针将知道它们都指向同一个对象,而不是尝试独立管理相同字符串的生命周期。
基本上,共享指针的operator=
(以及复制构造函数)会创建属于同一组的新共享指针(共享相同的引用计数)。因此,两个共享指针的use_count()
将返回2
(因为它们彼此都知道),并且只有在最后一个共享指针被破坏时才会释放内存。
答案 1 :(得分:5)
reset
函数需要一个指向新分配对象的指针,而不是已由另一个共享指针管理的对象。所以你的怀疑是正确的。 a
和b
都认为他们是指针的唯一所有者。因此,当它们超出范围时,它们都会在各自的析构函数中调用相同指针上的delete。如果您希望a
与b
分享所有权,请使用shared_ptr
分配运算符。
a = b;
答案 2 :(得分:3)
由b
拥有的指针被释放两次a
而b
超出范围,因为a
和b
两者都有用该指针的计数为1。
你的哑指针等效并不会释放任何东西,所以它不会崩溃,但如果你在valgrind下运行它,你会发现它泄漏了。
顺便说一句,库确实没有任何好方法可以确定指针是否已被另一个智能指针对象拥有。 (这不是编译器的工作---编译器只是翻译代码。)控制块不一定靠近对象。即使它不存在,也可以触发分段错误。也许shared_ptr
可以维护一个拥有指针的全局哈希表,但那会有太多的开销。所以相反,你必须明智地编写代码。永远不要调用.get()
并将原始指针传递给将取得所有权的函数。
答案 3 :(得分:2)
您需要了解shared_ptr
和make_shared
是库功能,而不是编译器的功能。
编译器不会检查提供它们包含的指针的上下文 - 库函数的行为在标准中指定。实际上,这意味着行为在库的源代码中是硬编码的,并且编译器不会根据使用环境对其进行更改。
get()
成员只返回一个原始指针。它不会导致包含对象以某种方式注册该对象可能由另一个shared_ptr
管理。
另一方面,reset()
成员假设它正在接收无主指针,并声称拥有权。
由于此a.reset(b.get())
导致a
和b
认为他们拥有相同的指针并对其生命周期负责,而且没有意识到其他声明所有权。当a
被销毁时,它会删除该指针。 b
也是如此。净效果是两次删除同一个对象。这是未定义的行为。
使用a = b
使a
和b
共同拥有。
答案 4 :(得分:1)
“智能指针周围有什么特别的事吗?” - 是的!这就是智能指针与原始指针的区别。智能指针会隐式自动执行某些特殊事情:
这是通过类特定的复制操作和析构函数在标准库中完成的。这些特殊的函数会在某些事件发生时被调用 - 就像魔术一样。
使用a=b
是在两个智能指针之间共享所有权的正确方法,但这有点令人费解。 C ++中的典型赋值操作使用称为value-type语义的方法。这意味着被分配对象的整个值实际上是从一个地方复制到另一个地方 - 或者至少系统使它看起来如此。然而,对于智能指针,事情是不同的。智能指针实现了reference type的概念。这意味着赋值仅传输引用并更新引用计数 - 这两个变量随后将引用相同的内存对象。
通常,您不希望在共享指针上调用get()
。它是一个低级功能,专为那些搞杂乱的人而设计。 get()
返回一个原始指针并放弃对程序员的控制 - 绕过智能指针的所有特殊功能。当你使用get()
时,你基本上是在告诉图书馆你知道自己在做什么 - 而且你会小心。如果事实证明并非如此,那么您会遇到诸如pointer being freed was not allocated
等丑陋的运行时错误。