我有一个很充分的理由离开。
这取决于你如何定义“充分”。到目前为止,你已经拒绝了许多论点,但对大多数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(...)
{
for(i; i>0; --i)
my_array[i-1].~T();
throw;
}
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]
是
my_object
类型的10个元素数组。 这很简单、明显、直观。 没有转换,没有计算字节大小,没有别的。
使用
malloc
需要学习如何惯用地使用
malloc
。而使用
new
则只需要了解
new
的工作原理。它少得多,并且更容易理解发生了什么。
此外,在
malloc
语句之后,您实际上并没有一个对象数组。
malloc
只是返回一块内存,您已经告诉C++编译器通过转换将其假装成一个指向对象的指针。它不是一个对象数组,因为C++中的对象有生命期,只有在构造函数被调用后才开始。这个内存中什么都没有构造,因此其中没有任何活动对象。
此时
my_array
并不是数组;它只是一块内存。直到下一步构建对象,它才成为一个
my_object
数组。这对于新的程序员来说非常难以理解,需要有经验的c++程序员(可能是从C语言学习而来)来知道它们不是实际的对象,应该小心处理。指针还没有像正确的
my_object*
那样行为良好,因为它还没有指向任何
my_object
。
相比之下,使用
new[]
就能创建出对象数组。对象已经被构造了,它们是活着、完整的。您可以像使用任何其他
my_object*
一样使用此指针。
Fin
以上内容并不意味着这种机制在适当的情况下不会有用。 但认可某些情况下的效用,和说它应该成为默认做事方式是完全不同的。
std::vector
或者std::array
。 - David Brownnew
和delete
,你在实际上获得了多少性能提升,相比于你失去了多少可读性和犯下了多少冗余错误?从来没有一个我或我认识的任何人工作过的程序中,不必要的初始化曾经成为瓶颈。只有在极少数情况下,不必要的默认构造函数调用才会成为问题。在这些罕见的情况下,已经为这些情况处理并安全地抽象化了。 - Sion Sheevok