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


注意:答案是在 特定的订单,但由于许多用户根据投票排序答案,而不是他们给出的时间,这里是一个 答案索引 按照他们最有意义的顺序:

(注意:这是一个条目 Stack Overflow的C ++ FAQ。如果您想批评在此表单中提供常见问题解答的想法,那么 发布所有这一切的元发布 会是那样做的地方。该问题的答案在 C ++聊天室常见问题解答的想法首先出现在那里,所以你的答案很可能被那些提出这个想法的人阅读。)  


1845
2017-12-12 12:44


起源


如果我们要继续使用C ++ - FAQ标记,那么应该如何格式化条目。 - John Dibling
我为德国C ++社区撰写了一系列关于运算符重载的文章: 第1部分:C ++中的运算符重载 涵盖所有运营商的语义,典型用途和专业。它在这里与你的答案有一些重叠,但还有一些额外的信息。第2部分和第3部分提供了使用Boost.Operators的教程。您是否希望我翻译它们并将其添加为答案? - Arne Mertz
哦,还有英文翻译: 基础 和 常见做法 - Arne Mertz


答案:


常见的运算符过载

超载运营商的大部分工作都是锅炉板代码。这并不奇怪,因为操作符只是语法糖,它们的实际工作可以通过(通常转发到)普通函数来完成。但重要的是你要正确使用这种锅炉板代码。如果您失败,您的操作员代码将无法编译,或者您的用户代码将无法编译,或者您的用户代码将出现令人惊讶的行为。

分配操作员

关于作业有很多话要说。但是,大部分内容已经在说过了 GMan着名的Copy-And-Swap常见问题解答,所以我将在这里跳过大部分内容,仅列出完美的赋值运算符以供参考:

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

Bitshift运算符(用于流I / O)

bitshift运算符 << 和 >>尽管仍然在硬件接口中用于从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>>,只有在读取本身成功时才需要手动设置流的状态,但结果不是预期的结果。

函数调用运算符

用于创建函数对象的函数调用运算符(也称为函子)必须定义为a 会员 函数,所以它总是有隐含的 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  与所有的经验法则一样,有时也可能有理由打破这一个。如果是这样,不要忘记二进制比较运算符的左侧操作数,对于成员函数将是 *this, 需要是 const也是。因此,作为成员函数实现的比较运算符必须具有以下签名:

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

(注意 const 最后。)

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

算术运算符

一元算术运算符

一元递增和递减运算符有前缀和后缀两种风格。为了告诉另一个,后缀变体采用额外的伪int参数。如果重载增量或减量,请确保始终实现前缀和后缀版本。 这是增量的规范实现,减量遵循相同的规则:

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

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

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

2  另请注意,后缀变体功能更多,因此使用效率低于前缀变量。这是一个很好的理由通常更喜欢前缀增量而不是后缀增量。虽然编译器通常可以优化内置类型的后缀增量的额外工作,但它们可能无法对用户定义的类型执行相同的操作(这可能是像列表迭代器一样无辜地看起来的东西)。一旦你习惯了 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[] (在这种情况下,您可以省略非const变量),您应该始终提供运算符的两种变体。

如果已知value_type引用内置类型,则运算符的const变量应返回副本而不是const引用。

指针类型的运算符

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

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

请注意,这些也几乎总是需要const和非const版本。 为了 -> 运营商,如果 value_type 是的 class (要么 struct 要么 union)类型,另一个 operator->() 被称为递归,直到 operator->() 返回非类类型的值。

一元地址运算符永远不应该重载。

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


继续 转换运算符


898
2017-12-12 12:47



operator->() 实际上是 非常 奇怪的。它不需要返回 value_type*  - 实际上,它可以返回另一个类类型, 只要类类型有 operator->(),随后将被调用。这种递归调用 operator->()继续进行直到 value_type* 返回类型发生。疯狂! :) - j_random_hacker
我不同意类指针运算符的const / non-const版本,例如: `const value_type&operator *()const;` - 这就像有一个 T* const 回来一个 const T& 在取消引用时,情况并非如此。或者换句话说:const指针并不意味着const指针。事实上,模仿并非易事 T const *  - 这是整体的原因 const_iterator 标准库中的东西。结论:签名应该是 reference_type operator*() const; pointer_type operator->() const - Arne Mertz
一条评论:建议的二进制算术运算符的实现并不是那么有效。 Se Boost运算符标题simmetry注意: boost.org/doc/libs/1_54_0/libs/utility/operators.htm#symmetry 如果使用第一个参数的本地副本do + =,则可以避免多一个副本,并返回本地副本。这样可以实现NRVO优化。 - Manu343726
正如我在聊天中提到的, L <= R 也可以表示为 !(R < L) 代替 !(L > R)。可能会在难以优化的表达式中保存额外的内联层(这也是Boost.Operators实现它的方式)。 - TemplateRex
@thomthom:如果某个类没有可公开访问的API来获取其状态,则必须使所有需要访问其状态的成员成为成员或者 friend 班上的。当然,这对所有运营商也是如此。 - sbi


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

当谈到C ++中的运算符重载时,有 你应该遵循三个基本规则。与所有这些规则一样,确实有例外。有时人们偏离了它们,结果并不是错误的代码,但这种积极的偏差很少而且很远。至少,我所看到的100个这样的偏差中有99个是没有道理的。然而,它可能也是1000中的999.所以你最好坚持以下规则。

  1. 只要操作员的意义不明显且无可争议,它就不应该超载。  相反,提供具有精心选择的名称的功能。
    基本上,重载运营商的第一个也是最重要的规则是: 不要这样做。这可能看起来很奇怪,因为有很多关于运算符重载的知识,因此很多文章,书籍章节和其他文本都涉及到这一切。尽管这看似明显的证据, 只有极少数情况下运算符重载是合适的。原因是实际上很难理解运算符应用背后的语义,除非在应用程序域中使用运算符是众所周知且无可争议的。与流行的看法相反,情况并非如此。

  2. 始终坚持运营商众所周知的语义。
    C ++对重载运算符的语义没有限制。您的编译器将很乐意接受实现二进制文件的代码 + 运算符从其右操作数中减去。但是,这样的运营商的用户永远不会怀疑这种表达方式 a + b 减去 a 从 b。当然,这假设应用程序域中的运算符的语义是无可争议的。

  3. 始终提供一系列相关操作。
    运营商彼此相关以及其他业务。如果您的类型支持 a + b,用户希望能够打电话 a += b也是。如果它支持前缀增量 ++a,他们会期待 a++ 工作也好。如果他们可以检查是否 a < b,他们肯定也希望能够检查是否 a > b。如果他们可以复制构造您的类型,他们希望分配也可以工作。


继续 会员与非会员之间的决定


441
2017-12-12 12:45



我所知道的唯一违反其中任何一项的是 boost::spirit 大声笑。 - Billy ONeal
@Billy:据一些人说,虐待 + 对于字符串连接是一种违规,但它现在已成为完善的实践,所以它似乎很自然。虽然我确实记得我在90年代看到的使用二进制的家庭酿造字符串类 &为此目的(指已建立实践的BASIC)。但是,是的,把它放入std lib基本上就是这样。虐待同样如此 << 和 >> 对于IO,BTW。为什么左移是明显的输出操作?因为当我们看到第一个“Hello,world!”时我们都学到了它。应用。而且没有其他原因。 - sbi
@curiousguy:如果你必须解释它,它显然并不明显且无可争议。同样,如果你需要讨论或捍卫超载。 - sbi
@sbi:“同行评审”总是一个好主意。对我来说,一个选择不当的操作符与选择不当的函数名称没有区别(我看到很多)。操作员只是功能。不多也不少。规则是一样的。要了解一个想法是否合适,最好的方法是了解需要多长时间才能被理解。 (因此,同行评审是必须的,但必须在没有教条和偏见的人之间选择同伴。) - Emilio Garavaglia
@sbi对我来说,唯一绝对明显且无可争议的事实 operator== 是它应该是一个等价关系(IOW,你不应该使用非信令NaN)。容器上有许多有用的等价关系。平等意味着什么? “a 等于 b“ 意思是 a 和 b 具有相同的数学价值。 a(非NaN)的数学概念 float 很清楚,但容器的数学值可以有许多不同的(类型递归)有用的定义。平等的最强定义是“它们是相同的对象”,它是无用的。 - curiousguy


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

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

并非所有运算符都可以在C ++中重载。在无法超载的运营商中有: .  ::  sizeof  typeid  .* 并且是C ++中唯一的三元运算符, ?: 

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

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

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

在C ++中,运算符以形式重载 具有特殊名称的函数。与其他函数一样,重载运算符通常可以实现为 左操作数类型的成员函数 或者作为 非会员职能。您是否可以自由选择或约束使用其中任何一个取决于几个标准。2 一元算子 @3,应用于对象x,调用为 operator@(x) 或者作为 x.operator@()。二进制中缀运算符 @,应用于对象 x 和 y,被称为 operator@(x,y) 或者作为 x.operator@(y)4 

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

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

2  这包括在内 后来的一部分 这个FAQ。

3  @ 在C ++中不是一个有效的运算符,这就是我将它用作占位符的原因。

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


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


230
2017-12-12 12:46



%= 不是“位操纵”操作符 - curiousguy
~ 是一元前缀,而不是二进制中缀。 - mrkj
.* 在不可重载的运算符列表中缺少。 - celticminstrel
@celticminstrel:确实,没有人注意到了4.5年......感谢你指出它,我把它放进去了。 - sbi
@ H.R。:如果您阅读本指南,您就会知道出了什么问题。我一般建议您阅读从问题中链接的前三个答案。这应该不会超过你生命的半小时,并给你一个基本的理解。您可以在以后查找特定于运算符的语法。您的具体问题表明您试图超载 operator+() 作为一个成员函数,但给它一个自由函数的签名。看到 这里。 - sbi


会员与非会员之间的决定

二元运算符 = (分配), [] (数组订阅), -> (成员访问),以及n-ary ()(函数调用)运算符,必​​须始终实现为 成员职能,因为语言的语法要求他们。

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

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

  1. 如果是的话 一元运算符,实现它作为一个 会员 功能。
  2. 如果二元运算符处理 两个操作数相同 (它保持不变),将此运算符实现为 非会员 功能。
  3. 如果是二元运算符  对待它的两个操作数 一样 (通常它会改变它的左操作数),它可能是有用的 会员 左操作数的类型的函数,如果它必须访问操作数的私有部分。

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

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

并且你想为它重载递增和递减运算符,你不能将它作为成员函数来执行,因为在C ++中,枚举类型不能具有成员函数。所以你必须将它作为一个自由函数重载。和 operator<() 对于嵌套在类模板中的类模板,当在类定义中作为成员函数内联完成时,更容易编写和读取。但这些确实是罕见的例外。

(然而, 如果 你做了一个例外,不要忘记这个问题 const - 对于成员函数,操作数的含义变为隐式 this 论据。如果作为非成员函数的运算符将其最左边的参数作为a const 引用,与成员函数相同的运算符需要有一个 const 最后做 *this 一个 const 参考。)


继续 常见的运算符过载


212
2017-12-12 12:49



Herb Sutter在Effective C ++(或者是C ++编码标准?)中的项目表示,应该更喜欢非成员非友元函数到成员函数,以增加类的封装。恕我直言,封装原因优先于您的经验法则,但它不会降低您的经验法则的质量值。 - paercebal
@paercebal: 有效的C ++ 是由迈耶斯, C ++编码标准 萨特。你指的是哪一个?无论如何,我不喜欢这样的想法,比方说, operator+=() 不是会员。它必须改变它的左手操作数,因此根据定义它必须深入挖掘其内部。如果不成为会员,你会得到什么? - sbi
@sbi:C ++编码标准中的第44项(Sutter) 喜欢编写非成员非友情函数当然,它只适用于只使用类的公共接口实际编写此函数的情况。如果你不能(或者可以,但它会严重阻碍性能),那么你必须让它成为会员或朋友。 - Matthieu M.
@sbi:哎呀,有效,特殊......难怪我把名字混在一起。无论如何,增益是尽可能地限制有权访问对象私有/受保护数据的函数的数量。这样,您可以增加类的封装,使其维护/测试/演变更容易。 - paercebal
@sbi:一个例子。假设您正在使用两者编写String类 operator += 和 append 方法。该 append 方法更完整,因为您可以将参数的子字符串从索引i追加到索引n -1: append(string, start, end) 这似乎合乎逻辑 += 呼叫附加 start = 0 和 end = string.size。在那一刻,追加可能是一个成员方法,但是 operator += 不需要成为成员,并使其成为非成员会减少使用String内部的代码数量,所以这是一件好事.... ^ _ ^ ... - paercebal


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

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

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

隐式转换运算符允许编译器隐式转换(如之间的转换) int 和 long)用户定义类型的值对某些其他类型。

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

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() 不是 左值,所以第一个不匹配:

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,显式转换运算符现在删除了对的需要 安全布尔成语。例如,C ++ 11之前的智能指针使用Safe Bool习惯用法来防止转换为整数类型。在C ++ 11中,智能指针使用显式运算符,因为在将类型显式转换为bool之后,不允许编译器隐式转换为整数类型。

继续 超载 new 和 delete


144
2018-05-17 18:32





超载 new 和 delete

注意: 这只涉及到 句法 超载 new 和 delete,而不是 履行 这样的重载运算符。我认为重载的语义 new 和 delete 值得拥有自己的FAQ在运算符重载的主题内,我永远不能正义。

基本

在C ++中,当你写一个 新的表达 喜欢 new T(arg) 评估此表达式时会发生两件事:第一 operator new 被调用以获取原始内存,然后是相应的构造函数 T 被调用以将此原始内存转换为有效对象。同样,当你删除一个对象时,首先调用它的析构函数,然后返回内存 operator delete
C ++允许您调整这两个操作:内存管理以及在分配的内存中构造/销毁对象。后者是通过为类编写构造函数和析构函数来完成的。微调内存管理是通过编写自己的内容来完成的 operator new 和 operator delete

运算符重载的第一个基本规则 - 不要这样做  - 特别适用于超载 new 和 delete。几乎是使这些运算符超载的唯一原因是 性能问题 和 记忆约束,在许多情况下,其他行动,如 更改算法 用过,会提供很多 更高的成本/收益率 而不是试图调整内存管理。

C ++标准库附带一组预定义的 new 和 delete 运营商。最重要的是这些:

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 new,你应该总是重载匹配 operator delete即使你从不打算打电话给它。原因是,如果构造函数在评估新表达式时抛出,则运行时系统会将内存返回到 operator delete 匹配 operator new 被调用来分配内存以创建对象。如果没有提供匹配 operator delete,默认的一个被调用,这几乎总是错误的。
如果你超载 new 和 delete,你也应该考虑重载数组变体。

放置 new

C ++允许new和delete运算符采用其他参数。
所谓的placement new允许您在某个地址创建一个对象,该地址传递给:

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的构造函数抛出异常。

你也可以超载 new 和 delete 与其他论点。与放置new的附加参数一样,这些参数也列在关键字后面的括号内 new。仅仅由于历史原因,这些变体通常也称为放置新的,即使它们的参数不是用于将对象放置在特定地址。

特定于类的新建和删除

最常见的是,您需要微调内存管理,因为测量已经显示特定类或一组相关类的实例经常被创建和销毁,并且运行时系统的默认内存管理已经调整为一般表现,在这种特定情况下效率低下。要改进这一点,您可以为特定类重载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,只需将标准库的预定义运算符替换为我们自己的运算符。但是,这很少需要完成。


131
2017-12-12 13:07



我也不同意更换全局运算符new和delete通常是为了性能:相反,它通常用于bug跟踪。 - Yttrill
您还应注意,如果使用重载的新运算符,则还需要为删除运算符提供匹配的参数。你在全球新/删除的部分中说它不是很感兴趣。 - Yttrill
@Yttrill你很困惑。该 含义 变得超载。 “运算符重载”意味着意义重载。它并不意味着字面上的函数被重载,并且 尤其是 operator new不会超出Standard的版本。 @sbi没有声称相反。通常称其为“重载新”,因为通常会说“重载加法运算符”。 - Johannes Schaub - litb
@sbi:看(或更好,链接到) gotw.ca/publications/mill15.htm 。对于有时使用的人来说,这只是一种很好的做法 nothrow 新。 - Alexandre C.
“如果你没有提供匹配的运算符删除,则默认调用” - >实际上,如果你添加任何参数而不创建匹配删除,则根本不调用operator delete,并且你有内存泄漏。 (15.2.2,只有找到适当的...运算符删除,才会释放对象占用的存储空间) - dascandy


为什么不能 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;

这非常直观。


29
2018-01-22 19:00