运算符重载的基本规则和惯用法是什么?


2141

注意:答案是按照特定顺序给出的,但是由于许多用户是根据投票而不是给出时间来对答案进行排序的,因此以下是最有意义的顺序的索引

(注意:这本来是Stack Overflow的C ++ FAQ的一个条目。如果您想批评以这种形式提供FAQ的想法,那么开始所有这些工作的meta上的发布将是这样做的地方。该问题在C ++聊天室中进行监控,该问题最初是从FAQ想法开始的,所以提出这个想法的人很可能会读懂您的答案。)


63
如果我们要继续使用C ++-FAQ标记,则应采用这种方式格式化条目。
John Dibling

我已经为德国C ++社区写了一系列有关运算符重载的简短文章:第1部分:C ++中的运算符重载涵盖了所有运算符的语义,典型用法和特殊性。在这里,它与您的答案有些重叠,不过,还有一些其他信息。第2部分和第3部分构成了有关使用Boost.Operators的教程。您想要我翻译它们并将其添加为答案吗?
Arne Mertz 2013年

哦,还有英文译本:基础知识通用做法
Arne Mertz,2017年

Answers:


1042

普通运算符重载

重载操作员中的大部分工作是样板代码。这也就不足为奇了,由于运算符仅仅是语法糖,它们的实际工作可以由(通常转发给)普通函数来完成。但是,重要的是要正确编写此样板代码。如果失败,则操作员的代码将无法编译,或者用户的代码将无法编译,或者用户的代码将表现出惊人的性能。

赋值运算符

关于任务有很多要说的。但是,大多数已经在 GMan著名的“复制和交换常见问题解答”中进行了介绍,因此在此我将跳过大部分内容,仅列出完美的赋值运算符以供参考:

X& X::operator=(X rhs)
{
  swap(rhs);
  return *this;
}

位移位运算符(用于流I / O)

位移位运算符 <<>>尽管移位移位和仍然用于它们从C继承的位处理功能的硬件接口中,但它们在大多数应用程序中已成为重载的流输入和输出运算符,因此越来越流行。有关作为位操作运算符的指导重载,请参见下面有关二进制算术运算符的部分。当对象与iostream一起使用时,要实现自己的自定义格式和解析逻辑,请继续。

在最常见的重载运算符中,流运算符是二进制中缀运算符,其语法对它们应为成员还是非成员不加限制。由于它们更改了左参数(它们更改了流的状态),因此应根据经验法则将其实现为其左操作数类型的成员。但是,它们的左操作数是标准库中的流,尽管标准库定义的大多数流输出和输入运算符的确定义为流类的成员,但是当您为自己的类型实现输出和输入操作时,无法更改标准库的流类型。这就是为什么您需要将自己的类型的这些运算符实现为非成员函数。两种的规范形式是:

std::ostream& operator<<(std::ostream& os, const T& obj)
{
  // write obj to stream

  return os;
}

std::istream& operator>>(std::istream& is, T& obj)
{
  // read obj from stream

  if( /* no valid object of T found in stream */ )
    is.setstate(std::ios::failbit);

  return is;
}

实施时 operator>>,仅当读取本身成功时才需要手动设置流的状态,但是结果不是预期的。

函数调用运算符

必须将用于创建函数对象(也称为函子)的函数调用运算符定义为成员函数,因此它始终具有this成员函数的隐式参数。除此之外,可以重载任何数量的附加参数,包括零。

这是语法示例:

class foo {
public:
    // Overloaded call operator
    int operator()(const std::string& y) {
        // ...
    }
};

用法:

foo f;
int a = f("hello");

在整个C ++标准库中,始终复制功能对象。因此,您自己的函数对象应该廉价复制。如果功能对象绝对需要使用复制成本高昂的数据,则最好将该数据存储在其他位置并让功能对象引用它。

比较运算符

根据经验法则,二进制中缀比较运算符应实现为非成员函数1。一元前缀否定!(应根据相同规则)应实现为成员函数。(但通常不建议重载它。)

标准库的算法(例如std::sort())和类型(例如std::map)将始终只期望operator<存在。但是,您的类型用户也希望所有其他运算符也都存在,因此,如果您定义operator<,请确保遵循运算符重载的第三条基本规则,并且还要定义所有其他布尔比较运算符。实现它们的规范方法是:

inline bool operator==(const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator!=(const X& lhs, const X& rhs){return !operator==(lhs,rhs);}
inline bool operator< (const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator> (const X& lhs, const X& rhs){return  operator< (rhs,lhs);}
inline bool operator<=(const X& lhs, const X& rhs){return !operator> (lhs,rhs);}
inline bool operator>=(const X& lhs, const X& rhs){return !operator< (lhs,rhs);}

这里要注意的重要一点是,这些运算符中只有两个实际执行任何操作,其他运算符只是将其参数转发给这两个运算符中的任何一个来执行实际工作。

超载剩余二进制布尔运算符的语法(||&&)如下比较运算符的规则。然而,这是非常不可能的,你会发现这些合理的使用案例2

1 与所有经验法则一样,有时也可能有理由打破这一原则。如果是这样,请不要忘记二进制比较运算符的左操作数(对于成员函数为)也必须*thisconst。因此,实现为成员函数的比较运算符必须具有以下签名:

bool operator<(const X& rhs) const { /* do actual comparison with *this */ }

(请注意const最后的。)

2 应该注意,的内置版本||&&使用快捷方式语义。虽然用户定义的语法(因为它们是方法调用的语法糖),却不使用快捷方式语义。用户将期望这些运算符具有捷径语义,并且它们的代码可能依赖于此,因此,强烈建议不要定义它们。

算术运算符

一元算术运算符

一元递增和递减运算符具有前缀和后缀形式。为了彼此区分,postfix变体采用了另一个哑int参数。如果您使增量或减量过载,请确保始终同时实现前缀和后缀版本。这是递增的规范实现,递减遵循相同的规则:

class X {
  X& operator++()
  {
    // do actual increment
    return *this;
  }
  X operator++(int)
  {
    X tmp(*this);
    operator++();
    return tmp;
  }
};

请注意,后缀变体是根据前缀实现的。另请注意,后缀会额外复制。2

一元负号和加号的重载不是很常见,最好避免。如果需要,它们可能应该作为成员函数重载。

2 还请注意,postfix变体比前缀变体执行更多的工作,因此使用效率较低。这是一个很好的理由,通常优先选择前缀增量而不是后缀增量。尽管编译器通常可以优化内置类型的后缀增量的其他工作,但对于用户定义的类型,它们可能无法执行相同的工作(这可能像列表迭代器一样无辜)。一旦您习惯了做i++,就很难记住在什么++i时候要做i不是内置类型在改变类型时必须更改代码),因此养成始终保持习惯的习惯会更好。使用前缀增量,除非明确需要后缀。

二元算术运算符

对于二元算术运算符,请不要忘记遵守第三个基本规则运算符重载:如果提供+,还提供+=,如果提供-,不要省略-=,等等。据说安德鲁·科尼格(Andrew Koenig)是第一个观察复合赋值的人。运算符可以用作非复合运算符的基础。也就是说,运营商+来讲是执行+=-在以下方面实现-=等。

根据我们的经验法则,+它的同伴应为非成员,而其复合赋值对应对象(+=等)(更改其左参数)应为成员。下面是示例性代码+=+; 其他二进制算术运算符应以相同的方式实现:

class X {
  X& operator+=(const X& rhs)
  {
    // actual addition of rhs to *this
    return *this;
  }
};
inline X operator+(X lhs, const X& rhs)
{
  lhs += rhs;
  return lhs;
}

operator+=返回每个引用的结果,同时operator+返回其结果的副本。当然,返回引用通常比返回副本更有效,但是在的情况下operator+,无法解决复制问题。编写时a + b,您期望结果是一个新值,这就是为什么operator+必须返回一个新值的原因。3 还请注意,operator+它的左操作数是通过复制而不是const引用获取的。其原因与给出operator=每个副本其参数的原因相同。

位操作运算符~ & | ^ << >>应以与算术运算符相同的方式实现。但是,(除了重载<<以及>>输出和输入),很少有合理的用例可以使这些重载。

3 再次,从中吸取的教训a += b是,总的来说,它比a + b可能更有效,因此应该优先考虑。

数组下标

数组下标运算符是二进制运算符,必​​须将其实现为类成员。它用于类容器类型,允许通过键访问其数据元素。提供这些的规范形式是这样的:

class X {
        value_type& operator[](index_type idx);
  const value_type& operator[](index_type idx) const;
  // ...
};

除非您不希望类的用户能够更改其返回的数据元素operator[](在这种情况下,您可以忽略non-const变体),否则应始终提供运算符的两个变体。

如果已知value_type引用内置类型,则该运算符的const变体最好返回一个副本而不是const引用:

class X {
  value_type& operator[](index_type idx);
  value_type  operator[](index_type idx) const;
  // ...
};

指针类型的运算符

为了定义自己的迭代器或智能指针,您必须重载一元前缀取消引用运算符*和二进制中缀指针成员访问运算符->

class my_ptr {
        value_type& operator*();
  const value_type& operator*() const;
        value_type* operator->();
  const value_type* operator->() const;
};

注意,这些也几乎总是需要const版本和非const版本。对于->运算符,如果value_typeclass(或structunion)类型,operator->()则递归调用另一个,直到a operator->()返回非类类型的值。

一元地址运算符绝对不能重载。

对于operator->*()看到这个问题。它很少使用,因此也很少过载。实际上,即使迭代器也不会使它过载。


继续向转换运算符


89
operator->()实际上是非常奇怪的。不需要返回value_type*-实际上,它可以返回另一个类类型,前提是该类类型具有operator->(),随后将对其进行调用。递归调用operator->()s直到发生value_type*返回类型。疯狂!:)
j_random_hacker 2011年

2
这不完全是关于有效性的。这是关于(在非常少数情况下)我们不能以传统惯用的方式做到这一点:当在计算结果时两个操作数的定义都需要保持不变时。正如我所说的,有两个经典的例子:矩阵乘法和多项式乘法。我们可以用定义**=但这会很尴尬,因为的第一个操作之一*=是创建一个新对象,即计算结果。然后,在for-ijk循环之后,我们将与交换此临时对象*this。即。1.copy,2.operator *,3.swap
Luc Hermitte 2012年

6
我不同意类似指针的运算符的const /非const版本,例如`const value_type&operator *()const;`-就像在取消引用时T* const返回a const T&,事实并非如此。或者换句话说:const指针并不意味着const指针。实际上,模仿并非易事T const *-这是const_iterator标准库中全部内容的原因。结论:签名应该是reference_type operator*() const; pointer_type operator->() const
Arne Mertz

6
一个评论:建议的二进制算术运算符的实现效率不高。Se Boost运算符标头模拟说明:boost.org/doc/libs/1_54_0/libs/utility/operators.htm#symmetry如果您使用第一个参数的本地副本,执行+ =并返回本地副本。这样可以进行NRVO优化。
Manu343726

3
正如我在聊天中提到,L <= R也可表现为!(R < L),而不是!(L > R)。可能会在难以优化的表达式中节省额外的内联层(这也是Boost.Operators实现它的方式)。
TemplateRex

494

C ++中运算符重载的三个基本规则

对于C ++中的运算符重载,应遵循三个基本规则。与所有此类规则一样,确实存在例外。有时人们偏离了他们,结果不是不好的代码,但是这种积极的偏差很少而且相去甚远。至少在我看到的100个此类偏差中,有99个是不合理的。但是,也有可能是1000分之999。因此,您最好遵循以下规则。

  1. 只要运算符的含义不明确且无可争辩,就不应重载。 而是提供一个功能齐全的名称。
    基本上,重载运算符的首要原则就是说:不要这样做。这可能看起来很奇怪,因为关于运算符重载有很多已知的知识,因此许多文章,书籍章节和其他文本都涉及到这一切。尽管有这些看似显而易见的证据,但只有极少数情况下适合使用运算符重载。原因是实际上很难理解运算符应用程序背后的语义,除非在应用程序域中对运算符的使用是众所周知的且无可争议。与普遍的看法相反,几乎没有这种情况。

  2. 始终遵守操作员的众所周知的语义。
    C ++对重载运算符的语义没有任何限制。您的编译器将很乐意接受实现二进制+运算符的代码,以从其右操作数中减去。然而,这样的运营商的用户将不会怀疑表达a + b减去ab。当然,这假定在应用程序域中运算符的语义是无可争议的。

  3. 始终提供一组相关操作中的所有内容。
    运算符彼此相关,并且与其他操作相关。如果您的类型支持a + b,则用户也希望能够拨打电话a += b。如果它支持前缀递增++a,他们将期望也a++能正常工作。如果他们能够检查是否a < b,他们肯定会希望也能检查是否a > b。如果他们可以复制构造您的类型,他们希望分配也能正常工作。


继续进行成员与非成员之间的决定


16
我知道唯一违反这些规定的就是boost::spirit大声笑。
Billy ONeal 2010年

66
@Billy:根据某些人的说法,滥用+字符串连接是一种违法,但是到现在为止,它已成为行之有效的实践,因此这似乎很自然。尽管我确实记得一个自酿字符串类,但我在90年代看到&了为此目的使用二进制文件的方法(已建立的实用程序请参考BASIC)。但是,是的,将其放入std lib基本上是一成不变的。滥用<<>>IO,BTW也是如此。为什么左移是显而易见的输出操作?因为当我们看到第一个“你好,世界!”时,我们都知道了这一点。应用。而且没有其他原因。
2010年

5
@curiousguy:如果您必须解释一下,这显然不是很清楚也没有争议。同样,如果您需要讨论或防御超载。
2011年

5
@sbi:“同行评议”始终是一个好主意。对我来说,错误选择的运算符与错误选择的函数名称没有什么不同(我见过很多)。运算符只是功能。不多不少。规则是一样的。要了解一个主意是否好,最好的方法是了解要花多长时间。(因此,必须进行同行评审,但必须在没有教条和偏见的人们之间选择同行。)
Emilio Garavaglia

5
@sbi对我来说,唯一绝对明显且无可争辩的事实operator==是它应该是一个等效关系(IOW,您不应使用非信号NaN)。容器上有许多有用的等效关系。平等意味着什么?“ a等于b”表示ab具有相同的数学值。(非NaN)的数学值的概念float很明确,但是容器的数学值可以具有许多不同的(类型递归)有用的定义。平等的最强定义是“它们是相同的对象”,这是没有用的。
curiousguy 2012年

265

C ++中运算符重载的通用语法

您不能更改C ++中内置类型的运算符的含义,只能对用户定义的类型1重载运算符。即,至少一个操作数必须是用户定义的类型。与其他重载函数一样,运算符只能对一组特定参数重载一次。

并非所有运算符都可以在C ++中重载。在不能重载的运算符中有:. :: sizeof typeid .*和C ++中唯一的三元运算符,?:

在C ++中可以重载的运算符包括:

  • 算术运算符:+ - * / %+= -= *= /= %=(所有二进制中缀);+ -(一元前缀);++ --(一元前缀和后缀)
  • 位操作:& | ^ << >>&= |= ^= <<= >>=(所有二进制中缀);~(一元前缀)
  • 布尔代数:(== != < > <= >= || &&所有二进制中缀);!(一元前缀)
  • 内存管理: new new[] delete delete[]
  • 隐式转换运算符
  • 其他:(= [] -> ->* , 所有二进制中缀);* &(所有一元前缀)()(函数调用,n元中缀)

但是,您可以重载所有这些事实并不意味着您应该这样做。请参阅运算符重载的基本规则。

在C ++中,运算符以具有特殊名称函数的形式被重载。为具有其他功能,重载操作符可以通常被实现为一个其左操作数的类型的成员函数或作为非成员函数。您是自由选择还是必须使用其中之一,取决于多个条件。2应用到对象x的一元运算符@3作为operator@(x)或作为调用x.operator@()@应用于对象x和的二进制中缀运算符y称为as operator@(x,y)或as x.operator@(y)4

实施为非成员函数的运算符有时是其操作数类型的朋友。

1 “用户定义”一词可能会引起误解。C ++区分内置类型和用户定义类型。前者属于int,char和double。后者属于所有struct,class,union和enum类型,包括那些来自标准库的类型,即使它们不是由用户定义的。

2 此常见问题的后续部分将对此进行介绍。

3 @是不是在C的有效运营++这就是为什么我使用它作为一个占位符。

4 C ++中唯一的三元运算符不能重载,并且唯一的n元运算符必须始终实现为成员函数。


继续遵循C ++中运算符重载的三个基本规则


~是一元前缀,不是二进制中缀。
mrkj 2012年

1
.*不可重载运算符列表中缺少。
celticminstrel 2015年

1
@Mateen我使用一个占位符而不是一个真正的运算符,以表明这与特殊运算符无关,但适用于所有这些运算符。而且,如果您想成为C ++程序员,则应该学会注意甚至是小字体。:)
2015年

1
@HR:如果您阅读了本指南,就会知道出了什么问题。我通常建议您阅读问题中链接的前三个答案。那不应该超过您一生的半小时,并且可以使您基本了解。您可以在以后查询特定于运营商的语法。您的特定问题建议您尝试重载operator+()作为成员函数,但给它一个自由函数的签名。看这里
sbi

1
@sbi:我已经阅读了第一篇三篇文章,并感谢您提出。:)我将尝试解决问题,否则我认为最好在一个单独的问题上提出。再次感谢您使我们的生活如此轻松!:D
侯赛因·拉纳玛

251

会员与非会员之间的决定

二进制运算符=(赋值),[](数组订阅),->(成员访问)以及n元()(函数调用)运算符必须始终实现为成员函数,因为语言的语法要求它们。

其他运算符既可以实现为成员,也可以实现为非成员。但是,其中一些通常必须实现为非成员函数,因为您无法修改其左操作数。其中最突出的是输入和输出运算符<<>>,其左操作数是标准库中的流类,您不能更改它们。

对于必须选择将其实现为成员函数或非成员函数的所有运算符,请使用以下经验法则来确定:

  1. 如果它是一元运算符,请将其实现为成员函数。
  2. 如果二进制运算符将两个操作数均等地对待(使它们不变),则将该运算符实现为非成员函数。
  3. 如果二进制运算符不能平等地对待其两个操作数(通常会更改其左操作数),则在必须访问该操作数的私有部分的情况下,使其成为其左操作数类型的成员函数可能会很有用。

当然,与所有经验法则一样,也有例外。如果你有类型

enum Month {Jan, Feb, ..., Nov, Dec}

并且要为其重载递增和递减运算符,则不能作为成员函数来执行此操作,因为在C ++中,枚举类型不能具有成员函数。因此,您必须将其作为自由函数进行重载。和operator<()嵌套类模板中的类模板是写起来更简单,当作为类定义的成员函数内联方式完成阅读。但是这些确实是罕见的例外。

(但是,如果发生异常,请不要忘记const操作数的-ness 问题,对于成员函数,该操作数成为隐式this参数。如果运算符作为非成员函数,则将其最左端的参数作为const参考,则与成员函数相同的运算符必须const在末尾具有a *this作为const参考。)


继续让Common运算符超载


9
Herb Sutter在Effective C ++(或C ++编码标准?)中的文章说,人们应该更喜欢非成员非友函数而不是成员函数,以增加类的封装。恕我直言,封装原因优先于您的经验法则,但不会降低经验法则的质量值。
paercebal

8
@paercebal:有效的C ++是Meyers 编写的,C ++编码标准是Sutter 编写的。您指的是哪一个?无论如何,我不喜欢operator+=()不成为会员的想法。它必须更改其左操作数,因此根据定义,它必须深入了解其内部。如果不成为会员,您会得到什么?
2010年

9
@sbi:C ++编码标准(Sutter)中的第44项当然,更喜欢编写非成员非朋友函数,仅当您实际上只能使用类的公共接口编写此函数时,它才适用。如果您不能(或可以但会严重影响性能),则必须使其成为成员或朋友。
Matthieu M. 2010年

3
@sbi:糟糕,有效,出色...难怪我把名字混了。无论如何,收益是要尽可能限制可以访问对象私有/受保护数据的功能的数量。这样,您可以增加类的封装,从而使类的维护/测试/演化更加容易。
paercebal

12
@sbi:一个例子。假设您正在使用operator +=append方法对String类进行编码。该append方法更加完善,因为您可以将参数的子字符串从索引i追加到索引n -1:用和append(string, start, end)进行+=调用追加似乎是合乎逻辑的。那时,append可以是成员方法,但不必成为成员方法,而使其成为非成员方法会减少使用String innards播放代码的数量,所以这是一件好事...。 ^ _ ^ ...start = 0end = string.sizeoperator +=
paercebal 2010年

165

转换运算符(也称为用户定义的转换)

在C ++中,您可以创建转换运算符,这些运算符使编译器可以在您的类型和其他定义的类型之间进行转换。转换运算符有两种,隐式和显式。

隐式转换运算符(C ++ 98 / C ++ 03和C ++ 11)

隐式转换操作符可以让编译器隐式转换(如之间的转换intlong用户定义的类型的值),以一些其它类型的。

以下是带有隐式转换运算符的简单类:

class my_string {
public:
  operator const char*() const {return data_;} // This is the conversion operator
private:
  const char* data_;
};

隐式转换运算符(如一参数构造函数)是用户定义的转换。尝试将调用与重载函数匹配时,编译器将授予一个用户定义的转换。

void f(const char*);

my_string str;
f(str); // same as f( str.operator const char*() )

乍一看,这似乎很有帮助,但是问题在于隐式转换甚至会在预期不到的时候启动。在以下代码中,void f(const char*)将被调用,因为my_string()不是lvalue,因此第一个不匹配:

void f(my_string&);
void f(const char*);

f(my_string());

初学者容易犯错,甚至经验丰富的C ++程序员有时也会感到惊讶,因为编译器会选择他们不怀疑的重载。这些问题可以通过显式转换运算符缓解。

显式转换运算符(C ++ 11)

与隐式转换运算符不同,显式转换运算符在您不希望它们出现时永远不会起作用。以下是带有显式转换运算符的简单类:

class my_string {
public:
  explicit operator const char*() const {return data_;}
private:
  const char* data_;
};

注意 explicit。现在,当您尝试从隐式转换运算符执行意外的代码时,会出现编译器错误:

prog.cpp:在函数'int main()'中:
prog.cpp:15:18:错误:没有匹配的函数调用'f(my_string)'
prog.cpp:15:18:注意:候选人为:
prog.cpp:11:10:注意:void f(my_string&)
prog.cpp:11:10:注意:参数1从'my_string'到'my_string&'的未知转换
prog.cpp:12:10:注意:void f(const char *)
prog.cpp:12:10:注意:参数1从'my_string'到'const char *'的未知转换

要调用显式强制转换运算符,必​​须使用static_cast,C样式强制转换或构造函数样式强制转换(即T(value))。

但是,有一个例外:允许编译器隐式转换为bool。另外,编译器在转换为以后,不允许进行另一次隐式转换bool(编译器一次只能进行2次隐式转换,但最多只能进行1个用户定义的转换)。

由于编译器不会强制bool转换“过去” ,因此显式转换运算符现在无需使用Safe Bool习惯用法。例如,C ++ 11之前的智能指针使用Safe Bool习惯用法来防止转换为整数类型。在C ++ 11中,智能指针使用显式运算符代替,因为在将类型显式转换为bool之后,不允许编译器隐式转换为整数。

继续重载newdelete


148

重载newdelete

注意:这仅处理重载和的语法,而不处理此类重载运算符的实现。我认为重载的语义应有自己的FAQ,在运算符重载的主题下,我永远无法做到这一点。newdeletenewdelete

基本

在C ++中,当你写一个新的表达new T(arg)两件事情发生时,该表达式计算:首先operator new被调用,以获得原始内存,然后适当的构造函数T被调用来把这个原始内存为有效的对象。同样,删除对象时,首先调用其析构函数,然后将内存返回给operator delete
C ++允许您调整以下两个操作:内存管理和分配的内存中对象的构造/销毁。后者是通过为类编写构造函数和析构函数来完成的。通过编写自己的operator new和可以对内存管理进行微调operator delete

操作符重载的第一条基本规则- 请勿这样做 -特别适用于重载newdelete。使这些运算符过载的几乎唯一原因是性能问题内存限制,并且在许多情况下,其他操作(如更改所使用的算法)将提供比尝试调整内存管理更高的成本/收益比

C ++标准库带有一组预定义newdelete运算符。最重要的是:

void* operator new(std::size_t) throw(std::bad_alloc); 
void  operator delete(void*) throw(); 
void* operator new[](std::size_t) throw(std::bad_alloc); 
void  operator delete[](void*) throw(); 

前两个为对象分配/取消分配内存,后两个为对象数组分配/释放内存。如果您提供自己的版本,则它们不会过载,但会替换标准库中的版本。
如果您重载operator newoperator delete,即使您从未打算调用它,也应该始终重载匹配。原因是,如果构造函数在对新表达式求值时抛出异常,则运行时系统会将内存返回与被调用以operator delete匹配operator new以分配内存来创建对象的匹配项。如果不提供匹配项operator delete,默认名称为,这几乎总是错误的。
如果您重载了newdelete,那么您也应该考虑重载数组变量。

放置 new

C ++允许new和delete运算符采用其他参数。
所谓的“新放置”功能使您可以在某个地址处创建对象,该对象将传递给:

class X { /* ... */ };
char buffer[ sizeof(X) ];
void f()
{ 
  X* p = new(buffer) X(/*...*/);
  // ... 
  p->~X(); // call destructor 
} 

标准库为此提供了new和delete运算符的适当重载:

void* operator new(std::size_t,void* p) throw(std::bad_alloc); 
void  operator delete(void* p,void*) throw(); 
void* operator new[](std::size_t,void* p) throw(std::bad_alloc); 
void  operator delete[](void* p,void*) throw(); 

请注意,在上面给出的新展示位置示例代码中, operator delete,除非X的构造函数抛出异常,否则永远不要调用它。

您还可以重载newdelete其他参数。与用于new放置的其他参数一样,这些参数也在关键字后面的括号内列出new。仅出于历史原因,此类变体通常也称为“新放置”,即使它们的参数不是要在特定地址放置对象。

特定于类的new和delete

通常,您会希望微调内存管理,因为测量表明,经常创建和销毁特定类或一组相关类的实例,并且已针对运行时系统的默认内存管理进行了调整。一般性能,在这种情况下交易效率低下。为了改善这一点,您可以为特定的类重载new和delete:

class my_class { 
  public: 
    // ... 
    void* operator new();
    void  operator delete(void*,std::size_t);
    void* operator new[](size_t);
    void  operator delete[](void*,std::size_t);
    // ... 
}; 

因此,new和delete重载时的行为类似于静态成员函数。对于的对象my_classstd::size_t参数将始终为sizeof(my_class)。但是,也需要为派生类的动态分配的对象调用这些运算符,在这种情况下,它可能会更大。

全局新建和删除

要使全局new和delete重载,只需用我们自己的标准库替换预定义的运算符。但是,很少需要这样做。


11
我也不同意替换全局操作符new和delete通常是为了提高性能:相反,通常是用于错误跟踪。
Yttrill 2010年

1
您还应该注意,如果使用重载的new运算符,则还需要提供具有匹配参数的delete运算符。您说过,在有关全局新/删除的部分中,它并不是很重要。
Yttrill 2010年

13
@Yttrill,您感到困惑。的意义超载。“运算符超载”的含义是超载。这并不意味着字面上的函数会被重载,特别是 new运算符不会重载Standard的版本。@sbi没有相反的说法。通常称其为“重载新运算符”,就像说“重载加法运算符”一样。
Johannes Schaub-litb 2010年

1
@sbi:请参阅(或更好的链接)gotw.ca/publications/mill15.htm。对于有时使用nothrow新手的人来说,这只是一个好习惯。
Alexandre C.

1
“如果不提供匹配的运算符删除,则称为默认值。”->实际上,如果添加任何参数并且不创建匹配的删除,则根本不会调用运算符删除,并且会发生内存泄漏。(15.2.2,仅在找到合适的...运算符删除后,才释放对象占用的存储空间)
dascandy 2015年

46

为什么不能将operator<<对象与std::cout文件流式传输为成员函数?

假设您有:

struct Foo
{
   int a;
   double b;

   std::ostream& operator<<(std::ostream& out) const
   {
      return out << a << " " << b;
   }
};

鉴于此,您不能使用:

Foo f = {10, 20.0};
std::cout << f;

由于operator<<作为的成员函数而过载Foo,因此操作员的LHS必须是一个Foo对象。这意味着,您将需要使用:

Foo f = {10, 20.0};
f << std::cout

这是非常不直观的。

如果您将其定义为非成员函数,

struct Foo
{
   int a;
   double b;
};

std::ostream& operator<<(std::ostream& out, Foo const& f)
{
   return out << f.a << " " << f.b;
}

您将可以使用:

Foo f = {10, 20.0};
std::cout << f;

这是非常直观的。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.