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

注意:答案是按照特定的顺序给出的,但是由于许多用户根据投票分类答案,而不是按照给定的时间排列答案,下面是答案索引,按其最有意义的顺序排列:

  • C ++中运算符重载的一般语法
  • C ++中运算符重载的三个基本规则
  • 会员与非会员之间的决定
  • 常见的运营商超载
    • 指派操作员
    • input和输出操作符
    • 函数调用操作符
    • 比较运营商
    • 算术运算符
    • arrays下标
    • 指针types的运算符
    • 转换运算符
  • 重载新的和删除

(注意:这是一个Stack Overflow的C ++常见问题解答的入口,如果你想批评在这个表单中提供FAQ的想法,那么在这个开始所有这些的meta上的贴子将是这个地方的答案。那个问题在C ++聊天室中进行监控,常见问题解决scheme首先出现,所以你的答案很可能会被那些提出这个想法的人阅读)。

常见的运营商超载

大多数重载操作员的工作是锅炉代码。 这并不奇怪,因为操作员只是语法上的糖,他们的实际工作可以通过简单的function来完成。 但是重要的是你能够正确使用这个锅炉代码。 如果你失败了,你的操作员的代码将不会编译,或者你的用户的代码不会被编译,或者你的用户的代码将会出乎意料。

指派操作员

关于任务有很多要说的。 然而,其中大部分内容已经在GMan着名的“复制与交换常见问题解答” ( Copy-and-Swap FAQ )中说过了,所以我将在这里略过大部分内容,只列出完美赋值运算符以供参考:

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

Bitshift操作符(用于streamI / O)

尽pipe在大多数应用中,位移运算符<<>>仍然用于从Cinheritance的位操作函数的硬件接口中,但是作为过载streaminput和输出运算符已经变得更普遍。 有关作为位操作运算符重载的指导,请参见下面有关二进制算术运算符的部分。 为了在您的对象与iostreams一起使用时实现自己的自定义格式和parsing逻辑,请继续。

stream操作符是最常见的重载操作符,它们是二元中缀操作符,其语法不论是成员还是非成员,都没有限制。 由于它们改变了左边的参数(它们改变了stream的状态),所以根据经验规则,它们应该被实现为左操作数types的成员。 但是,它们的左边的操作数是来自标准库的stream,虽然标准库定义的大部分stream输出和input操作符都被定义为stream类的成员,但是当您为自己的types实现输出和input操作时,不能更改标准库的streamtypes。 这就是为什么你需要为自己的types实现这些运算符作为非成员函数。 两者的规范forms是这样的:

 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>> ,手动设置stream的状态只有当读取成功时才是必要的,但结果不是预期的。

函数调用操作符

用于创build函数对象(也称为函子)的函数调用操作符必须被定义为成员函数,因此它总是隐含着成员函数的this参数。 除此之外,它可以被重载以获取任意数量的附加参数,包括零。

在整个C ++标准库中,函数对象总是被复制的。 因此你自己的函数对象应该很便宜。 如果一个函数对象绝对需要使用昂贵的数据进行复制,那么最好是把这些数据存储在其他地方,然后让函数对象引用它。

比较运营商

根据经验法则,二进制中缀比较运算符应该被实现为非成员函数1 。 一元前缀否定! 应该(根据相同的规则)作为一个成员函数来实现。 (但是超载它通常不是一个好主意。)

标准库的algorithm(例如std::sort() )和types(例如std::map )总是只能期望operator< 但是, types用户也期望所有其他运算符都存在 ,因此如果定义了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);} 

这里要注意的重要一点是,这些操作员中只有两个实际上做了任何事情,其他人只是把他们的论点转发到这两个操作员之一来做实际的工作。

重载其余二进制布尔运算符( ||&& )的语法遵循比较运算符的规则。 但是,你很难find这2个合理的用例。

1 就像所有的经验法则一样,有时候也可能有理由打破这一条。 如果是这样的话,不要忘记,二进制比较运算符的左边的操作数,对于成员函数来说,这将是*this ,也需要是const 因此,作为成员函数实现的比较运算符必须具有以下签名:

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

(注意最后的const 。)

2 应该注意的是||的内置版本 &&使用快捷语义。 而用户定义的(因为它们是方法调用的语法糖)不使用快捷方式语义。 用户会期望这些操作符具有快捷方式的语义,并且它们的代码可能依赖于它,因此非常build议不要定义它们。

算术运算符

一元算术运算符

一元增量和减量运算符有前缀和后缀的味道。 为了告诉其他人,后缀变体需要一个额外的虚拟int参数。 如果您超载递增或递减,一定要始终实现前缀和后缀版本。 这里是增量的规范实现,递减遵循相同的规则:

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

请注意,后缀变体是用前缀来实现的。 另外请注意,后缀做了一个额外的副本。 2

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

2 另外请注意,后缀变体做了更多的工作,因此比前缀变体使用效率低。 这是一个很好的理由,一般喜欢前缀增量超过后缀增量。 虽然编译器通常可以优化内置types的后缀增量的额外工作,但是对于用户定义的types(可能看起来像列表迭代器),它们可能无法做到这一点。 一旦你习惯了i++ ,当i不是内置types的时候,记得去做++i变得非常困难(再加上你在更改types时不得不改变代码),所以最好养成总是使用前缀增量的习惯,除非明确需要postfix。

二进制算术运算符

对于二元算术运算符,不要忘记服从第三个基本规则运算符重载:如果提供+ ,也提供+= ,如果你提供- ,不要忽略-=等。Andrew Koenig据说是第一个观察到复合赋值运算符可以作为非复合赋值运算符的基础。 也就是说,operator +是用+=来实现的, -=等来实现

根据我们的经验规则, +和它的同伴应该是非成员,而他们的复合赋值对应( +=等),改变他们的左边的论点,应该是成员。 以下是+=+的示例代码,其他二进制算术运算符应该以相同的方式实现:

 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=的理由operator=每个副本的参数一致。

位操作符~ & | 应该像算术运算符一样执行^ << >> 。 但是,除了输出和input重载<<>>之外,重载这些重载的合理用例很less。

再次,从中得出的教训是a += b通常比a + b更有效率,如果可能的话应该是优选的。

arrays下标

数组下标运算符是必须作为类成员实现的二元运算符。 它用于容器types,允许通过键访问数据元素。 提供这些的规范forms是这样的:

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

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

如果已知value_type引用内置types,则运算符的const变体应该返回一个副本,而不是一个const引用。

指针types的运算符

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

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

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

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

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


继续转换操作员

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

当涉及到C ++中的运算符重载时, 应遵循三条基本规则 。 与所有这些规则一样,确实有例外。 有时候人们偏离了他们,结果并不坏,但是这种积极的偏差却很less。 至less,我所看到的100个这样的偏差中有99个是不合理的。 但是,千分之999也是如此。所以你最好遵守以下规则。

  1. 每当操作员的意思不明确无误时,不应超载。 相反,提供一个精心挑选名称的function。
    基本上,超载运营商的首要原则就是: 不要这样做 。 这可能看起来很奇怪,因为有很多关于操作符重载的知识,所以很多文章,书籍章节和其他文本都涉及到这些。 但是,尽pipe这个看似明显的证据, 只有less数情况下运营商超载是适当的 。 原因在于,实际上很难理解运算符应用背后的语义,除非在应用领域中运算符的使用是公知的和无可争议的。 与stream行的观点相反,这种情况几乎不存在。

  2. 始终坚持运营商众所周知的语义。
    C ++对重载运算符的语义没有限制。 您的编译器会高兴地接受实现了二元运算符的代码,从其右操作数中减去。 但是,这样一个运算符的用户决不会怀疑expression式a + bb减去b 。 当然,这假设应用程序领域中操作符的语义是无可争议的。

  3. 始终提供一整套相关的操作。
    运营商是相互关联的,也是相互关联的 。 如果你的types支持a + b ,用户也希望能够调用a += b 。 如果它支持前缀增量++a ,他们将会期望a++能工作。 如果他们可以检查a < b ,他们肯定会期望也能够检查a > b 。 如果他们可以复制构build你的types,他们希望分配工作。


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

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

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

并不是所有的操作符都可以用C ++重载。 不能超载的运营商有: . :: sizeof typeid .*和C ++中唯一的三元运算符, ?:

在C ++中可以重载的操作符包括:

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

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

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

作为非成员函数实现的操作符有时是操作数types的朋友。

1 术语“用户定义”可能会有些误导。 C ++区分了内置types和用户定义types。 前者属于例如int,char和double; 到后者属于所有结构,类,联合和枚举types,包括来自标准库的types,即使它们不是由用户定义的。

2 本FAQ 的后面部分将介绍这一点。

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

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


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

会员与非会员之间的决定

二元运算符= (赋值), [] (数组预订), -> (成员访问)以及n-ary () (函数调用)运算符必须始终作为成员函数来实现,因为语言要求他们去。

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

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

  1. 如果它是一个一元运算符 ,则将其作为成员函数来实现。
  2. 如果一个二元运算符平等地对待这两个操作数 (使它们保持不变),则将该运算符作为非成员函数来实现。
  3. 如果一个二元运算符没有同时处理它的两个操作数(通常它会改变它的左操作数),如果它必须访问操作数的私有部分,使它成为左操作数types的成员函数可能是有用的。

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

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

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

(但是, 如果你犯了一个例外,不要忘记操作数的const问题,对于成员函数,这个操作数就成为隐含的this参数。如果操作符作为非成员函数将其最左边的参数作为一个const引用,与成员函数相同的运算符需要在最后有一个const来使*this成为一个const引用。)


继续到常用运算符超载 。

转换运营商(也称为用户定义的转化)

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

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

隐式转换运算符允许编译器将用户定义types的值隐式转换(如intlong之间的转换)为其他types。

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

 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 ++程序员有时会感到惊讶,因为编译器select了一个他们没有怀疑的超载。 这些问题可以通过显式的转换操作来缓解。

显式转换运算符(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:note:void f(my_string&)
 prog.cpp:11:10:注意:参数1从“my_string”到“my_string&
 prog.cpp:12:10:note:void f(const char *)
 prog.cpp:12:10:注意:从'my_string'到'const char *'的参数1没有已知的转换

为了调用显式的转换运算符,你必须使用static_cast ,一个C风格的转换或者一个构造器风格cast(即T(value) )。

但是,有一个例外:允许编译器隐式转换为bool 。 此外,编译器在转换为bool之后不允许执行另一个隐式转换(编译器允许一次执行2个隐式转换,但在max处只有1个用户定义的转换)。

由于编译器不会投射“过去” bool ,显式转换运算符现在不需要安全布尔成语 。 例如,C ++ 11之前的智能指针使用安全Bool惯用法来防止转换为整型。 在C ++ 11中,智能指针使用显式操作符,因为编译器在将types显式转换为布尔型之后,不允许隐式转换为整型。

继续重载newdelete

重载newdelete

注意:这只涉及重载newdelete语法 ,而不涉及这样的重载操作符的实现 我认为,重载newdelete的语义值得自己的FAQ ,在运算符重载的话题,我永远不能做正义。

基本

在C ++中,当你编写一个像new T(arg)这样的新expression式时,当这个expression式被计算时,会发生两件事:第一个operator new被调用来获得原始内存,然后T的相应构造函数被调用来把这个原始内存变成有效的对象。 同样,当你删除一个对象时,首先调用它的析构函数,然后内存返回给operator delete
C ++允许您调整这两个操作:内存pipe理和在分配的内存中构build/销毁对象。 后者是通过编写一个类的构造函数和析构函数来完成的。 微调内存pipe理是通过编写你自己的operator newoperator delete

运算符重载的基本规则的第一个 – 不要这样做 – 特别适用于重载newdelete 。 几乎导致这些运算符过载的唯一原因是性能问题内存限制 ,在许多情况下,其他操作(如对所用algorithm的更改)将提供比试图调整内存pipe理更高的成本/增益比率

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

前两个分配/取消分配一个对象的内存,后两个分配一个对象的数组。 如果您提供自己的版本,它们将不会超载,但是replace标准库中的版本。
如果你重载operator new ,你应该总是重载匹配的operator delete ,即使你不打算调用它。 原因是,如果一个构造函数在评估一个新expression式的时候抛出,那么运行时系统将把内存返回给与被调用的operator new匹配的operator new operator delete ,以分配内存来创build对象。不提供匹配的operator delete ,默认的一个被调用,这几乎总是错误的。
如果你重载newdelete ,你应该考虑重载数组variables。

安置new

C ++允许新的和删除操作符采取额外的参数。
所谓的placement new允许您在传递给某个地址的某个地址创build一个对象:

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

标准库带有新的和删除操作符的适当的重载:

 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的示例代码中,除非X的构造函数抛出exception,否则不会调用operator delete

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

特定于类的新build和删除

大多数情况下,您会希望对内存pipe理进行微调,因为测量显示特定类或相关类组的实例经常被创build和销毁,并且运行时系统的默认内存pipe理被调整一般的performance,在这个特定的情况下低效地处理。 为了改善这一点,你可以重载新的和删除特定的类:

 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_class对象, std::size_t参数将始终为sizeof(my_class) 。 However, these operators are also called for dynamically allocated objects of derived classes , in which case it might be greater than that.

Global new and delete

To overload the global new and delete, simply replace the pre-defined operators of the standard library with our own. However, this rarely ever needs to be done.

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

Let's say you have:

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

Given that, you cannot use:

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

Since operator<< is overloaded as a member function of Foo , the LHS of the operator must be a Foo object. Which means, you will be required to use:

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

which is very non-intuitive.

If you define it as a non-member function,

 struct Foo { int a; double b; }; std::ostream& operator<<(std::ostream& out, Foo const& f) { return out << fa << " " << fb; } 

You will be able to use:

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

which is very intuitive.