malloc&放置新的与新的

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

如果我正在创build一个对象数组,那么使用什么是令人信服的理由(除了缓解):

#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 [] – 来创build一个没有默认的无参数构造函数的数组,而malloc方法因此可以被使用。

如果你不想让隐式的构造函数调用初始化你的内存,只需要一个有保证的内存分配来placement new那么使用mallocfree来代替new[]delete[]是完全正确的。

使用new over malloc有力的理由是, new通过构造函数调用提供了隐式初始化,为你节省了额外的memset或者在malloc之后的相关函数调用。对于new ,在每次分配之后你不需要检查NULL ,只需要包含exception处理程序做这个工作节省你不像malloc冗余错误检查。
这两个令人信服的理由不适用于您的使用。

哪一个是性能高效的,只能通过性能分析来确定,现在的方法没有错。 在一个侧面说明我没有看到一个令人信服的理由,为什么要使用malloc over new[]

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

这取决于你如何定义“引人注目”。 你迄今为止拒绝的许多论点对于大多数C ++程序员来说肯定是有吸引力的,因为你的build议不是在C ++中分配裸数组的标准方法。

简单的事实是:是的,你绝对可以按照你描述的方式来做事情。 没有理由,你所描述的将不起作用

但是再次,你可以在C中有虚函数。如果你花费时间和精力,你可以用普通的C来实现类和inheritance。 那些function也是完全的。

因此,重要的不是某件事能否奏效。 但更多的是成本 。 在C中实现inheritance和虚函数比C ++更容易出错。 在C中有多种方式来实现它,导致不兼容的实现。 而因为它们是C ++的一stream语言特性,所以不太可能有人会手动实现该语言提供的function。 因此,每个人的inheritance和虚函数都可以配合C ++的规则。

这也是一样的。 那么手动malloc / free数组pipe理的收益和损失是多less?

我不能说任何我要说的话都是对你来说“令人信服的理由”。 我相当怀疑它会,因为你似乎已经下了决心。 但是为了logging:

性能

您声称以下:

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

这一说法表明,效率收益主要是在build造有关物体。 也就是说,哪个构造函数被调用。 该语句假定您不想调用默认构造函数; 你只是使用默认的构造函数来创build数组,然后使用真正的初始化函数将实际的数据放入对象中。

那么…如果这不是你想要做的? 如果你想要做的是创build一个空的数组,一个默认构造? 在这种情况下,这个优点就完全消失了。

脆弱性

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

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

对于一个简单的情况,这很好。 你有一个macros或constvariables,说明你有多less个对象。 你循环每个元素来销毁数据。 这是一个简单的例子。

现在考虑一个真正的应用 ,而不是一个例子。 你会在多less个不同的地方创build一个数组? 许多? 数百? 每个人都需要有自己的for循环来初始化数组。 每个人都需要有自己的for循环来销毁数组。

甚至有一次这样的错误,你可能会损坏内存。 或者不要删除东西。 或者其他任何可怕的事情。

这里有一个重要的问题:对于一个给定的数组,你在哪里保持大小 ? 你知道你为每个你创build的数组分配了多less项吗? 每个数组可能都有自己的方式来知道它存储了多less个项目。 所以每个析构函数循环都需要正确地获取这些数据。 如果它错了…繁荣。

然后我们有一个例外的安全,这是一个全新的蠕虫jar。 如果其中一个构造函数抛出一个exception,那么之前构造的对象需要被破坏。 你的代码不这样做; 它并不是特例安全的。

现在考虑一下替代scheme:

 delete[] my_array; 

这不能失败。 它总是会毁掉每一个元素。 它跟踪数组的大小,并且是exception安全的。 所以这是保证工作。 它不能工作(只要你分配new[] )。

当然,你可以说你可以把数组包装在一个对象中。 这就说得通了。 你甚至可以在数组的types元素上模拟对象。 这样,所有的desturctor代码是相同的。 大小包含在对象中。 或许也许你意识到用户应该对内存分配的特定方式有一些控制,这样不仅仅是malloc/free

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

这就是为什么许多C ++程序员不再inputnew[]

灵活性

你的代码使用malloc/free 。 但是让我们说我正在做一些分析。 而且我意识到,某些经常创build的types的malloc/free太贵了。 我为他们创build了一个特殊的内存pipe理器。 但是如何把所有的数组分配给它们?

那么,我必须search代码库的任何位置,你创build/销毁这些types的数组。 然后我必须相应地改变它们的内存分配器。 然后我必须不断地观察代码库,以便其他人不会更改这些分配器或引入使用不同分配器的新数组代码。

如果我使用new[]/delete[] ,我可以使用运算符重载。 我只是为运算符new[]提供一个重载,并为这些types提供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是“创buildtypes实例”的C ++关键字。 my_object[10]my_objecttypes的10元素数组。 这很简单,明显,直观。 没有铸造,没有计算字节大小,什么也没有。

malloc方法需要学习如何使用malloc惯用。 new方法只需要了解new作品。 这是不那么冗长,更明显的是发生了什么事情。

而且,在malloc语句之后,实际上并不包含一个对象数组。 malloc只是返回一个内存块,你告诉C ++编译器假装是一个指向对象的指针(带有一个强制转换)。 它不是一个对象数组,因为C ++中的对象有生命周期。 一个对象的生命周期直到构build完成才会开始。 那个记忆中没有任何东西叫做构造函数,所以里面没有活物。

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

相比之下,你在new[]情况下确实有活的东西。 物体已经build成; 他们是生活和完整的。 你可以像使用其他的my_object*一样使用这个指针。

以上都没有说这个机制在正确的情况下是没有用的。 但在某些情况下承认某种东西的效用是一回事。 说这应该成为默认的做事方式还是另一回事。

我也不会说。

最好的办法是:

 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)); } 

这是因为内部向量可能是为你做新的位置。 它还pipe理所有与内存pipe理相关的其他问题,您没有考虑到这一点。

你已经在这里重新实现了new[] / delete[] ,你写的东西在开发专门的分配器中很常见。

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

尚未提及的一件好事就是数组的大小由new[] / delete[]知道。 delete[]只是做权利,并在被问及时破坏所有元素。 拖动一个附加的variables(或三个),所以你究竟如何销毁数组是一个痛苦。 然而,一个专门的收集types将是一个很好的select。

new[] / delete[]是方便的。 他们引入了小的开销,可以从很多愚蠢的错误中拯救你。 你是否已经迫不及待地拿走了这个function,并且无处不在地使用一个集合/容器来支持你的定制构造? 我已经实现了这个分配器 – 真正的混乱是在实践中为所有需要的构造变体创build函子。 无论如何,你通常会以牺牲一个比大家都知道的成语更难维护的程序来执行更精确的执行。

恕我直言,那里都丑,最好使用vector。 只要确保提前分配空间的性能。

或者:

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

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

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

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

 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:

 typedef std::vector<my_object> object_vect; 

通过引用将它们传递给函数:

 void some_function(const object_vect& my_array); 

编辑:在C + + 11还有std ::数组。 它的问题虽然是它的大小是通过模板来完成的,所以你不能在运行时创build不同大小的模板,除非他们期望完全相同的大小(或者是模板函数本身),否则不能将它传递给函数。 但是对于像缓冲区这样的东西可能是有用的。

 std::array<int, 1024> my_array; 

编辑2:同样在C + + 11有一个新的emplace_back作为push_back的替代。 这基本上允许你“移动”你的对象(或直接在向量中构build你的对象),并保存一份副本。

 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 ☹ 

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

  1. 为什么你的解决scheme被打破
  2. C ++ 11个处理原始内存的新设施
  3. 更简单的方法来完成这件事
  4. build议

1.为什么你的解决scheme被打破

首先,你提出的两个片段是不相同的。 new[]只是起作用, exception情况 ,你的失败将会非常严重。

在封面下面做了些什么,它跟踪了被构造的对象的数量,因此,如果在第三个构造函数调用期间发生exception,它将正确地调用已经构造的2个对象的析构函数。

然而,你的解决scheme失败了:

  • 要么你根本不处理exception(并可怕地泄漏)
  • 或者你只是试图在整个数组上调用析构函数,即使它是一半的(可能会崩溃,但谁知道未定义的行为)

所以这两者显然不是等同的。 你的坏了

2. C ++ 11用于处理原始内存的新设施

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

检查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是no-throw,它返回实际分配内存的元素的数量作为该pair的第二个成员(因此.first首先获得指针)。

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

3.更简单的方法来完成

据我所知,你真正似乎要求的是一种types化的内存池,其中某些进位不能被初始化。

你知道boost::optional吗?

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

那么,你的用例真的看起来像一个std::vector< boost::optional<T> >给我(或者一个deque ?)

4.build议

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

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

通过一个对象(模板化),您可以将devise的精髓集中在一个地方,然后在任何地方重复使用。

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

你应该使用vectors

不pipe是否教条,这正是所有的STL容器做分配和初始化。

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

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

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

 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分配exception机制。

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

你在这里展示的实际上是使用内存分配器不同于系统通用分配器的方式 – 在这种情况下,你将使用分配器(alloc-> malloc(sizeof(my_object)))分配内存,然后使用安置新运营商来初始化它。 这在高效的内存pipe理方面有许多优点,在标准模板库中相当普遍。

如果你正在编写一个模仿std::vectorfunction的类,或者需要控制内存分配/对象创build(插入/删除数组等),那么这就是要走的路。 在这种情况下,这不是“不调用默认构造函数”的问题。 它成为一个能够“分配原始记忆, memmove旧对象,然后在旧地址创build新对象”的问题,能够使用某种forms的realloc等问题。 毫无疑问,自定义分配+放置new的方式更灵活…我知道,我有点醉,但std::vector是sissies …关于效率 – 可以写自己的版本的std::vector将至less尽可能快(在sizeof()方面最小),大多数使用std::vector函数的80%,可能less于3小时。

 my_object * my_array=new my_object [10]; 

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

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

这将是一个数组大小的对象,但他们可能是“破”。 如果你的class级有虚拟function,那么你将无法调用这些function。 请注意,这不仅仅是您的成员数据可能不一致,而且整个对象也会被“破坏”(缺less一个更好的词)

我不是说只要你知道这个,就做第二个是不对的。