我写了一些这样的代码:
shared_ptr<int> r = make_shared<int>();
int *ar = r.get();
delete ar; // report double free or corruption
// still some code
当代码运行到delete ar;
时,程序崩溃并报告``双重释放或损坏'',我很困惑为什么双重释放? “ r”仍在范围内,没有从堆栈中弹出。删除运算符做魔术吗?是否知道原始指针当前由智能指针处理?然后将“ r”中的计数器自动减为零?
我知道不建议进行该操作,但是我想知道为什么?
答案 0 :(得分:1)
您要删除不是来自new
的指针,因此您有未定义的行为(可能发生任何事情)。
来自delete上的cppreference:
对于第一种(非数组)形式,表达式必须是指向对象类型的指针或上下文可隐式转换为此类指针的类类型,并且其值必须为null或由以下项创建的非数组对象的指针new表达式,或指向由new表达式创建的非数组对象的基础子对象的指针。如果expression是其他任何东西,包括它是通过new-expression的数组形式获得的指针,则行为是不确定的。
如果分配是通过new
完成的,那么我们可以确定我们拥有的指针可以在delete
上使用。但是对于shared_ptr.get()
,我们不确定是否可以使用delete
,因为它可能不是new
返回的实际指针。
答案 1 :(得分:0)
这当然取决于库如何实现make_shared,但是最可能的实现是:
std :: make_shared为两个对象分配一个块:
std :: make_shared()将调用一次内存分配器,然后将调用两次new放置以初始化(调用构造函数)上述两件事。
| block requested from allocator |
| shared_ptr control block | X object |
#1 #2 #3
这意味着内存分配器提供了一个大块,其地址为#1。 共享指针然后将其用于控制块(#1)和实际包含的对象(#2)。 当使用shred_ptr(.get())保留的实际对象调用delete时,将调用delete(#2)。 由于分配器不知道#2,因此会出现损坏错误。
答案 2 :(得分:-1)
shared_ptr<int> r = make_shared<int>();
不能保证这将调用new int
(无论如何用户都无法严格观察)或更一般的调用new T
(对于用户定义的特定于类的{{1 }});实际上,它不会(不能保证不会)。
接下来的讨论不仅涉及operator new
,还涉及具有所有权语义的“智能指针”。对于任何拥有的智能指针 smart_owning :
make_owning 而不是shared_ptr
的主要动机是要避免在没有所有者的情况下随时进行内存分配;当表达式的求值顺序不能保证对参数列表中子表达式的求值恰好在调用该函数之前时,这在C ++中至关重要。历史上使用C ++:
smart_owning<T>(new T)
可以评估为:
f (smart_owning<T>(new T), smart_owning<U>(new U));
这样T *temp1 = new T;
U *temp2 = new U;
auto &&temp3 = smart_owning<T>(temp1);
auto &&temp4 = smart_owning<U>(temp2);
和temp1
在很短的时间内不会由任何拥有的对象管理:
temp2
会引发异常因此,如果引发异常,则new U
或temp1
可能会泄漏(但不能同时泄漏),这是我们首先要避免的确切问题。这意味着涉及拥有智能指针的构造的复合表达式不是一个好主意。很好:
temp2
通常,使用函数调用auto &&temp_t = smart_owning<T>(new T);
auto &&temp_u = smart_owning<U>(new U);
f (temp_t, temp_u);
包含多个子表达式的表达式被认为是合理的(就子表达式的数量而言,这是一个非常简单的表达式)。禁止使用此类表达式非常烦人,并且很难证明其合理性。
[这是一个原因,在我看来,这是最有说服力的原因,为什么C ++标准化委员会删除了评估顺序的不确定性,以致此类代码不安全。 (这不仅是分配内存的问题,而且是所有托管分配的问题,例如文件描述符,数据库句柄……)]
因为代码经常需要在子表达式中执行诸如f (smart_owning<T>(new T), smart_owning<U>(new U))
之类的事情,并且因为告诉程序员分解包含许多简单行的分配的适度复杂的表达式并不吸引人(更多的代码行并不意味着易于阅读),库编写者提供了一个简单的解决方法:一个函数来创建具有动态生命周期的对象并将其创建在一起。这就解决了求值问题的顺序(但是起初很复杂,因为它需要完美传递构造函数的参数)。
将两个任务分配给一个函数(分配一个smart_owning<T>(allocate_T())
的实例和一个T
的实例)可以自由地进行有趣的优化:您可以通过同时放置两个托管对象来避免动态分配和它的所有者彼此相邻。
但是,这又不是smart_owning
之类的功能的主要用途。
因为专有所有权智能指针根据定义不需要保留引用计数,并且根据定义不需要在实例之间共享删除器所需的数据,因此可以保留数据在“智能指针”(*)中,则不需要额外的分配来构造make_shared
;但仍添加了unique_ptr
函数模板,以避免悬而未决的指针问题,而不是优化非事物(不是在第一手就完成的分配)。
(*),BTW表示唯一所有者“智能指针”确实没有 pointer 语义,因为指针语义意味着您可以复制“ pointer”,并且您不能拥有指向同一实例的唯一所有者的两个副本;无论如何,“智能指针”绝不是指针,该术语具有误导性。
摘要:
make_unique
进行可选优化,其中 make_shared<T>
没有单独的动态内存分配:没有T
。显然,仍然存在具有另一个operator new(sizeof (T))
动态生存期的实例: placement new 。
如果用显式销毁替换显式内存释放,并在该点后立即添加暂停:
operator new
该程序可能会正常运行; 显式销毁由智能指针管理的对象仍然是不合逻辑,不可辩驳,不正确的。这不是“您的”资源。如果class C {
public:
~C();
};
shared_ptr<C> r = make_shared<C>();
C *ar = r.get();
ar->~C();
pause(); // stops the program forever
可能会异常退出,则拥有的智能指针将尝试破坏甚至不存在的托管对象。
答案 3 :(得分:-2)
<罢工> 参见here。我说:
std :: shared_ptr是一个智能指针,它通过指针保留对象的共享所有权。几个shared_ptr对象可能拥有同一对象。发生以下任一情况时,对象将被销毁并释放其内存:
- 拥有该对象的最后剩余的shared_ptr被破坏;
- 通过operator =或reset()为拥有该对象的最后剩余shared_ptr分配了另一个指针。
在构造过程中,使用delete-expression或提供给shared_ptr的自定义删除器破坏对象。
因此指针被 shared_ptr
删除。您不应该自己删除存储的指针
更新:
对不起,我没有意识到还有更多的语句,并且指针没有超出范围。
我正在阅读更多内容,但标准并未对get()
的行为进行过多说明,但是here是一个注释,我引用:
shared_ptr可能在存储指向另一个对象的指针时共享对象的所有权。 get()返回存储的指针,而不是托管的指针。
因此,看起来允许get()
返回的指针不必与shared_ptr
分配的指针相同(大概使用new
)。因此,delete
指针是未定义的行为。我将进一步研究细节。
更新2:
该标准在第20.7.2.2.6节(约make_shared
)中说明:
6备注:鼓励但不要求执行不超过一个的内存分配。 [注意:这提供了等效于侵入式智能指针的效率。 —尾注]
7 [注意:这些函数通常会分配比sizeof(T)更多的内存,以允许内部记账结构(例如引用计数)。 —尾注]
因此make_shared
的特定实现可以分配一个(或更多)内存块,并使用该内存的一部分来初始化存储的指针(但可能不会分配所有内存)。 get()
必须返回一个指向存储对象的指针,但是如前所述,标准没有要求get()
返回的指针必须是new
分配的指针。因此,delete
指针是未定义的行为,您会发出一个信号,但是任何事情都会发生。