是否有任何有效的用例使用新的C++的新的,删除,原始指针或c样式的数组?

hvelit 发布于 2019-11-05 c++ 最后更新 2019-11-05 07:38 57 浏览

自从C++ 11成为标准后,我们​​将Dynamic memory management设施称为智能指针。
即使在早期的标准中,我们也有c ++标准Containers library作为原始数组的替代品(使用new T[]分配)。 粗体问题: 撇开new覆盖的位置,是否有任何使用智能指针或标准容器无法实现的有效用例,但只能直接使用newdelete(除了实现此类容器/智能指针类之外)? 有时传言说,对于某些情况,使用newdelete可以“更有效”。这些实际上是什么?这些边界情况不需要像标准容器或智能指针所需要的那样跟踪分配吗? 对于原始的c样式固定大小的数组几乎是一样的:现在有std::array,它可以轻松地进行各种赋值,复制,引用等等,并且在语法上符合每个人的预期。是否有任何用例可以选择优先于std::array<T,N> myArray;T myArray[N]; c样式的数组?


关于与第三方图书馆的互动: 假设第三方库返回像new一样分配的原始指针
MyType* LibApi::CreateNewType() {
    return new MyType(someParams);
}
您始终可以将其包装为智能指针以确保调用delete
std::unique_ptr<MyType> foo = LibApi::CreateNewType();
即使API要求您调用其传统功能来释放资源
void LibApi::FreeMyType(MyType* foo);
您仍然可以提供删除功能:
std::unique_ptr<MyType, LibApi::FreeMyType> foo = LibApi::CreateNewType();

我特别感兴趣的是有效的“每日”用例,而不是所提到的标准设施所涵盖的学术/教育目的要求和限制。
newdelete可能用于内存管理/垃圾收集器框架,或者标准容器的实现无疑是 1

一个主要动机...... ...提出这个问题是给出一个替代方法与任何(家庭作业)问题,这些问题被限制使用标题中提到的任何结构,但是关于生产就绪代码的严重问题。 这些通常被称为内存管理的基础知识,这是国际海事组织公然错误/误解为适合初学者讲座和任务。 这里有一个着名的video关于在教C++语言时需要进行的范式变化。
1) Add .:关于该段落,这应该是一个明确的指示,newdelete不适用于初级c ++学生,但应留给更高级的课程。

已邀请:

homnis

赞同来自:

添加到其他答案,有些情况下新/删除有意义 -

  1. 与第三方库集成,返回原始指针并希望在完成后返回指向库的指针(库具有自己的内存管理功能)。
  2. 使用资源受限的嵌入式设备,其中内存(RAM / ROM)是一种奢侈品(甚至几千字节)。您确定要为应用程序添加更多运行时(RAM)和编译(ROM / Overlay)内存要求,还是要使用new / delete进行仔细编程?
  3. 从纯粹主义的角度来看,在某些情况下,智能指针不会直观地工作(由于它们的性质)。例如,对于构建器模式,如果使用智能指针,则应使用reinterpret_pointer_cast。另一种情况是您需要从基类型转换为派生类型。如果从智能指针获取原始指针,将其转换为另一个智能指针并最终将指针释放多次,则会给自己带来危险。

aquia

赞同来自:

另一个可能的有效用例是当你编写一些garbage collector时。 想象一下,您正在使用C++ 11(或某些Ocaml字节码解释器)编写一些Scheme解释器。该语言要求您编写GC代码(因此您需要在C++中编写代码)。所以所有权不是本地的,如answered by Yakk。而你想要垃圾收集Scheme值,而不是原始内存! 您最终可能会使用显式newdelete。 换句话说,C++ 11 smart pointers支持某些reference counting方案。但这是一种糟糕的GC技术(它与循环引用不友好,这在Scheme中很常见)。 例如,一种实现简单mark-and-sweep GC的简单方法是在某个全局容器中收集Scheme值的所有指针等等...... 另请阅读GC handbook

lnulla

赞同来自:

对于简单的用例,智能指针,标准容器和引用应该足以使用无指针和原始分配和解除分配。 现在我可以考虑一下这些案例:

  • 开发容器或其他低级概念 - 毕竟标准库本身是用C++编写的,它确实使用了原始指针,new和delete
  • 低级别优化。它绝不应该是一流的问题,因为编译器足够聪明以优化标准代码,并且可维护性通常比原始性能更重要。但是,当分析显示代码块占执行时间的80%以上时,低级优化是有意义的,这就是低级C标准库仍然是C++标准的一部分的原因

hquia

赞同来自:

我仍然使用原始指针的主要用例是在实现使用covariant return types的层次结构时。 例如:

#include <iostream>
#include <memory>
class Base
{
public:
    virtual ~Base() {}
virtual Base* clone() const = 0;
};
class Foo : public Base
{
public:
    ~Foo() override {}
// Case A in main wouldn't work if this returned `Base*`
    Foo* clone() const override { return new Foo(); }
};
class Bar : public Base
{
public:
    ~Bar() override {}
// Case A in main wouldn't work if this returned `Base*`
    Bar* clone() const override { return new Bar(); }
};
int main()
{
    Foo defaultFoo;
    Bar defaultBar;
// Case A: Can maintain the same type when cloning
    std::unique_ptr<Foo> fooCopy(defaultFoo.clone());
    std::unique_ptr<Bar> barCopy(defaultBar.clone());
// Case B: Of course cloning to a base type still works
    std::unique_ptr<Base> base1(fooCopy->clone());
    std::unique_ptr<Base> base2(barCopy->clone());
return 0;
}

punde

赞同来自:

所有权不应该是本地的。 作为示例,指针容器可能不希望对其中的指针的所有权驻留在指针本身中。 如果您尝试使用前向唯一ptrs编写链接列表,则在销毁时您可以轻松地清除堆栈。 像拥有指针的容器这样的向量可能更适合于在容器或子容器级别存储删除操作,而不是在元素级别。 在那些和类似的情况下,你像智能指针一样包装所有权,但你在更高的层次上做。许多数据结构(图形等)可能具有类似的问题,其中所有权正确地驻留在比指针更高的位置,并且它们可能不直接映射到现有的容器概念。 在某些情况下,可能很容易从其余的数据结构中分解出容器所有权。在其他人可能不会。 有时你有非常复杂的非本地非参考计算生命周期。在这些情况下,没有合理的位置来放置所有权指针。 在这里确定正确性很难,但并非不可能。存在正确且具有如此复杂的所有权语义的程序。


所有这些都是极端情况,很少有程序员在职业生涯中遇到过多次。

overo

赞同来自:

我将成为逆势而言,并且在记录中说“不”(至少对于我确定你真的打算提出的问题,对于大多数被引用的案例而言)。 使用newdelete的明显用例(例如,GC堆的原始内存,容器的存储)实际上并非如此。对于这些情况,您需要“原始”存储,而不是对象(或对象数组,这分别是newnew[]提供的)。 由于您需要原始存储,因此您确实需要/想要使用operator newoperator delete来管理原始存储本身。然后使用放置new在该原始存储中创建对象,并直接调用析构函数来销毁对象。根据具体情况,您可能希望使用间接级别 - 例如,标准库中的容器使用Allocator类来处理这些任务。这作为模板参数传递,其提供定制点(例如,基于特定容器的典型使用模式来优化分配的方式)。 因此,对于这些情况,您最终使用new关键字(在放置新位置和operator new的调用中),但不是像T *t = new T[N];那样,这是我非常确定您打算询问的内容。

kautem

赞同来自:

如果我们想要创建自己的轻量级内存分配机制,您仍然可以使用newdelete。例如 1.使用就地新:通常用于从预分配的内存中分配;

char arr[4];
int * intVar = new (&arr) int; // assuming int of size 4 bytes
2.使用特定于类的分配器:如果我们想为自己的类定制分配器。
class AwithCustom {
public:
    void * operator new(size_t size) {
         return malloc(size);
    }
void operator delete(void * ptr) {
          free(ptr);
    }
};

funde

赞同来自:

3个常见示例,您必须使用new而不是make_...

  • 如果您的对象没有公共构造函数
  • 如果您想使用自定义删除器
  • 如果您正在使用c ++ 11并且想要创建一个由unique_ptr管理的对象(尽管我建议您在这种情况下编写自己的make_unique)。
但是,在所有这些情况下,您将直接将返回的指针包装到智能指针中。 2-3(可能不常见)示例,您不希望/不能使用智能指针:
  • 如果您必须通过c-api传递您的类型(您是实施create_my_object或实施必须取消空白的回调*)
  • 条件所有权的情况:想一个字符串,当从字符串文本创建时,它不会分配内存,而只是指向该数据。 NOWdays你可能会使用std::variant<T*, unique_ptr<T>>代替,但只有你可以使用变量中存储的所有权信息,并且你接受检查每个访问的哪个成员是活动的开销。当然,只有当你不能/不想承担两个指针(一个拥有和一个非拥有)的开销时,这才是相关的。
    • 如果您希望将所有权基于比指针更复杂的任何内容。例如。你想使用gsl :: owner,这样你就可以很容易地查询它的大小并拥有所有其他好东西(迭代,范围检查......)。不可否认,你最有可能将它包装在你自己的类中,所以这可能属于实现容器的范畴。

id_aut

赞同来自:

某些API可能希望您使用new创建对象,但将接管对象的所有权。例如,Qt库具有父子模型,其中父项删除其子项。如果您使用智能指针,如果您不小心,则会遇到双删除问题。 例:

{
    // parentWidget has no parent.
    QWidget parentWidget(nullptr);
// childWidget is created with parentWidget as parent.
    auto childWidget = new QWidget(&parentWidget);
}
// At this point, parentWidget is destroyed and it deletes childWidget
// automatically.
在这个特定的例子中,您仍然可以使用智能指针,它会很好:
{
    QWidget parentWidget(nullptr);
    auto childWidget = std::make_unique<QWidget>(&parentWidget);
}
因为对象以与声明相反的顺序被销毁。 unique_ptr将首先删除childWidget,这将使childWidgetparentWidget取消注册,从而避免双重删除。但是,大多数时候你没有那种整洁。在许多情况下,父级将首先被销毁,在这种情况下,子元素将被删除两次。 在上述情况下,我们拥有该范围内的父级,因此可以完全控制情况。在其他情况下,父级可能不是小时,但我们将我们的子小部件的所有权交给那个居住在其他地方的父级。 您可能会想到要解决这个问题,您只需要避免使用父子模型并在堆栈上创建所有小部件而不使用父级:
QWidget childWidget(nullptr);
或者使用智能指针而没有父级:
auto childWidget = std::make_unique<QWidget>(nullptr);
然而,这也会在你的脸上爆炸,因为一旦你开始使用小部件,它可能会在你背后重新成为父级。一旦另一个对象成为父对象,则在使用unique_ptr时会获得双删除,并在堆栈上创建堆栈时进行堆栈删除。 最简单的方法是使用new。其他任何事情要么是麻烦,要么是更多的工作,或两者兼而有之。 这些API可以在现代的,不推荐使用的软件(如Qt)中找到,并且早在智能指针出现之前就开发了很多年。它们不能轻易改变,因为这会破坏人们现有的代码。

yquo

赞同来自:

仍然有机会在C++中使用malloc/free,因为您可以使用new/delete,以及包含所提供的STL内存模板的任何更高级别。 我认为为了真正学习C++并特别理解C++ 11内存模板,您应该使用newdelete创建简单的结构。只是为了更好地了解它们的工作原理所有智能指针类都依赖于这些机制。因此,如果您了解newdelete的作用,您将更多地欣赏该模板并真正找到使用它们的智能方法。 今天我个人尽量避免使用它们,但一个主要原因是性能,如果它是关键的,你应该关心它。 这些是我一直记在心里的经验法则: std::shared_ptr:指针的自动管理但由于它用于跟踪访问的指针的引用计数,每次访问这些对象时性能都会更差。比较简单的指针,我会说慢6倍。请记住,您可以使用get()并提取原始指针,并继续访问它。你必须小心那一个。我喜欢将其作为*get()的参考,所以性能更差并不是真的。 std::unique_ptr指针访问可能只发生在代码中的一个点上。由于此模板禁止复制,由于r-references &&功能,它比std::shared_ptr快得多。因为我会说这个类中仍然存在一些所有权开销,它们的速度大约是原始指针的两倍。您访问该对象而不是该模板中的原始指针。我也想在这里使用引用技巧,以减少对对象的访问。 关于性能,可能是这样,这些模板速度较慢,但​​请记住,如果要优化软件,首先应该进行分析,然后查看真正需要许多指令的内容。智能指针不太可能是问题,但确定它取决于您的实现。 在C++中,没有人应该关心mallocfree,但它们存在于遗留代码中。它们的基本不同之处在于它们对c ++类一无所知,而newdelete运算符的情况则不同。 我在我的项目Commander Genius到处使用std::unique_ptrstd::shared_ptr,我很高兴它们存在。从那以后,我不必处理内存泄漏和段错误。在此之前,我们有自己的智能指针模板。所以对于高效的软件,我不能推荐它们。

gcum

赞同来自:

另一个尚未提及的示例是当您需要通过遗留(可能是异步)C回调传递对象时。通常,这些东西需要一个函数指针和一个void *(或一个不透明的句柄)来传递一些有效负载。只要回调在何时/如何/多少次调用时提供一些保证,采用普通的new-> cast-> callback-> cast-> delete是最直接的解决方案(好的,删除将是可能由回调网站上的unique_ptr管理,但是新的仍然存在。当然,存在替代解决方案,但在这种情况下总是需要实现某种显式/隐式“对象生命周期管理器”。

dsed

赞同来自:

另一个用例可能是第三方库返回原始指针,该指针内部由自己的侵入式引用计数(或自己的内存管理 - 任何API /用户界面未涵盖)覆盖。 很好的例子是OpenSceneGraph及其osg :: ref_ptr容器和osg :: Referenced基类的实现。 尽管可以使用shared_ptr,但是对于像用例这样的场景图,侵入式引用计数更好。 就个人而言,我确实在unique_ptr上看到了任何“聪明”的东西。它只是范围锁定新& amp;删除。尽管shared_ptr看起来更好,但它需要开销,这在许多实际情况下是不可接受的。 所以一般来说我的用例是: 处理非STL原始指针包装器时。

west

赞同来自:

当你必须跨越DLL边界传递一些东西。你(几乎)不能用智能指针做到这一点。

kqui

赞同来自:

OP特别询问在日常使用案例中如何/何时手动滚动会更有效 - 我将解决这个问题。 假设有一个现代编译器/ stl /平台,那么每天都不会使用new和delete的手动使用会更有效率。对于shared_ptr案例,我认为它将是边缘的。在一个非常紧凑的循环中,通过使用原始新来避免引用计数(并找到一些其他清理方法 - 除非以某种方式强加给你,你选择使用shared_ptr是有原因的),可能会有所收获,但这不是日常或常见的例子。对于unique_ptr,实际上没有任何差异,所以我认为可以说它更多的是谣言和民间传说,而且表现明智它实际上根本不重要(差异在正常情况下不可测量)。 在某些情况下,不希望或不可能使用其他人已经涵盖的智能指针类。

oad

赞同来自:

在使用私有构造函数时,有时必须调用new。 假设您决定为一个打算由友元工厂调用的类型或显式创建方法的私有构造函数。您可以在此工厂内调用new,但make_unique将无效。

qquas

赞同来自:

当你想创建多维数组但不熟悉像std :: move这样的C++ 11语法时,或者不熟悉为智能指针编写自定义删除器时。

nut

赞同来自:

我处理的问题之一是挖掘用于硬件设计和语言分析的大数据结构,其中包含数亿个元素。内存使用和性能是一个考虑因素。 容器是快速组装数据并使用它的一种很好的方便方法,但实现使用额外的内存和额外的解引用,这会影响内存和性能。我最近使用不同的自定义实现替换智能指针的实验在verilog预处理器中提供了大约20%的性能提升。几年前,我确实比较了自定义列表和自定义树与矢量/地图,还看到了收益。自定义实现依赖于常规的新/删除。 因此,new / delete在定制设计数据结构的高效应用程序中非常有用。

qut

赞同来自:

一个有效的用例是必须与遗留代码进行交互。 特别是如果将原始指针传递给拥有它们所有权的函数。 并非您使用的所有库都可能使用智能指针并使用它们,您可能需要提供或接受原始指针并手动管理它们的生命周期。 如果它的历史很长,甚至可能在您自己的代码库中。 另一个用例是必须与没有智能指针的C进行交互。