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

时间:2010-12-12 12:44:56

标签: c++ operators operator-overloading c++-faq

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

<子> (注意:这是Stack Overflow's C++ FAQ的一个条目。如果您想批评在此表单中提供常见问题解答的想法,那么the posting on meta that started all this就是这样做的地方。在C++ chatroom中监控了这个问题,首先是FAQ的想法,所以你的答案很可能会被那些提出想法的人阅读。)

7 个答案:

答案 0 :(得分:968)

答案 1 :(得分:465)

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

当谈到C ++中的运算符重载时,你应该遵循 三个基本规则 。与所有此类规则一样,确实存在例外情况。有时人们偏离了他们,结果并不是糟糕的代码,但这种积极的偏差很少。至少,我所看到的100个这样的偏差中有99个是没有道理的。但是,它可能只有1000中的999。所以你最好坚持以下规则。

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

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

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


  4. 继续The Decision between Member and Non-member

答案 2 :(得分:245)

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

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

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

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

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

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

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

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

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

2 本常见问题解答的a later part涵盖了此内容。

3 operator@(x,y)不是C ++中的有效运算符,这就是我将其用作占位符的原因。

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


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

答案 3 :(得分:228)

会员与非会员之间的决定

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

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

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

  1. 如果是 一元运算符 ,请将其实现为 成员 功能。
  2. 如果二元运算符同时将 两个操作数视为 (它保持不变),请将此运算符实现为 非成员 功能。
  3. 如果二元运算符 对其两个操作数 同等 (通常会更改其左侧)如果它必须访问操作数的私有部分,那么使它成为左操作数类型的 成员 函数可能是有用的。
  4. 当然,与所有经验法则一样,也有例外。如果您有类型

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

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

    (但是,如果你做了一个例外,不要忘记操作数的const - 问题,对于成员函数,它成为隐式this参数如果作为非成员函数的运算符将其最左边的参数作为const引用,则与成员函数相同的运算符最后需要const来生成{{1} } *this引用。)


    继续Common operators to overload

答案 4 :(得分:154)

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

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

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

继续Overloading new and delete

答案 5 :(得分:139)

重载newdelete

注意: 这只涉及重载new 语法 delete,而不是此类重载运算符的 实现 。我认为重载 new and delete deserve their own FAQ 的语义,在运算符重载的主题中我永远不会公正。

基本

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

运算符重载的第一个基本规则 - 不执行 - 尤其适用于重载operator deletenew。导致这些运算符超载的几乎唯一原因是 性能问题 内存约束 ,并且在很多情况下,还有其他操作与所使用的算法的更改一样,将比尝试调整内存管理提供更多 更高的成本/增益比

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

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,则会调用默认值,这几乎总是错误的 如果重载newdelete,则应考虑重载数组变体。

展示位置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(); 

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

您还可以使用其他参数重载newdelete。与放置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_class的对象,std::size_t参数将始终为sizeof(my_class)。但是,这些运算符也被称为 派生类 的动态分配对象,在这种情况下,它可能大于此。

全局新增和删除

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

答案 6 :(得分:37)

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;

非常直观。