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

C中运算符重载的一般语法++C语言中运算符重载的三个基本规则++成员与非成员之间的决定常见操作员过载赋值运算符输入和输出运算符函数调用运算符比较运算符算术运算符阵列订阅指针类类型的运算符转换运算符重载新建和删除

(注意:这是Stack Overflow的C++FAQ的一个条目。如果你想批评以这种形式提供FAQ的想法,那么在meta上发布这一切的帖子就是这样做的地方。该问题的答案在C++聊天室中进行监控,FAQ想法最初就在那里开始,所以你的答案很可能会被提出该想法的人阅读。)


当前回答

重载新建和删除运算符

注意:这只处理重载new和delete的语法,而不处理此类重载运算符的实现。我认为重载new和delete的语义应该有自己的常见问题解答,在运算符重载的主题中,我永远无法做到这一点。

基础

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

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

C++标准库附带一组预定义的新建和删除运算符。最重要的是:

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

前两个为对象分配/解除分配内存,后两个为一个对象数组。如果您提供自己的版本,它们将不会过载,而是替换标准库中的版本。如果重载运算符new,则应始终重载匹配的运算符delete,即使您从未打算调用它。原因是,如果构造函数在计算新表达式时抛出,则运行时系统会将内存返回给运算符delete,该运算符delete与被调用以分配内存以在其中创建对象的运算符new相匹配。如果不提供匹配的delete运算符,则会调用默认的delete,这几乎总是错误的。如果重载new和delete,也应该考虑重载数组变量。

放置新的

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的示例代码中,除非X的构造函数抛出异常,否则永远不会调用运算符delete。

还可以使用其他参数重载new和delete。与放置new的附加参数一样,这些参数也列在关键字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,只需将标准库的预定义运算符替换为我们自己的运算符即可。然而,很少需要这样做。

其他回答

常见操作员过载

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

赋值运算符

关于任务有很多话要说。然而,GMan著名的“复制和交换常见问题解答”中已经提到了其中的大部分内容,因此我将跳过这里的大部分,只列出完美的赋值运算符以供参考:

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

Bitshift运算符(用于流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;
}

在实现运算符>>时,只有当读取本身成功时,才需要手动设置流的状态,但结果不是预期的。

函数调用运算符

用于创建函数对象(也称为函子)的函数调用运算符必须定义为成员函数,因此它始终具有成员函数的隐式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)始终只希望运算符<出现。但是,您类型的用户也会期望所有其他运算符都存在,因此如果您定义了运算符<,请确保遵循运算符重载的第三个基本规则,并定义所有其他布尔比较运算符。实现它们的规范方法是:

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)也需要是常量。因此,作为成员函数实现的比较运算符必须具有以下签名:

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

(注意末尾的常量。)

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

算术运算符

一元算术运算符

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

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

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

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

2还请注意,后缀变体比前缀变体工作更多,因此使用效率更低。这是一个很好的理由,通常更喜欢前缀增量而不是后缀增量。虽然编译器通常可以优化内置类型的后缀增量这一额外工作,但对于用户定义的类型,编译器可能无法做到这一点(这可能像列表迭代器一样看起来很无辜)。一旦你习惯了使用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+返回其结果的副本。当然,返回引用通常比返回副本更有效,但在运算符+的情况下,没有办法避免复制。当您编写a+b时,您希望结果是一个新值,这就是为什么运算符+必须返回一个新的值。3还要注意,运算符+通过复制而不是常量引用获取其左操作数。这样做的原因与operator=为每个副本获取其参数的原因相同。

位操作运算符~&|^<>>的实现方式应与算术运算符相同。然而,(除了输出和输入的重载<<和>>之外)很少有合理的用例来重载这些。

3同样,要从中吸取的教训是,一般来说,a+=b比a+b更有效,如果可能的话,应该首选。

阵列订阅

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

class X {
        value_type& operator[](index_type idx);
  const value_type& operator[](index_type idx) 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;
};

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

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

对于运算符->*(),请参见此问题。它很少使用,因此很少过载。事实上,即使迭代器也不会重载它。


继续转换运算符

重载新建和删除运算符

注意:这只处理重载new和delete的语法,而不处理此类重载运算符的实现。我认为重载new和delete的语义应该有自己的常见问题解答,在运算符重载的主题中,我永远无法做到这一点。

基础

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

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

C++标准库附带一组预定义的新建和删除运算符。最重要的是:

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

前两个为对象分配/解除分配内存,后两个为一个对象数组。如果您提供自己的版本,它们将不会过载,而是替换标准库中的版本。如果重载运算符new,则应始终重载匹配的运算符delete,即使您从未打算调用它。原因是,如果构造函数在计算新表达式时抛出,则运行时系统会将内存返回给运算符delete,该运算符delete与被调用以分配内存以在其中创建对象的运算符new相匹配。如果不提供匹配的delete运算符,则会调用默认的delete,这几乎总是错误的。如果重载new和delete,也应该考虑重载数组变量。

放置新的

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的示例代码中,除非X的构造函数抛出异常,否则永远不会调用运算符delete。

还可以使用其他参数重载new和delete。与放置new的附加参数一样,这些参数也列在关键字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,只需将标准库的预定义运算符替换为我们自己的运算符即可。然而,很少需要这样做。

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

在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_;
};

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

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次用户定义的转化)。

因为编译器不会抛出“过去”布尔值,所以显式转换运算符现在不再需要安全布尔值习惯用法。例如,C++11之前的智能指针使用安全布尔习惯用法来防止转换为整型。在C++11中,智能指针改用显式运算符,因为编译器在将类型显式转换为bool后,不允许隐式转换为整型。

继续重载新建和删除。

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

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

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

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

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

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

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

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

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

2这将在本常见问题解答的后面部分介绍。

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

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


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

成员与非成员之间的决定

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

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

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

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

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

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

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

(但是,如果发生异常,请不要忘记操作数的常量问题,对于成员函数,该操作数将成为隐式this参数。如果作为非成员函数的运算符将其最左边的参数作为常量引用,则作为成员函数的同一运算符需要在末尾有一个常量,以使*this成为常量引用。)


继续到“常用运算符”以重载。