在使用 std::unique_ptr 在退出作用域块时自动释放内存的情况下,为啥不直接使用堆栈呢?

Posted

技术标签:

【中文标题】在使用 std::unique_ptr 在退出作用域块时自动释放内存的情况下,为啥不直接使用堆栈呢?【英文标题】:In the case of using a std::unique_ptr to automatically deallocate memory upon exiting a scoped block, why not just use the stack?在使用 std::unique_ptr 在退出作用域块时自动释放内存的情况下,为什么不直接使用堆栈呢? 【发布时间】:2020-06-10 18:27:09 【问题描述】:

这是关于智能指针的一个很好的答案,例如唯一指针:What is a smart pointer and when should I use one?。

这是他们提供的一个示例,作为唯一指针的最简单用法:

void f()

    
       std::unique_ptr<MyObject> ptr(new MyObject(my_constructor_param));
       ptr->DoSomethingUseful();
     // ptr goes out of scope -- 
      // the MyObject is automatically destroyed.

    // ptr->Oops(); // Compile error: "ptr" not defined
                    // since it is no longer in scope.

然而,这引出了一个问题:在这种情况下,目标是简单地删除对象(释放内存),当它超出范围时,唯一指针指向的对象,为什么不把整个对象放在而是堆栈,像这样??

void f()

    
       MyObject myobj(my_constructor_param);
       myobj.DoSomethingUseful();
     // myobj goes out of scope -- 
      // and is automatically destroyed.

    // myobj.Oops(); // Compile error: "myobj" not defined
                     // since it is no longer in scope.

在我看来,唯一的逻辑可能是某些对象太臭了,它们可能会溢出堆栈,因为堆栈似乎限制在几十 KB 到几 MB (@987654322 @),而一个堆可能有数百 GB!

逻辑是什么?在这里给我一些关于唯一指针这个看似不必要的用例的见解。我错过了什么?

相关:

    “要记住的堆栈的另一个特性是,可以存储在堆栈上的变量的大小是有限制的(因操作系统而异)。分配在堆上的变量不是这种情况。 " (https://gribblelab.org/CBootCamp/7_Memory_Stack_vs_Heap.html)

【问题讨论】:

这不是unique_ptr 的好用例。考虑一下您想要返回动态分配的对象,或者您想要将动态分配的对象存储在容器中的情况。它对多态类型特别有用,我猜这是将它用作堆栈对象的用例。 unique_ptr 的一个很好的用例是,如果它没有在编译时定义,是否以及要分配多少对象。 为了捍卫您复制的答案,该示例是为了演示智能指针的工作原理,而不是演示典型的用例。阅读开头“智能指针应该优先于原始指针。如果您觉得需要使用指针(首先考虑是否真的这样做),您通常会想要使用智能指针,因为这可以缓解原始指针的许多问题,主要是忘记删除对象,泄露内存。”所以前提是您已经决定需要智能指针而不是原始指针 btw MyObject() myobj; 是错字,不是吗? 再想一想,asnwer 的问题是“我什么时候应该使用智能指针?”,所以确实不是最好的例子 【参考方案1】:

虽然这本身并不是一个糟糕的有用示例,但它会产生一些细微的变化。

    多态性
struct Base  void blah()  std::cout << "Base\n";;
struct Derived : Base  void blah() std::cout << "Derived\n";;

void blub(bool which)  
    std::unique_ptr<Base> ptr = which ? new Base : new Derived;
    ptr->blah();

    非标准删除器
 
    auto close = [] (FILE* fp)  fclose(fp);;
    std::unique_ptr<FILE, decltype(close)> ptr(fopen("name"), close);
 // closes file
    动态数组(您可以使用向量)
 
    std:: unique_ptr<int[]> ptr( new int [n]); 
    // From C++14 on, prefer if it is no problem to value-initialize the array
    auto ptr = std::make_unique<int[]>(n);
    // From C++20 on, there is no reason for the naked new
    auto ptr = std::make_unique_for_overwrite<int[]>(n);
    // is equivalent to the first line

编辑:这对于大数组也是合理的,即使在编译时知道大小。如果数组的大小真的很大(这应该非常非常罕见)并且您真的不想冒堆栈溢出的风险,那么这将是一种更安全的可能性。但很可能std::vector 仍然是更好的选择。只有当您的对象类型既不可移动也不可复制时,向量才会出现问题(因为它无法在必要时重新分配自身,因此您基本上可以调用不修改成员函数)。

    有条件地创建对象。 (仅在 C++11 和 14 中,之后使用 std::optional
void blah (bool smth)

    std::unique_ptr<T> opt;
    if (smth) 
        opt = std::unique_ptr<T>(new T);
    

【讨论】:

好例子,我会在你有条件地创建对象实例时添加一个 仅在 C++11 和 14 中。之后我肯定会使用可选的。 很好,请修正语法,opt = new T; 无法编译,可能 opt = std::make_unique()` 但仅限 C++14 std::optional 不是智能指针的替代品,因为它将对象始终存储在堆栈上,没有动态分配。一旦你使用动态分配,它就不是std::optional 我是对你的例子的补充,我的理解是如果静态分配非常大的对象数组会溢出堆栈,但对于动态分配的大型对象数组则不会,这仍然是正确的,对吧?您没有在回答中提到这一点。【参考方案2】:

大小不是主要问题,尽管如果你有递归,它可能很重要,例如(我看到一个库在递归函数中在堆栈上分配 64 KiB 缓冲区。但 Musl 提供 128 KiB 堆栈 [使线程轻量级],所以...)但是对象可能是多态的,甚至不是“就地”创建的,而是从某个函数(作为指针)返回的; unique_ptr 可能很方便存储它。

除此之外,unique_ptr(与 auto_ptr AFAIK 不同)不限于堆栈使用。它也可以是类成员。也不需要实际存储任何东西,您可以随时分配和重置它。

此外,它不限于 C++ 类,您可以在其中存储任何需要清理的内容,例如文件描述符或 FILE*。

【讨论】:

【参考方案3】:

如果你想返回一个指向对象的指针,那么在没有智能指针的情况下,我们必须这样做:

MyClass* someFunc() 
    MyClass* myObjPtr = new MyClass();
    .
    .
    return myObjPtr;


void otherFunc() 
    MyClass* myPtr = someFunc();
    .
    .
    delete myPtr;  // delete explicitely before return.
    return;

如果我们使用智能指针,我们可以这样做:

typedef std::shared_ptr<MyClass> MyClassPtr;

MyClassPtr someFunc() 
    MyClassPtr myObjPtr(new MyClass());
    .
    .
    return myObjPtr;


void otherFunc() 
    MyClassPtr myPtr = someFunc();
    .
    .
    return;  // when smart pointer goes out of scope,
    // the heap allocated object which it contains is deallocated properly.

【讨论】:

以上是关于在使用 std::unique_ptr 在退出作用域块时自动释放内存的情况下,为啥不直接使用堆栈呢?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 std::unique_ptr 重置与赋值不同?

复制接口的 std::unique_ptr [关闭]

如何在 C++ 中使用 std::unique_ptr?

如何在构造函数中使用删除器初始化 std::unique_ptr?

如何在结构上使用 std::unique_ptr?

错误:使用已删除的函数‘std::unique_ptr<...> [关闭]