ChatGPT解决这个技术问题 Extra ChatGPT

运算符重载的基本规则和习语是什么?

注意:答案是按特定顺序给出的,但是由于许多用户根据投票而不是给出的时间对答案进行排序,所以这里是一个按最有意义的顺序排列的答案索引:

C++中运算符重载的一般语法

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

会员与非会员之间的决定

重载的常用运算符 赋值运算符 输入和输出运算符 函数调用运算符 比较运算符 算术运算符 数组下标运算符,用于指针类类型

赋值运算符

输入和输出运算符

函数调用运算符

比较运算符

算术运算符

数组下标

类指针类型的运算符

转换运算符

重载new和delete

(注意:这是 Stack Overflow's C++ FAQ 的一个条目。如果您想批评以这种形式提供常见问题解答的想法,那么 the posting on meta that started all this 将是这样做的地方。回答该问题在 C++ chatroom 中进行监控,FAQ 想法首先从此处开始,因此您的回答很可能会被提出该想法的人阅读。)

如果我们要继续使用 C++-FAQ 标签,这就是条目的格式。
我为德国 C++ 社区写了一系列关于运算符重载的简短文章:Part 1: operator overloading in C++ 涵盖了所有运算符的语义、典型用法和特性。它与您在这里的答案有一些重叠,但是还有一些额外的信息。第 2 部分和第 3 部分制作了使用 Boost.Operators 的教程。您希望我翻译它们并将它们添加为答案吗?
哦,还有英文翻译:the basicscommon practice
缺少地址运算符 operator&
@Red.Wave:实际上,即使在自己的段落中,常见的运算符答案的末尾也有一个句子,但它说“不要这样做”。我认为是 Dinkumware 的 Pete Becker(后来被微软收购的 std lib 的公司)曾经说过那些重载 operator&() 然后期望结果类型与标准库一起工作的人应该被迫实现 std执行这个奇迹的lib。 IOW,如果您认为您有一个重载此运算符的应用程序,我很想听听。 (不过,不要屏住呼吸等待掌声。)

s
sbi

常用运算符重载

重载运算符的大部分工作是样板代码。这并不奇怪,因为运算符只是语法糖,它们的实际工作可以通过(并且通常被转发到)普通函数来完成。但重要的是你要正确地获得这个样板代码。如果你失败了,要么你的操作员的代码不能编译,要么你的用户代码不能编译,或者你的用户代码的行为会出人意料。

赋值运算符

关于任务有很多话要说。不过大部分已经在GMan's famous Copy-And-Swap FAQ中讲过了,这里就跳过大部分,只列出完美的赋值运算符供参考:

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

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

移位运算符 <<>> 尽管仍然用于硬件接口以实现它们从 C 继承的位操作功能,但在大多数应用程序中作为重载流输入和输出运算符已变得更加普遍。有关作为位操作运算符的重载指南,请参阅下面有关二元算术运算符的部分。当您的对象与 iostreams 一起使用时,为了实现您自己的自定义格式和解析逻辑,请继续。

在最常见的重载运算符中,流运算符是二元中缀运算符,其语法对它们应该是成员还是非成员没有任何限制。由于它们改变了左参数(它们改变了流的状态),根据经验法则,它们应该被实现为左操作数类型的成员。但是,它们的左操作数是来自标准库的流,虽然标准库定义的大多数流输出和输入操作符确实定义为流类的成员,但是当您为自己的类型实现输出和输入操作时,您不能更改标准库的流类型。这就是为什么您需要为您自己的类型实现这些运算符作为非成员函数。两者的规范形式如下:

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>> 时,仅当读取本身成功时才需要手动设置流的状态,但结果不是预期的。

函数调用运算符

用于创建函数对象的函数调用运算符,也称为函子,必须定义为 member 函数,因此它始终具有 member 的隐式 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 做了一个额外的 copy.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[] 返回的数据元素(在这种情况下,您可以省略非常量变体),否则您应该始终提供运算符的两种变体。

如果已知 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 和 non-const 版本。对于 -> 运算符,如果 value_typeclass(或 structunion)类型,则递归调用另一个 operator->(),直到 operator->() 返回非类类型的值。

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

对于 operator->*(),请参阅 this question。它很少使用,因此很少超载。事实上,即使是迭代器也不会重载它。

继续Conversion Operators


operator->() 实际上非常很奇怪。不需要返回 value_type* - 事实上,它可以返回另一个类类型,前提是该类类型有一个 operator->(),随后会调用它。这种对 operator->() 的递归调用会继续进行,直到出现 value_type* 返回类型。疯狂! :)
这不完全是关于有效性。这是关于我们不能在(非常)少数情况下以传统的惯用方式做到这一点:当我们计算结果时两个操作数的定义需要保持不变。正如我所说,有两个经典示例:矩阵乘法和多项式乘法。我们可以根据 *= 定义 *,但这会很尴尬,因为 *= 的第一个操作会创建一个新对象,这是计算的结果。然后,在 for-ijk 循环之后,我们将用 *this 交换这个临时对象。 IE。 1.copy, 2.operator*, 3.swap
我不同意您的类似指针的运算符的 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
一条评论:建议的二元算术运算符的实现并不高效。 Se Boost operator headers simmetry 注意:boost.org/doc/libs/1_54_0/libs/utility/operators.htm#symmetry 如果您使用第一个参数的本地副本,执行 += 并返回本地副本,则可以避免多副本。这启用了 NRVO 优化。
关于比较运算符的部分将需要更新以提及 operator<=>() 当这是最终的 (C++20)。
C
Community

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

当谈到 C++ 中的运算符重载时,您应该遵循三个基本规则。与所有此类规则一样,确实有例外。有时人们已经偏离了它们,结果不是糟糕的代码,但这种积极的偏差很少而且相差甚远。至少,我见过的 100 个这样的偏差中有 99 个是不合理的。但是,它也可能是 1000 中的 999。因此,您最好遵守以下规则。

每当一个运算符的含义不是很清楚和无可争议的时候,它就不应该被重载。相反,提供一个名称选择得当的函数。基本上,重载运算符的首要规则,其核心是:不要这样做。这可能看起来很奇怪,因为有很多关于运算符重载的知识,所以很多文章、书籍章节和其他文本都涉及到这一切。但是尽管有这些看似明显的证据,但只有极少数情况下运算符重载是合适的。原因是实际上很难理解运算符应用背后的语义,除非运算符在应用领域的使用是众所周知且无可争议的。与普遍的看法相反,这种情况几乎从未发生过。始终坚持运营商众所周知的语义。 C++ 对重载运算符的语义没有任何限制。您的编译器将愉快地接受实现二进制 + 运算符的代码以从其右操作数中减去。但是,这种运算符的用户永远不会怀疑表达式 a + b 会从 b 中减去 a。当然,这是假设算子在应用领域的语义是无可争议的。始终提供一组相关操作中的所有操作。运算符相互关联并与其他操作相关。如果您的类型支持 a + b,那么用户也希望能够调用 a += b。如果它支持前缀增量 ++a,他们会期望 a++ 也能正常工作。如果他们可以检查是否 a < b,他们肯定会期望也能够检查是否 a > b。如果他们可以复制构造您的类型,他们希望分配也能工作。

继续The Decision between Member and Non-member


我知道的唯一违反任何这些的是boost::spirit大声笑。
@Billy:根据一些人的说法,滥用 + 进行字符串连接是一种违规行为,但它现在已经成为公认的实践,因此看起来很自然。虽然我确实记得我在 90 年代看到的一个自制字符串类,它为此目的使用二进制 &(参考 BASIC 以获得已建立的实践)。但是,是的,将它放入标准库中基本上是一成不变的。对于 IO 滥用 <<>> 也是如此,顺便说一句。为什么左移是明显的输出操作?因为当我们看到我们的第一个“Hello, world!”时,我们都知道了它。应用。而且没有其他原因。
@curiousguy:如果你必须解释它,它显然不是很清楚和无可争议的。同样,如果您需要讨论或捍卫超载。
@sbi:“同行评审”总是一个好主意。对我来说,选择不当的运算符与选择不当的函数名没有什么不同(我见过很多)。运算符只是函数。不多不少。规则是一样的。要了解一个想法是否好,最好的方法是了解需要多长时间才能被理解。 (因此,必须进行同行评审,但必须在没有教条和偏见的人之间选择同行。)
@sbi 对我来说,关于 operator== 的唯一绝对明显和无可争辩的事实是它应该是一个等价关系(IOW,你不应该使用非信号 NaN)。容器上有许多有用的等价关系。平等是什么意思? “a 等于 b”表示 ab 具有相同的数学值。 (非 NaN)float 的数学值的概念很清楚,但容器的数学值可以有许多不同的(类型递归)有用的定义。平等的最强定义是“它们是相同的对象”,它是无用的。
C
Community

会员与非会员之间的决定

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

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

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

如果是一元运算符,则将其实现为成员函数。如果二元运算符同等对待两个操作数(保持不变),则将此运算符实现为非成员函数。如果一个二元运算符没有平等地对待它的两个操作数(通常它会改变它的左操作数),如果它必须访问操作数的私有部分,让它成为它的左操作数类型的成员函数可能会很有用。

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

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

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

(但是,如果你做了一个例外,不要忘记操作数的 const-ness 问题,对于成员函数,它成为隐式 this 参数。如果运算符作为非-member 函数将其最左边的参数作为 const 引用,与成员函数相同的运算符需要在末尾有一个 const 以使 *this 成为 const 引用。)

继续Common operators to overload


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

C++中运算符重载的一般语法

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

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

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

算术运算符:+ - * / % 和 += -= *= /= %=(所有二进制中缀); + - (一元前缀); ++ -- (一元前缀和后缀)

位操作: & | ^ << >> 和 &= |= ^= <<= >>= (所有二进制中缀); ~(一元前缀)

布尔代数:== != < > <= >= || && (所有二进制中缀); ! (一元前缀)

内存管理:new new[] delete delete[]

隐式转换运算符

miscellany: = [] -> ->* , (所有二进制中缀); * & (所有一元前缀) () (函数调用, n-ary 中缀)

然而,你可以超载所有这些并不意味着你应该这样做。请参阅运算符重载的基本规则。

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

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

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

2 这在本常见问题解答的 a later part 中有所介绍。

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

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

继续The Three Basic Rules of Operator Overloading in C++


~ 是一元前缀,而不是二元中缀。
不可重载运算符列表中缺少 .*
@Mateen 我 想要 使用占位符而不是真正的运算符,以明确这不是关于特殊运算符,而是适用于所有运算符。而且,如果你想成为一名 C++ 程序员,你应该学会注意小字体。 :)
@HR:如果您阅读了本指南,您就会知道出了什么问题。我通常建议您阅读与该问题相关的前三个答案。这不应该超过你生命的半小时,并且给你一个基本的了解。您可以稍后查找特定于运算符的语法。您的具体问题建议您尝试重载 operator+() 作为成员函数,但给它一个自由函数的签名。请参阅here
@sbi:我已经阅读了前三篇文章,感谢您制作它们。 :) 我会尝试解决这个问题,否则我认为最好单独提出一个问题。再次感谢您让我们的生活如此轻松! :D
C
Community

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

在 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: In function ‘int main()’:
prog.cpp:15:18: error: no matching function for call to ‘f(my_string)’
prog.cpp:15:18: note: candidates are:
prog.cpp:11:10: note: void f(my_string&)
prog.cpp:11:10: note:   no known conversion for argument 1 from ‘my_string’ to ‘my_string&’
prog.cpp:12:10: note: void f(const char*)
prog.cpp:12:10: note:   no known conversion for argument 1 from ‘my_string’ to ‘const char*’

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

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

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

继续Overloading new and delete


v
vaeVictis

重载 new 和 delete 运算符

注意:这里只处理重载 newdelete语法,不处理与此类重载运算符的实现。我认为重载的语义 new and delete deserve their own FAQ,在运算符重载的主题中,我永远无法做到公正。

基本

在 C++ 中,当您编写像 new T(arg) 这样的 新表达式 时,会在计算此表达式时发生两件事:首先 operator new 被调用以获取原始内存,然后调用 T 的适当构造函数将这个原始内存转换为有效对象。同样,当您删除一个对象时,首先调用其析构函数,然后将内存返回给 operator delete
C++ 允许您调整这两个操作:内存管理和对象的构造/销毁分配的内存。后者是通过为类编写构造函数和析构函数来完成的。通过编写您自己的 operator newoperator 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 new,您应该始终即使您从未打算调用它,也会重载匹配的 operator delete。原因是,如果构造函数在计算新表达式期间抛出异常,运行时系统会将内存返回到与 operator new 匹配的 operator delete,该 operator delete 被调用以分配内存以在其中创建对象。如果您没有提供匹配的 operator delete,调用默认的 operator delete,这几乎总是错误的。
如果您重载 newdelete,您也应该考虑重载数组变体。

新的展示位置

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(); 

请注意,在上面给出的放置 new 示例代码中,永远不会调用 operator delete,除非 X 的构造函数抛出异常。

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

特定于类的新建和删除

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

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

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

全局新建和删除

要重载全局 new 和 delete,只需将标准库的预定义运算符替换为我们自己的。然而,这很少需要做。


我也不同意替换全局运算符 new 和 delete 通常是为了性能:相反,它通常是为了跟踪错误。
您还应该注意,如果您使用重载的 new 运算符,您还需要提供具有匹配参数的删除运算符。您在关于全局新/删除的部分中说它并没有太大的兴趣。
@Yttrill 你把事情搞糊涂了。意义变得超载。 “运算符重载”是什么意思,意思是重载。这并不意味着字面上的函数被重载,特别是 operator new 不会重载标准版本。 @sbi 没有相反的说法。通常将其称为“重载 new”,就像通常说“重载加法运算符”一样。
@sbi:见(或更好,链接到) gotw.ca/publications/mill15.htm 。对于有时使用 nothrow new 的人来说,这只是一种好的做法。
“如果你没有提供匹配的操作符删除,则调用默认的” -> 实际上,如果你添加任何参数并且不创建匹配的删除,则根本不会调用任何操作符删除,并且你有内存泄漏。 (15.2.2,对象占用的存储空间只有在找到合适的 ... operator delete 时才会被释放)
C
Community

Why can't operator<< function for streaming objects to std::cout or to a file be a member function?

假设您有:

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;

这是非常直观的。


我一直在阅读有关重载 << 以与 std::cout 一起使用的内容,但是重载 << 以与同一类一起使用呢?在那种情况下,它可以是成员函数吗?
D
Dharman

简而言之,我将参考一些要点,这些要点是我在过去一周学习 Python 和 C++、oops 和其他东西时遇到的,所以它如下所示:

运算符的 Arity 不能被进一步修改!重载的运算符只能有一个默认参数,而函数调用运算符则不能。只有内置运算符可以重载,rest 不能!

有关更多信息,您可以参考以下链接,该链接会将您重定向到 GeekforGeeks 提供的文档。

https://www.geeksforgeeks.org/g-fact-39/