关于new和delete的用法,以及Stroustrup的建议

30
关于 `new` 和 `delete` 的使用,以及 Stroustrup 的建议...
他说过类似这样的话(但不是完全一样,这是我的笔记):
"一个经验法则是,`new` 应该出现在构造函数和类似操作中,`delete` 应该出现在析构函数中。此外,`new` 经常用于资源句柄的参数中。否则,避免使用 `new` 和 `delete`,应该使用资源句柄 (智能指针) 代替。"
我想知道有经验的 C++11 开发者是否真正应用了这个建议。
我的印象是,哇,这似乎是一个非常酷的规则要遵循。但后来我有些怀疑,就像对于任何通用规则一样。最终你仍然会在必要的地方使用 `new` 和 `delete`。但也许这个规则是一个好的指南,我不知道。

6
最终你会在必要的地方使用new和delete。幸运的是,这并非必须。 :) - user1804599
我还没有花时间学习C++11,但我希望这个规则也能变得有用 :) - user3111311
5
在过去的十年中,我通常使用 Boost 库并借助公司内部库来遵循这个规则。C++11 只是将这个已经确立的实践标准化了。 - Cubbi
如果我没错的话,主要的想法是将new和delete放在非常低的层级中,并将它们从更接近用户的高层级代码中移除。 - user3111311
1
与此问题无关,但我想回答一下隐式转换的问题(现已删除):我曾多次被意外的隐式转换所困扰,因此我建议“默认显式”。在考虑一个构造函数Foo(const Bar&)时,我总是问自己:“如果一个函数接受一个Foo参数,而我传递一个Bar对象,我希望它能够默默地工作,还是更可能是程序员的疏忽/误解?”通常情况下,后者才是真正的情况。隐式转换对于可变到const迭代器等情况很好,但大多数情况下,我发现我不想要它们。 - Angew is no longer proud of SO
随着newdelete的贬低,也许将(de)allocation从库函数推广到运算符是一个错误... - CTMacUser
3个回答

48

这是一个很好的规则。事实上,您可以通过使用适当的make_函数,在智能指针的参数中避免使用new。例如,不要写成:

std::shared_ptr<int> p(new int(5));

你通常可以做以下事情:

auto p = std::make_shared<int>(5);

这样做的好处是更加安全。虽然还不存在std::make_unique,但计划将其引入到C++14中(它已经在工作草案中了)。如果你现在需要它,有一些现有的实现

你甚至可以更进一步,在构造函数和析构函数中避免使用newdelete。即使是在类成员变量中,如果你总是将动态分配的对象包装在智能指针中,就不再需要自己管理内存了。参见零规则。这个想法是,你的类不需要实现任何形式的所有权语义(SRP),这是智能指针的职责。因此,理论上你永远不需要编写复制/移动构造函数、复制/移动赋值运算符或析构函数,因为隐式定义的函数通常会做正确的事情。


4
提及“零规则”是加分项。使默认构造函数和赋值运算符正常工作是智能指针和资源句柄等通用资源管理方式的最大好处之一。我已经记不清上次写析构函数是什么时候了。如果按照这种方式编写类(由句柄管理资源),您可以避免许多有关类可能具有的各种构造函数、赋值运算符等的常见 C++ 问题,而在 C++11 中,这个问题变得更加严重,即所谓的“三法则”现在成了“五法则”。 - Manu343726
如果您在类成员中使用unique_ptr(我同意如果您的类具有独占所有权,则比原始指针更好),那么您不必编写自己的复制构造函数/赋值运算符吗? - Shea Levy
1
@SheaLevy 取决于你需要深拷贝还是浅拷贝。对于浅拷贝,你不应该想要一个复制构造函数/赋值运算符,因为那会破坏独特的所有权(这是默认行为)。如果你想要深拷贝,你需要拷贝指向的对象,并让新的 unique_ptr 指向它,这将涉及到实现一个复制构造函数/赋值运算符。不过你不需要做任何内存管理,这是最重要的事情。 - Joseph Mansfield

14

这似乎更像是一项调查而不是一个问题,但我们来看一下吧:在应用程序代码中,我通常根本不使用new。由于我们的编码准则,代码确实使用指针,但没有这些“裸”指针实际上正在传递所有权。所有对象都由某个其他对象拥有。

公平地说,当需要分配对象时,分配通常使用类似于std :: make_shared<T> (...)的东西,这有时会出现在应用程序代码中。这种相当彻底的缺乏new(或类似物)的主要原因是对象通常使用有状态分配器进行分配,而不是通过资源管理器进行分配,实际上相当复杂。因此,在应用程序代码中直接使用new或放置版本的内存分配的地方很少。

在一些基础设施代码中,特别是创建自定义容器时,情况略有不同:在那里会分配内存(从分配器中分配并使用放置new进行初始化)。然而,即使在那里,任何内存分配和对象初始化的结果也会立即传递给资源管理器。基本上,我无法处理显式资源管理,并且使用资源管理器只能使我免于必要的工作。


4
我认为每一个资源都应该有一个所有者。这个所有者要负责清理。通常情况下,这个所有者会是一种智能指针,但即使 std::vector 也是一个资源的所有者:它所存储的连续元素的内存块。这个建议不仅适用于内存,还包括文件描述符、数据库句柄、互斥锁等任何资源。
当您在代码的某些部分手动调用 new 和 delete 时,您作为程序员成为了资源的所有者。随着所有权的到来,您需要承担清理自己的责任。现在,您和所有维护程序员需要确保 new 后的所有代码路径最终都会 lead to a delete。即使对于简单的函数,这也很容易出错。使用异常处理,除非您小心地将所有内容包装在 try catch 块中,否则几乎是不可能的,这会导致运行时性能损失,并污染您的代码与多余的异常逻辑。最后,即使您做得正确,您只是浪费了大量时间来进行这个繁琐的资源管理工作。编译器是可以为您完成此项工作的工具,请使用它。
最糟糕的情况是某个子系统分配了某个资源,它在整个应用程序中传递,然后一些其他遥远的子系统释放它。在这种情况下,可能的代码路径数量是难以处理的。对于人类而言,很难甚至不可能推断和信任。在我看来,这种编程风格是难以维护的。您过去曾经使用过多少个 C 项目,其中充斥着内存错误,特别是在很少或从未执行的错误处理路径上?我已经处理过太多了,再也不想看到了。
C 具有手动内存管理,Java 和其他语言具有垃圾回收。C++ 具有 RAII。它与 C 一样高效,并且几乎与垃圾回收一样安全。
我的规则很简单,如果您发现自己需要手动清理任何资源,那么你刚刚写了一个 bug。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接