malloc&placement new vs. new

Rob*_*ahy 37 c++ malloc placement-new new-operator

过去几天我一直在研究这个问题,到目前为止,除了教条论点或对传统的诉求之外,我还没有真正找到任何令人信服的东西(即"它是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);
Run Code Online (Sandbox Code Playgroud)

过度

#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);
Run Code Online (Sandbox Code Playgroud)

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

delete [] my_array;
Run Code Online (Sandbox Code Playgroud)

另一个你清理:

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

free(my_array);
Run Code Online (Sandbox Code Playgroud)

我出于一个令人信服的理由.上诉的事实,这是C++(不是C),因此mallocfree不应使用不-据我可以告诉- 引人注目的一样,因为它是教条主义.是否有一些我缺少的东西new []优于malloc

我的意思是,就我所知,你根本不能使用new []- 根本没有 - 创建一个没有默认的无参数构造函数的数组,而这个malloc方法可以这样使用.

Nic*_*las 63

我出于一个令人信服的理由.

这取决于你如何定义"引人注目".到目前为止,您拒绝的许多论据肯定对大多数C++程序员都很有吸引力,因为您的建议不是在C++中分配裸数组的标准方法.

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

但话说回来,你可以在C中使用虚函数.如果你把时间和精力放在它中,你可以在普通的C中实现类和继承.这些也完全是功能性的.

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

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

我不能说我要说的任何内容对你来说都是一个"令人信服的理由".我很怀疑它会,因为你似乎已经下定决心了.但是为了记录:

性能

您声明如下:

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

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

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

脆弱性

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

for (int i=0;i<MY_ARRAY_SIZE;++i) my_array[i].~T();
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

现在,考虑替代方案:

delete[] my_array;
Run Code Online (Sandbox Code Playgroud)

这不能失败.它将永远摧毁每一个元素.它跟踪数组的大小,并且它是异常安全的.所以它保证工作.它不能工作(只要你分配它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;
Run Code Online (Sandbox Code Playgroud)

比较一下:

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);
Run Code Online (Sandbox Code Playgroud)

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

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

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

malloc方法需要学习如何malloc习惯使用.该new方法只需要了解其new工作原理.它的冗长和更加明显的正在发生的事情要少得多.

此外,在malloc语句之后,您实际上没有对象数组.malloc简单地返回一个内存块,你告诉C++编译器假装是一个指向对象的指针(带有强制转换).它不是一个对象数组,因为C++中的对象具有生命周期.并且对象的生命周期在构造之前不会开始.该内存中的任何内容都没有调用它的构造函数,因此其中没有生命对象.

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

相比之下,你的确拥有生物new[].物体已经建成; 他们是现场和完整的形式.您可以像使用其他指针一样使用此指针my_object*.

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

  • @ user268396:你在说什么?C++规范非常清楚:如果一个对象抛出它的构造函数,所有以前构造的对象都将被销毁.这是规范的*自动*和*必需*.如果你看到这种情况没有发生,那就是编译错误. (5认同)
  • 鉴于这里的优秀声明,我宁愿添加你的答案而不是写我自己的答案.你错过了一个很好的理由:异常安全.如果对第3个构造函数的调用失败,则应该为两个第一个构造对象调用析构函数.`new []`自动执行此操作,OP的解决方案是错误的. (4认同)

Alo*_*ave 37

如果你不想通过隐式构造函数调用来初始化你的内存,并且只需要一个有保证的内存分配,placement new那么使用mallocfree不是new[]delete[].

使用over 的令人信服的理由是通过构造函数调用提供隐式初始化,保存后续的附加或相关函数调用而且在每次分配后都不需要检查,只是封闭异常处理程序将完成这项工作,从而节省冗余错误检查不同. 这两个令人信服的理由不适用于您的使用.newmallocnewmemsetmallocnewNULLmalloc

哪一个是性能效率只能通过分析确定,你现在的方法没有错.在一个侧面说明我没有看到令人信服的理由,为什么使用mallocnew[]两种.

  • @Als:我认为OP经验丰富(至少在C中),但是他的解决方案在异常情况下是不正确的.如果第三个对象的初始化抛出会发生什么?谁清理了以前建造的2个物体?他如何确保不会在未初始化的原始内存上调用析构函数?`new []`处理它,OP不处理它,它不是一个完全成熟的替代品,只是一个摇摇欲坠的替代品. (6认同)
  • @Als:在OP的解决方案中使用`new []`的令人信服的理由是**Exception Safety**.如果在构造期间抛出异常,OP代码将崩溃或泄漏,`new []`将在清理*之后让异常传播*. (4认同)
  • :D好点"为什么要使用'malloc`而不是'new`" (3认同)

Mar*_*ork 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));
}
Run Code Online (Sandbox Code Playgroud)

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

  • 使用`emplace_back`来避免复制构造函数,或者在没有复制构造函数的情况下. (3认同)
  • @NeilG`my_object(i)`是一个右值,所以如果可能的话,对象将被移动.但是,`emplace_back`会更有效率:) (2认同)

jus*_*tin 10

你已经重新实现new[]/ delete[]在这里,你所写的内容在开发专用分配器时非常常见.

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

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

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


Dav*_*hop 6

恕我直言那里既丑陋,最好使用矢量.只需确保提前分配空间以提高性能.

或者:

std::vector<my_object> my_array(MY_ARRAY_SIZE);
Run Code Online (Sandbox Code Playgroud)

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

my_object basic;
std::vector<my_object> my_array(MY_ARRAY_SIZE, basic);
Run Code Online (Sandbox Code Playgroud)

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

std::vector<my_object> my_array;
my_array.reserve(MY_ARRAY_SIZE);
Run Code Online (Sandbox Code Playgroud)

然后,如果你需要将它作为一个C风格的指针数组来访问(只是确保你在保留旧指针时不添加东西,但无论如何你都不能用常规的c风格数组.)

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

访问个别元素:

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
Run Code Online (Sandbox Code Playgroud)

Typedef for pretty:

typedef std::vector<my_object> object_vect;
Run Code Online (Sandbox Code Playgroud)

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

void some_function(const object_vect& my_array);
Run Code Online (Sandbox Code Playgroud)

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

std::array<int, 1024> my_array;
Run Code Online (Sandbox Code Playgroud)

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

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 ?
Run Code Online (Sandbox Code Playgroud)

  • 我想提出这个答案,但是`&my_array [0]`和`&my_array.front()`完全相同.在这两种情况下,如果向量为空,则行为未定义.此外,在超出范围的情况下抛出异常不会使错误的程序更正确. (6认同)

Mat*_* M. 5

哦,我想,考虑到答案的数量,没有理由介入......但我想我被其他人吸引了.我们走吧

  1. 为什么你的解决方案被破坏
  2. C++ 11用于处理原始内存的新工具
  3. 完成这项工作的简单方法
  4. 建议

1.为什么你的解决方案被破坏了

首先,您提供的两个片段不相同.new[]只是工作,你异常存在可怕失败.

什么new[]掩护下所做的是,它会跟踪构建的是,物体的数量,因此如果说在第三构造函数中调用它正确地呼吁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);
}
Run Code Online (Sandbox Code Playgroud)

注意,它get_temporary_buffer是无抛出的,它返回实际已经分配了内存的元素的数量作为第二个成员pair(因此.first获取指针).

(*)或许并不像MooingDuck所说的那么新.

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

至于我concered,你似乎真的被要求的是一种类型的存储池,其中一些可能炮位的不是已被初始化.

你知道boost::optional吗?

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

那么,你的用例std::vector< boost::optional<T> >对我来说真的很像(或许是一个deque?)

4.建议

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

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

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

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