operator new()和operator new[]()有什么区别?

9

fncs:operator new和operator new[](不是new和new[]运算符)之间有什么区别吗?除了调用语法,当然。我问这个问题是因为我可以使用::operator new(sizeof(T)*numberOfObject)来分配X个字节的对象,并使用数组表示法访问它们,那么::operator new[]有什么大不了的呢?它只是一种语法糖吗?

#include <new>
#include <iostream>
#include <malloc.h>

using namespace std;
struct X
{
  int data_;
  X(int v):data_(v){}
};
int _tmain(int argc, _TCHAR* argv[])
{
  unsigned no = 10;
  void* vp = ::operator new(sizeof(X) * no);
  cout << "Mem reserved: " << _msize(vp) << '\n';
  X* xp = static_cast<X*>(vp);
  for (unsigned i = 0; i < no; ++i)
  {
    new (xp + i) X(i);
  }
  for (unsigned i = 0; i < no; ++i)
  {
    cout << (xp[i]).data_ << '\n';
  }
  for (unsigned i = 0; i < no; ++i)
  {
    (xp + i)->~X();
  }
  ::operator delete(vp);
  return 0;
}

2
除了你的代码无法通过标准的C++编译器(请查找“main”的签名)之外,它并没有说明如何使用operator new[]。也许你发布了属于其他问题的代码? - Cheers and hth. - Alf
2
在有关内存分配函数的问题中,最好不要发布包含内存泄漏的示例代码(除非您正在询问是否存在泄漏)。 - Ben Voigt
3
你用一种我认为是非法的方式使用了delete,来代替一个内存泄漏。标准规定“delete-expression”运算符销毁由“new-expression”创建的最派生对象或数组。” vp确实指向由“new-expression”创建的对象,但它是放置(placement)new,而且new和delete的调用不匹配。具体地说,以这种方式调用delete vp;并不能撤消它之前的vp = ::operator new(sizeof(X) * no);语句。 - Ben Voigt
@Ben 那你会怎么做呢?我的意思是你会用什么方式去释放它?你会首先显式地调用每个对象的析构函数,然后再调用 delete vp 吗? - There is nothing we can do
1
@There:有不同的版本可供实现者在特定情况下提供适当的优化机会。 - Martin York
显示剩余17条评论
5个回答

7
这些函数(operator new等)通常不是直接调用的,而是由new/new[]表达式隐式调用(对称地,operator delete/operator delete[]函数由delete/delete[]表达式隐式调用)。使用非数组类型的new语法的表达式将隐式调用operator new函数,而使用new[]的表达式将隐式调用operator new[]
这里的重要细节是,由new[]表达式创建的数组通常会在稍后由delete[]表达式销毁。后者需要知道要销毁的对象数量(如果对象具有非平凡的析构函数),即这些信息必须以某种方式从new[]表达式(在其已知时)传递到相应的delete[]表达式(在需要时)。在典型的实现中,此信息存储在new[]表达式分配的块内部,因此在对operator new[]的隐式调用中请求的内存大小通常比元素数和元素大小的乘积大。额外的空间用于存储家庭信息(即元素数量)。稍后的delete[]表达式将检索该家庭信息,并在实际调用operator delete[]释放内存之前使用它来调用正确数量的析构函数。
在您的示例中,您没有使用任何这些机制。在您的示例中,您显式调用内存分配函数,手动执行构造,并完全忽略了销毁步骤(这是可以的,因为您的对象具有平凡的析构函数),这意味着至少对于销毁目的,您不需要跟踪数组中元素的确切数量。无论如何,您会手动跟踪该数字,存储在一个no变量中。
然而,在一般情况下,这是不可能的。在一般情况下,代码将使用new[]表达式和delete[]表达式,并且元素数量必须以某种方式从new[]传递到delete[],这意味着它必须在内部存储,这就是为什么需要专门为数组分配内存的函数- operator new[]。它与仅具有上述乘积作为大小的operator new不等效。

我会重新构思最后一段。尽管new/delete不需要计数(因为它始终为1),但它们可以使用与new[]/delete[]相同的技术实现(从而消除了new与delete匹配以及new[]与delete[]匹配的要求)。分离的原因(我认为)是为了允许new/delete的优化版本。 - Martin York
@Martin:对于全局分配器和释放器函数而言,将它们分开的原因几乎可以肯定是为了调试目的,这样库就可以使用内存跟踪来捕获那些错误匹配new和delete运算符的操作。例如class X p = new X[10]; delete p;可以通过在::operator new[]中进行内存追踪来检测到。优化更可能取决于块的大小,而不是存储在其中的单个对象或数组。 - Ben Voigt
@Martin:我认为这是一个先有鸡还是先有蛋的问题:要么newnew[]的优化版本(new[]在这里是“基本”功能),要么new[]new的扩展(对于数组而言,new在这种情况下是“基本”功能)。我总是更喜欢把它看作是后者,即newnew[]更基础。你似乎更喜欢前者。也许你是对的。 - AnT stands with Russia

1
在你的示例代码中,你使用了放置 new 来执行 operator new[] 自动执行的构造 - 不同之处在于 new[] 只会执行默认构造,而你正在执行非默认放置构造。
以下内容与你的示例更或多或少等效:
#include <iostream>

using namespace std;

struct X
{
    int data_;
    X(int v=0):data_(v){}
};

int main(int argc, char* argv[])
{
    unsigned no = 10;

    X* xp = new X[no];

    for (unsigned i = 0; i < no; ++i) {
        X tmp(i);
        xp[i] = tmp;
    }

    for (unsigned i = 0; i < no; ++i)
    {
        cout << (xp[i]).data_ << '\n';
    }

    delete[] xp;

    return 0;
}

这个例子中的区别在于:

  • 我认为这个例子更易读(强制类型转换可能很丑陋,而放置new是一种相当高级的技术,不经常使用,因此不经常被理解)
  • 它正确地销毁了分配的对象(在这个例子中并不重要,因为对象是POD,但在一般情况下,您需要调用数组中每个对象的析构函数)
  • 它必须默认构造对象数组,然后迭代它们以设置对象的实际值 - 是这个例子与您的例子相比的一个缺点

我认为通常使用new[]/delete[]比分配原始内存并使用放置new来构造对象要好得多。它将簿记复杂性推入这些运算符中,而不是在您的代码中。但是,如果“默认构造/设置所需值”操作的成本被发现过高,则手动执行它的复杂性可能是值得的。那应该是非常罕见的情况。

当然,任何关于new[]/delete[]的讨论都需要提到,使用new[]/delete[]可能应该避免,而应该选择使用std::vector

在评论中进行了一些讨论后,我们了解到他的问题是关于void* ::operator new(std::size_t)函数,而不是new运算符。 - Ben Voigt
阅读了评论后,我比以往更加困惑 - 评论似乎大多在讨论如何正确销毁使用放置new创建的对象。我应该删除这个答案吗?它仍然似乎与问题相关(除了问题已被修改为销毁“放置”对象),但你说我没有抓住重点。 - Michael Burr
根据我的理解,他在问 void* ::operator new[](size_t) 是否是 ::operator new(size_t) 的语法糖,也就是说他是否可以直接调用标量版本而省略中间人。答案是否定的,因为 ::operator new[]() 返回一个指针,可以(不会有未定义行为)传递给 ::operator delete[](void*),而 ::operator new() 则不行。顺便说一句,我的意思是我回答后的前四条评论,而不是问题本身,也许这是引起混淆的原因。 - Ben Voigt
@There:你没有看到我的评论吗?将::operator new[]()的返回值传递给::operator delete[](void*)是正确的。将::operator new()的返回值传递给::operator delete[](void*)会引发未定义的行为。仅仅因为两个函数具有相同的签名并不意味着它们是相同的函数。显然,sin(x)cos(x)不是相同的函数,尽管它们都具有相同的签名。 - Ben Voigt
@那里:你是不是太迟钝了?一个值的意义不仅仅在于它的类型。考虑这个例子:int* p1 = new int; int* p2 = new int[10];p1p2都有相同的类型,但是p2[8] = 5;是有效的,而p1[8] = 5则是未定义的行为。malloc也返回void*。你认为调用::operator delete[](malloc(5))可以吗?C++标准说不行。我可以举出无数例子,但是除非你放弃你预先设定的观念,即你正在解释为什么newnew[]是相同的,并停下来思考你可能是错的,否则更多的例子也没有任何好处。 - Ben Voigt
显示剩余2条评论

0

函数 void* operator new(size_t)void* operator new[](size_t) 的所需行为之间几乎没有什么区别,除了它们与不同的释放函数配对。

这些运算符本身非常不同。 运算符之间的差异之一是使用哪个分配函数,但最终还有许多其他差异,包括调用多少构造函数等。 但是您的示例代码并未使用这些运算符(嗯,它正在使用放置 new)。 您可能需要更改问题标题以明确这一点。

来自 [basic.stc.dynamic.deallocation] 章节:

如果一个释放函数通过抛出异常来终止,那么行为是未定义的。提供给释放函数的第一个参数的值可能是空指针值;如果是这样,并且释放函数是标准库中提供的,则调用没有效果。否则,在标准库中提供给operator delete(void*)的值应该是先前调用标准库中的operator new(std::size_t)operator new(std::size_t, const std::nothrow_t&)返回的值之一,并且在标准库中提供给operator delete[](void*)的值应该是先前调用标准库中的operator new[](std::size_t)operator new[](std::size_t, const std::nothrow_t&)返回的值之一。

@Ben 所以基本上我说“indexed”版本的这个函数只是一种语法糖并没有错吧? - There is nothing we can do
@Ben 对于你最后的评论:(在VS中)最终调用的是operator delete(void*)。因此,无论您是调用delete vp;还是delete[] vp;,都将调用相同的operator delete(void*)。 - There is nothing we can do
这让我想到一个很好的问题,因为至少在MSVC中,operator new[]的实现是return (operator new(count)); - Stuart Golodetz
1
@Stuart:我没有一个完美的答案。我可以想到很多理由为不同类型的对象(不同大小)从不同的池中分配内存,我也可以看到任何定义自定义operator new的类可能不希望从中分配数组(特别是考虑到允许数组存储稍后delete需要调用的析构函数数量的开销),但现在我想不出全局单个对象和数组被分配不同的任何理由。尽管与特定于类的情况保持一致是一件好事,在我看来。 - Ben Voigt
1
@Stuart:这里又有一个:它提供了一种检测许多 new/delete 不匹配的情况的方法,对于调试版本的分配器来说非常有用,因为它可以捕获析构函数没有被调用的真实问题。 - Ben Voigt
显示剩余15条评论

0

分配是一回事,对象的构造/销毁是另一回事。

new 执行一次分配和一次构造。然而,new[] 仍然分配一个连续的内存块,但调用多个构造函数。

deletedelete[] 的情况也是一样的。

顺便说一句 - 我不完全确定我即将说的话,但我相信如果您从 new[] 收到一个地址并调用 delete,您不会立即发生内存泄漏。整个内存块可能会被释放。然而,这是无效的,因为您只会在第一个对象上调用析构函数,而不是在数组中每个对象上都调用。这可能会导致 二次 内存泄漏...当然还有由于构造函数和析构函数之间破坏了1-1关系而产生的大量逻辑错误。

(此外,请记得考虑使用 boost::arraystd::vector 而不是使用 new[]! :))


在评论中进行了一些讨论后,我们了解到他的问题是关于void* ::operator new(std::size_t)函数,而不是new运算符。 - Ben Voigt

0

正如user sankoz他的回答中对于实际上相同的问题所解释的那样,拥有单独的operator new[]是为了分别重载类中的单个对象分配和数组分配。

例如,如果您有一些特定的类,并且您知道它的实例永远不会大于50个字节,您可能希望为该类重载operator new,以便其实例在大小为50的超快速分配器上分配。

但是,如果用户调用new[]呢?数组可以具有任意数量的元素,因此您无法普遍地将它们分配到自定义分配器上。解决方案是,除非您想要,否则您不必关心数组分配。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接