malloc&安置新与新

时间:2012-01-22 07:16:00

标签: c++ malloc new-operator placement-new

过去几天我一直在研究这个问题,到目前为止,除了教条论点或传统诉求之外,我还没有真正找到任何令人信服的东西(即“这是C ++方式!”)。

如果我正在创建一个对象数组,使用时有什么令人信服的理由(除了易用性):

#define MY_ARRAY_SIZE 10

//  ...

my_object * my_array=new my_object [MY_ARRAY_SIZE];

for (int i=0;i<MY_ARRAY_SIZE;++i) my_array[i]=my_object(i);

#define MEMORY_ERROR -1
#define MY_ARRAY_SIZE 10

//  ...

my_object * my_array=(my_object *)malloc(sizeof(my_object)*MY_ARRAY_SIZE);
if (my_object==NULL) throw MEMORY_ERROR;

for (int i=0;i<MY_ARRAY_SIZE;++i) new (my_array+i) my_object (i);

据我所知,后者比前者更有效率(因为你没有不必要地将内存初始化为某些非随机值/调用默认构造函数),唯一的区别就在于你清理:

delete [] my_array;

另一个你清理:

for (int i=0;i<MY_ARRAY_SIZE;++i) my_array[i].~T();

free(my_array);

我出于引人注目的原因。有吸引力的是它是C ++(而不是C),因此mallocfree不应该被使用 - 据我所知 - 引人注目尽管教条。是否有一些我遗漏的内容使new []优于malloc

我的意思是,尽管我可以告诉你,你根本不能使用new []来制作一个没有默认的无参数构造函数的数组,而{{{因此可以使用1}}方法。

11 个答案:

答案 0 :(得分:64)

  

我出于令人信服的理由。

这取决于你如何定义&#34;引人注目的&#34;。你迄今为止拒绝的许多论点肯定对大多数C ++程序员都很有吸引力,因为你的建议不是用C ++分配裸数组的标准方法。

一个简单的事实是:是的,你绝对可以按照你描述的方式做事。没有理由说你所描述的内容不会起作用

但话说回来,你可以在C中拥有虚函数。如果你花时间和精力,你可以在普通的C中实现类和继承。这些也完全是功能性的。

因此,重要的不是能否工作。但更多关于成本的内容。在C语言中实现继承和虚函数比在C ++中更容易出错。在C中实现它有多种方法,这会导致不兼容的实现。然而,由于它们是C ++的一流语言特性,因此人们不太可能手动实现该语言提供的功能。因此,每个人的继承和虚函数都可以与C ++规则配合使用。

同样如此。那么手动malloc /免费阵列管理的收益和损失是什么?

我不能说我即将发表的任何内容构成了一个令人信服的理由&#34;为了你。我很怀疑它会,因为你似乎已经下定决心了。但是为了记录:

效果

您声明如下:

  

据我所知,后者比前者更有效率(因为你不必不必要地将内存初始化为某些非随机值/调用默认构造函数),唯一的区别就在于一个你清理的:

该陈述表明效率增益主要在于构造有问题的物体。也就是说,调用哪些构造函数。该声明预先假定您不希望调用默认构造函数;您只使用默认构造函数来创建数组,然后使用实际初始化函数将实际数据放入对象中。

嗯......如果那不是你想做的怎么办?如果你想要做的是创建一个数组,默认构建一个怎么办?在这种情况下,这种优势完全消失。

脆弱性

让我们假设数组中的每个对象都需要有一个专门的构造函数或者在其上调用的东西,这样初始化数组就需要这样的东西。但请考虑您的销毁代码:

for (int i=0;i<MY_ARRAY_SIZE;++i) my_array[i].~T();

对于一个简单的案例,这很好。你有一个宏或const变量,说明你有多少个对象。然后循环遍历每个元素以销毁数据。这对于一个简单的例子来说非常棒。

现在考虑一个真正的应用程序,而不是一个例子。您将在多少个不同的位置创建阵列?许多?数百?每个人都需要有自己的for循环来初始化数组。每个人都需要有自己的for循环来销毁数组。

错误输入一次,你可以破坏记忆。或者不删除某些东西。或任何其他可怕的事情。

这是一个重要的问题:对于给定的数组,您在哪里保留大小?您知道为每个创建的阵列分配了多少项?每个数组可能都有自己的方式来知道它存储了多少项。因此每个析构函数循环都需要正确获取这些数据。如果它弄错了......热潮。

然后我们有异常安全,这是一个全新的蠕虫病毒。如果其中一个构造函数抛出异常,则需要销毁先前构造的对象。你的代码没有这样做;它并非例外安全。

现在,考虑替代方案:

delete[] my_array;

这不会失败。它将永远摧毁每一个元素。它跟踪数组的大小,并且它是异常安全的。所以保证可以工作。它无法 工作(只要您使用new[]分配它)。

当然,你可以说你可以将数组包装在一个对象中。那讲得通。您甚至可以在数组的类型元素上模拟对象。这样,所有desturctor代码都是相同的。大小包含在对象中。也许,也许,只是也许,你意识到用户应该对内存分配的特定方式有一定的控制权,因此它不仅仅是malloc/free

恭喜:您刚刚重新发明了 std::vector

这就是为什么许多C ++程序员甚至不再输入new[]的原因。

灵活性

您的代码使用malloc/free。但是,让我们说我做了一些分析。我意识到malloc/free对于某些经常创建的类型来说太贵了。我为他们创建了一个特殊的内存管理器。但是如何将所有数组分配挂钩到它们?

好吧,我必须在代码库中搜索您创建/销毁这些类型的数组的任何位置。然后我必须相应地更改他们的内存分配器。然后我必须不断观察代码库,以便其他人不会更改这些分配器或引入使用不同分配器的新阵列代码。

如果我使用new[]/delete[],我可以使用运算符重载。我只是为这些类型的运算符new[]delete[]提供重载。没有代码必须改变。对某人来说,规避这些超载要困难得多;他们必须积极尝试。等等。

因此,我可以获得更大的灵活性和合理的保证,我的分配器将被用于应该使用的位置。

可读性

考虑一下:

my_object *my_array = new my_object[10];
for (int i=0; i<MY_ARRAY_SIZE; ++i)
  my_array[i]=my_object(i);

//... Do stuff with the array

delete [] my_array;

将其与此相比:

my_object *my_array = (my_object *)malloc(sizeof(my_object) * MY_ARRAY_SIZE);
if(my_object==NULL)
  throw MEMORY_ERROR;

int i;
try
{
    for(i=0; i<MY_ARRAY_SIZE; ++i)
      new(my_array+i) my_object(i);
}
catch(...)  //Exception safety.
{
    for(i; i>0; --i)  //The i-th object was not successfully constructed
        my_array[i-1].~T();
    throw;
}

//... Do stuff with the array

for(int i=MY_ARRAY_SIZE; i>=0; --i)
  my_array[i].~T();
free(my_array);

客观地说,哪一个更容易阅读和理解发生了什么?

请看一下这句话:(my_object *)malloc(sizeof(my_object) * MY_ARRAY_SIZE)。这是非常低级别的事情。你没有分配任何东西;你正在分配一大块内存。您必须手动计算内存块的大小以匹配对象的大小*您想要的对象数。它甚至还有演员。

相比之下,new my_object[10]讲述了这个故事。 new是&#34;创建类型&#34;的实例的C ++关键字。 my_object[10]my_object类型的10元素数组。它简单,明显,直观。没有投射,没有字节大小的计算,没有。

malloc方法需要学习如何惯用mallocnew方法只需要了解new的工作原理。它不那么冗长,而且更加明显是什么。

此外,在malloc语句之后,您实际上没有对象数组。 malloc只返回一个内存块,你告诉C ++编译器伪装成一个指向对象的指针(带有一个转换)。它不是一个对象数组,因为C ++中的对象具有生命周期。对象的生命周期在构建之前不会开始。

中没有任何东西在该内存中有一个被调用的构造函数,因此它没有生命对象。

此时

my_array不是数组;它只是一块记忆。在您下一步构建它们之前,它不会成为my_object的数组。这对于一个新的程序员来说是非常不直观的;需要一个经验丰富的C ++手(可能从C学习的人)知道那些不是活物,应该小心对待。指针的行为还不像正确的my_object*,因为它还没有指向任何my_object

相比之下,你new[]案例中有生命对象。物体已经建成;他们是现场和完整的形式。您可以像使用任何其他my_object*一样使用此指针。

以上都没有说这种机制在适当的情况下不具备潜在的用途。但在某些情况下承认某事物的效用是一回事。说它应该是默认的做事方式,这是另一回事。

答案 1 :(得分:36)

如果您不希望通过隐式构造函数调用来初始化内存,并且只需要为placement new确保内存分配,则可以使用mallocfree代替new[]delete[]

使用new而不是malloc令人信服的原因是new通过构造函数调用提供了隐式初始化,从而为您节省了额外的memset或相关内容函数调用发布malloc对于new而言,在每次分配后都不需要检查NULL,只需附上异常处理程序就能完成这项工作,从而节省冗余错误检查,而不像{{1 }}。
这两个令人信服的理由都不适用于您的使用。

哪一个是性能效率只能通过分析来确定,你现在的方法没有错。在旁注中,我没有看到为什么使用malloc而不是malloc的令人信服的理由。

答案 2 :(得分:19)

我不会说。

最好的方法是:

std::vector<my_object>   my_array;
my_array.reserve(MY_ARRAY_SIZE);

for (int i=0;i<MY_ARRAY_SIZE;++i)
{    my_array.push_back(my_object(i));
}

这是因为内部向量可能正在为您做新的展示位置。它还管理您未考虑的与内存管理相关的所有其他问题。

答案 3 :(得分:10)

您已在此处重新实现了new[] / delete[],并且您所编写的内容在开发专用分配器时非常常见。

与分配相比,调用简单构造函数的开销将花费很少的时间。它不一定“效率更高” - 它取决于默认构造函数和operator=的复杂性。

尚未提及的一件好事是,new[] / delete[]知道数组的大小。 delete[]只做正确的事,并在被问到时破坏所有元素。拖动一个额外的变量(或三个),所以你究竟如何销毁数组是一个痛苦。但是,专用的收藏类型将是一个很好的选择。

为方便起见,

new[] / delete[]是首选。它们引入的开销很小,可以帮助您避免许多愚蠢的错误。您是否被迫使用此功能并在任何地方使用集合/容器来支持您的自定义构造?我已经实现了这个分配器 - 真正的混乱是为实际需要的所有构造变化创建仿函数。无论如何,你经常以牺牲一个程序为代价执行更精确的程序,这个程序通常比每个人都知道的习语更难维护。

答案 4 :(得分:6)

恕我直言,丑陋,使用矢量更好。只需确保提前分配空间以提高性能。

或者:

std::vector<my_object> my_array(MY_ARRAY_SIZE);

如果要使用所有条目的默认值进行初始化。

my_object basic;
std::vector<my_object> my_array(MY_ARRAY_SIZE, basic);

或者,如果您不想构建对象但想要保留空间:

std::vector<my_object> my_array;
my_array.reserve(MY_ARRAY_SIZE);

然后,如果你只需要将它作为C风格的指针数组进行访问(只是确保你不要在保留旧指针时添加内容,但你不能用常规的c风格数组做到这一点反正。)

my_object* carray = &my_array[0];      
my_object* carray = &my_array.front(); // Or the C++ way

访问个别元素:

my_object value = my_array[i];    // The non-safe c-like faster way
my_object value = my_array.at(i); // With bounds checking, throws range exception

Typedef for pretty:

typedef std::vector<my_object> object_vect;

将它们传递给带引用的函数:

void some_function(const object_vect& my_array);

编辑: 在C ++ 11中还有std :: array。它的问题是它的大小是通过模板完成的,所以你不能在运行时制作不同大小的模板,你不能将它传递给函数,除非它们期望完全相同的大小(或者是模板函数)他们自己)。但它对于像缓冲区这样的东西很有用。

std::array<int, 1024> my_array;

EDIT2: 同样在C ++ 11中,有一个新的emplace_back作为push_back的替代品。这基本上允许你移动&#39;您的对象(或直接在向量中构建对象)并保存副本。

std::vector<SomeClass> v;
SomeClass bob {"Bob", "Ross", 10.34f};
v.emplace_back(bob);
v.emplace_back("Another", "One", 111.0f); // <- Note this doesn't work with initialization lists ☹

答案 5 :(得分:5)

哦,我想,考虑到答案的数量,没有理由介入......但我想我和其他人一样。我们走吧

  1. 为什么你的解决方案被打破
  2. C ++ 11处理原始内存的新工具
  3. 更简单的方法来完成这项工作
  4. 通知书
  5. <强> 1。为什么您的解决方案被破坏

    首先,您提供的两个片段不相同。 new[]只是有效,在例外 的情况下,你的会失败。

    掩护下的new[]是它跟踪构造的对象的数量,因此如果在第3个构造函数调用期间发生异常,则正确调用2个已构造对象的析构函数

    然而,你的解决方案非常糟糕:

    • 要么你根本不处理异常(并且泄密可怕)
    • 或者您只是尝试在整个阵列上调用析构函数,即使它已经构建了一半(可能会崩溃,但是谁知道未定义的行为)

    所以这两者显然不相同。 你好了

    <强> 2。 C ++ 11用于处理原始内存的新工具

    在C ++ 11中,委员会成员已经意识到我们有多喜欢摆弄原始内存,他们已经引入了一些设施来帮助我们更有效,更安全地做到这一点。

    检查cppreference的<memory>简报。这个例子展示了新的好东西(*):

    #include <iostream>
    #include <string>
    #include <memory>
    #include <algorithm>
    
    int main()
    {
        const std::string s[] = {"This", "is", "a", "test", "."};
        std::string* p = std::get_temporary_buffer<std::string>(5).first;
    
        std::copy(std::begin(s), std::end(s),
                  std::raw_storage_iterator<std::string*, std::string>(p));
    
        for(std::string* i = p; i!=p+5; ++i) {
            std::cout << *i << '\n';
            i->~basic_string<char>();
        }
        std::return_temporary_buffer(p);
    }
    

    注意get_temporary_buffer是无抛出的,它返回实际已将内存分配为pair的第二个成员的元素数(因此.first得到指针)。

    (*)也许并不像MooingDuck所说的那么新。

    第3。更简单的方法来完成这项工作

    就我而言,你真正想要的是一种类型化的内存池,其中某些位置可能已被初始化。

    您知道boost::optional吗?

    它基本上是一个原始内存区域,可以适合给定类型的一个项目(模板参数),但默认情况下没有任何内容。它具有与指针类似的接口,可让您查询内存是否实际被占用。最后,使用In-Place Factories,您可以安全地使用它,而无需复制对象,如果它是一个问题。

    嗯,你的用例对我来说真的像std::vector< boost::optional<T> >(或者是deque?)

    <强> 4。建议

    最后,如果你真的想要自己做,无论是学习还是因为没有STL容器真的适合你,我建议你把它包装在一个对象中以避免代码遍布整个地方。

    不要忘记:不要重复自己!

    使用对象(模板化),您可以在一个地方捕获设计的本质,然后在任何地方重复使用。

    当然,为什么不在这样做时利用新的C ++ 11设施:)?

答案 6 :(得分:3)

您应该使用vectors

答案 7 :(得分:2)

Dogmatic与否,这正是所有STL容器分配和初始化所做的事情。

他们使用分配器然后分配未初始化的空间并通过容器构造函数初始化它。

如果这样(就像很多人用来说)“不是c ++ ”那么标准库怎么可能像那样实现呢?

如果你只是不想使用malloc / free,你可以只用new char[]分配“字节”

myobjet* pvext = reinterpret_cast<myobject*>(new char[sizeof(myobject)*vectsize]);
for(int i=0; i<vectsize; ++i) new(myobject+i)myobject(params);
...
for(int i=vectsize-1; i!=0u-1; --i) (myobject+i)->~myobject();
delete[] reinterpret_cast<char*>(myobject);

这使您可以利用初始化和分配之间的分离,仍然采用new分配异常机制的优势。

请注意,将我的第一行和最后一行放入myallocator<myobject>课程,将第二行和最后一行放入myvector<myobject>课程,我们只需重新实现std::vector<myobject, std::allocator<myobject> >

答案 8 :(得分:1)

这里显示的实际上是使用与系统通用分配器不同的内存分配器时的方法 - 在这种情况下,您将使用分配器分配内存(alloc-&gt; malloc(sizeof(my_object)))然后使用placement new运算符对其进行初始化。这在高效的内存管理方面有很多优点,在标准模板库中很常见。

答案 9 :(得分:1)

如果您正在编写一个模仿std::vector功能或需要控制内存分配/对象创建(在数组/删除中插入等)的类 - 那就是要走的路。在这种情况下,它不是“不调用默认构造函数”的问题。它成为一个问题,能够“分配原始内存,memmove旧对象,然后在旧地址创建新对象”,能够使用某种形式的realloc等问题。毫无疑问,自定义分配+展示位置new更加灵活......我知道,我有点醉了,但是std::vector是为了sissies ...关于效率 - 可以写自己的版本std::vector sizeof()速度最快(并且很可能更小,就std::vector而言)最常用的{{1}}功能的80%,可能不到3小时。

答案 10 :(得分:0)

my_object * my_array=new my_object [10];

这将是一个包含对象的数组。

my_object * my_array=(my_object *)malloc(sizeof(my_object)*MY_ARRAY_SIZE);

这将是一个与对象大小相同的数组,但它们可能会被“破坏”。例如,如果您的类具有虚拟功能,那么您将无法调用它们。请注意,不仅仅是您的成员数据可能不一致,而且整个对象被“破坏”(缺少更好的词)

我不是说做第二个是错的,只要你知道这一点。