38

过去几天我一直在研究这个问题,到目前为止,除了教条式的论点或对传统的诉求(即“这是 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),因此不应该被使用这一事实malloc——free据我所知——并不像教条一样引人注目。有什么我想念的东西优于吗?new []malloc

我的意思是,尽我所能告诉你,你甚至不能使用new []- 根本 - 来制作一个没有默认无参数构造函数的数组,而malloc可以使用该方法。

4

11 回答 11

64

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

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

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

但是话又说回来,您可以在 C 中拥有虚函数。如果您投入时间和精力,您可以在纯 C 中实现类和继承。这些功能也完全正常。

因此,重要的不是某事能否奏效。但更多关于成本是多少。在 C 中实现继承和虚函数比 C++ 更容易出错。有多种方法可以在 C 中实现它,这会导致实现不兼容。然而,因为它们是 C++ 的一流语言特性,所以很少有人会手动实现该语言提供的功能。这样大家的继承和虚函数就可以配合C++的规则了。

这也是一样。那么手动 malloc/free 数组管理有哪些得失呢?

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

表现

您声称以下内容:

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

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

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

脆弱性

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

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

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

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

即使输入一次错误,您也会损坏内存。或者不删除一些东西。或者任何数量的其他可怕的事情。

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

然后我们有异常安全,这是一个全新的蠕虫罐。如果其中一个构造函数抛出异常,则需要销毁之前构造的对象。您的代码没有这样做;它不是异常安全的。

现在,考虑替代方案:

delete[] my_array;

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

当然,您可以说您可以将数组包装在一个对象中。那讲得通。您甚至可以在数组的类型元素上对对象进行模板化。这样,所有的析构函数代码都是一样的。大小包含在对象中。也许,只是也许,你意识到用户应该对内存分配的特定方式有一些控制,所以它不仅仅是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是“创建类型实例”的 C++ 关键字。my_object[10]是一个 10 元素my_object类型的数组。它简单、明显且直观。没有强制转换,没有字节大小的计算,什么都没有。

malloc方法需要学习如何malloc习惯性地使用。该new方法只需要了解其new工作原理。它不那么冗长,而且更明显正在发生的事情。

此外,在malloc语句之后,您实际上并没有对象数组。malloc只需返回您告诉 C++ 编译器假装是指向对象的指针(带有强制转换)的内存块。它不是对象数组,因为 C++ 中的对象具有生命周期。一个对象的生命周期直到它被构造出来才开始。该内存中的任何内容都没有调用过构造函数,因此其中没有活的对象。

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

相比之下,你的箱子里确实有活的物体new[]。对象已构建;它们是活的并且完全成型。您可以像使用任何其他指针一样使用此指针my_object*

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

于 2012-01-22T09:51:31.353 回答
38

如果您不想通过隐式构造函数调用来初始化内存,并且只需要有保证的内存分配,placement new那么使用mallocandfree而不是new[]and是完全可以的delete[]

使用over的令人信服的原因是通过构造函数调用提供隐式初始化,为您节省额外的或相关的函数调用,并且您不需要在每次分配后检查,只需包含异常处理程序即可完成工作,从而为您节省冗余错误检查不像. 这两个令人信服的理由都不适用于您的使用。newmallocnewmemsetmallocnewNULLmalloc

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

于 2012-01-22T07:19:55.590 回答
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));
}

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

于 2012-01-22T07:56:19.097 回答
10

您在这里重新实现了new[]/ delete[],您所写的内容在开发专门的分配器中很常见。

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

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

new[]/delete[]为方便起见更可取。它们引入的开销很小,并且可以使您免于许多愚蠢的错误。您是否被迫取消此功能并在任何地方使用集合/容器来支持您的自定义构造?我已经实现了这个分配器——真正的麻烦是为你在实践中需要的所有构造变体创建仿函数。无论如何,您通常会以牺牲程序为代价来获得更精确的执行,而该程序通常比每个人都知道的习语更难维护。

于 2012-01-22T07:41:45.380 回答
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:

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 的替代品。这基本上允许您“移动”您的对象(或直接在向量中构造您的对象)并为您保存一份副本。

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 ☹
于 2012-01-22T07:38:59.107 回答
5

哦,好吧,我在想,鉴于答案的数量,没有理由介入……但我想我和其他人一样被吸引了。我们走吧

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

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

首先,您提供的两个片段是不等价的。只是工作,你的在Exceptions的存在下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);
}

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

(*)或者可能不像 MooingDuck 所说的那么新。

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

就我而言,您似乎真正要求的是一种类型化的内存池,其中一些位置无法初始化

你知道boost::optional吗?

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

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

4. 建议

最后,如果你真的想自己做,无论是为了学习还是因为没有真正适合你的 STL 容器,我建议你将它包装在一个对象中,以避免代码到处乱窜。

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

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

当然,为什么不在这样做的同时利用新的 C++11 工具:) 呢?

于 2012-01-22T12:26:57.567 回答
3

你应该使用vectors.

于 2012-01-22T07:40:47.497 回答
2

不管是否教条,这正是所有 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> >

于 2012-01-22T08:10:50.010 回答
1

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

于 2012-01-22T08:52:54.897 回答
1

您在此处显示的实际上是使用与系统通用分配器不同的内存分配器时要走的路 - 在这种情况下,您将使用分配器 (alloc->malloc(sizeof(my_object))) 分配内存,然后使用放置 new 运算符来初始化它。这在高效的内存管理方面有很多优点,并且在标准模板库中很常见。

于 2012-01-22T07:34:34.323 回答
0
my_object * my_array=new my_object [10];

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

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

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

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

于 2012-01-22T07:37:07.600 回答