我出于一个令人信服的理由离开了。
这取决于您如何定义“引人注目”。到目前为止,您拒绝的许多论点对于大多数 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_object
s。
相比之下,你的箱子里确实有活的物体new[]
。对象已构建;它们是活的并且完全成型。您可以像使用任何其他指针一样使用此指针my_object*
。
鳍
以上都没有说这种机制在适当的情况下没有潜在的用处。但在某些情况下承认某事的效用是一回事。说它应该是默认的做事方式是另一回事。