这是由一个采访问题激发的:
shared_ptr<void> p(new Foo());
一旦p
超出作用域,Foo
的析构函数是否会被调用?
事实证明是这样的,我不得不查看GCC 1中shared_ptr
的实现,并发现显然控制块保存了一个指向实际类型(Foo
)的指针和一个指向析构函数的指针,当ref计数达到0时,析构函数被调用。
1:对不起,我在我的手机上,我无法复制到impl的链接。
但我还是纳闷:为什么?2为什么需要它?3我在标准中遗漏了什么吗?
另一方面,上面的代码行不能用unique_ptr
编译,因为在这种情况下显然没有引用计数。
3条答案
按热度按时间1tuwyuhd1#
当
.get()
被调用时,std::shared_ptr<T>
示例本身必须跟踪要返回的指针。它的类型总是T*
,除非T
是数组,在这种情况下,它的类型是std::remove_extent_t<T>*
(例如,std::shared_ptr<int[]>::get()
返回int*
)。同样,当
std::shared_ptr<T>
被销毁时,它必须检查它是否是引用它的控制块的最后一个std::shared_ptr
示例。如果是,它必须执行删除器。为了使它工作,控制块必须跟踪传递给删除器的指针。它 * 不一定 * 是T*
或std::remove_extent_t<T>*
类型。它们不相同的原因是,例如,类似下面的代码应该可以工作:
这里,
sp
拥有一个S
类型的对象,并且也指向同一个对象。函数foo
使用std::shared_ptr<int>
,因为它是某个API的一部分,该API需要一个int
对象,只要API没有使用它,该对象就将保持活动状态(但是如果调用者愿意的话,他们也可以让它保持更长的活动时间)。foo
API不关心您给予它的int
是否是某个更大对象的一部分;它只关心在保存int
时它不会被破坏。因此,我们可以创建一个名为ip
的std::shared_ptr<int>
,它指向sp->member
,并将其传递给foo
。现在,这个int
对象只有在包含的S
对象是活动的时候才能存在。因此,只要ip
是活动的,它就必须使 * 整个 *S
对象保持活动状态。我们现在可以调用sp.reset()
,但是S
对象必须保持活动状态,因为仍然有一个shared_ptr
引用它。最后,当ip
被销毁时,它必须销毁 * 整个 *S
对象,而不仅仅是它本身所指向的int
。因此,对于std::shared_ptr<int>
示例ip
来说,存储int*
(当调用.get()
时将返回)是不够的;它所指向的控制块也必须存储X1 M37 N1 X以传递到删除器。出于同样的原因,您的代码将调用
Foo
析构函数,即使它是一个执行析构的std::shared_ptr<void>
。你问:“我在标准中遗漏了什么吗?”我认为你是在问标准是否需要这种行为,如果需要,在标准中是在哪里规定的?答案是肯定的。标准规定
std::shared_ptr<T>
* 存储 * 指针,也可以 * 拥有 * 指针;这两个指针不需要相同。特别地,[util.smartptr.shared.const]/14描述了“[构造]一个shared_ptr
示例,该示例 * 存储 *p
并 * 与 *r
的初始值 * 共享所有权”的构造函数。这样创建的shared_ptr
示例可能拥有一个与它存储的指针不同的指针。但是,当它被销毁时,[util.smartptr.shared.dest]/1适用:如果这是最后一个示例,则删除 owned 指针(而不是 stored 指针)。t40tm48m2#
我假设对于这段代码来说,答案是微不足道的:
每个对
new
的调用都必须由对delete
的调用来平衡。每个构造的对象也必须被析构。因此,如果我不会调用~Foo(),这会令人相当惊讶,并会导致资源泄漏、悬空指针或任何数量的UB,因为析构函数没有被调用。
对我来说,更大的问题是:
shared_ptr
有错误的类型,所以它不应该调用正确的析构函数,这样就不应该编译(就像unique_ptr
失败一样)。原因是我相信这一点天才:
您可以建立指向较大对象之成员的共用指标,只要指向成员的指标存在,就会让较大对象保持作用中。
为了使这个特性起作用,
shared_ptr
和shared_ptr
的控制块都有一个指针,它们可以有不同的类型。控制块总是指向对象,而shared_ptr
指向成员。当你正常地创建一个shared_ptr
时,它们碰巧是相同的类型,指向相同的地址。但显然情况并不总是如此。这也允许用指向
Foo
的控制块生成一个shared_ptr<void>
。这里两者指向相同的地址,但具有不同的类型。控制块知道原始对象的类型以及最后要调用的析构函数。shared_ptr
和控制块可以有不同类型的指针,这就允许使用以下复制构造函数:只要
Y*
可以转换/兼容T*
,你就可以在复制构造过程中改变shared_ptr
的类型。它创建临时
shared_ptr<Foo>
,其中控制块具有Foo*
,然后p
重用相同的控制块。osh3o9ms3#
std::shared_ptr::shared_ptr - cppreference.com
| | |
| - -|- -|
|
constexpr shared_ptr() noexcept;
|(上)||
constexpr shared_ptr( std::nullptr_t ) noexcept;
个|(下)||
template< class Y > explicit shared_ptr( Y* ptr );
|(三)|| ......|一个人。|
....
3-7)建构
shared_ptr
,其中ptr
做为Managed对象的指标。| | |
| - -|- -|
| 对于 (3- 4,6),
Y*
必须可转换为T*
。|(C17之前)|| 如果
T
是U[N]
类型的数组,则 (3- 4,6) 如果Y(*)[N]
无法转换为T*
,则不参与重载解析。如果T
是数组类型U[]
,(3- 4,6) 如果Y(*)[]
不能转换为T*
,则不参与重载解析。否则,(3- 4,6) 如果Y*
不能转换为T*
,则不参与重载解析。|(自C17起)|此外:
1.如果
T
不是数组类型,则使用**delete-expression**delete指针;delete[]
ptr,如果T
是数组类型 (C++17起) 作为删除器。Y
必须是完整类型。删除表达式必须格式正确,具有定义良好的行为,并且不引发任何异常。此外,如果删除表达式格式不正确,此构造函数不参与重载解析。(C++17起)所以基本上使用了第三种形式。
同样,保存引用计数器(强和弱)的数据也保存了对象的析构函数的信息。构造函数的
(3)
形式获取这些信息。请注意,
std::unique_ptr
在默认情况下不保存此类信息,因此在此情况下将失败(无法编译)。