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

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

  • C ++中运算符重载的一般语法
  • C ++中运算符重载的三个基本规则
  • 会员与非会员之间的决定
  • 常见的操作符超载
  • 作业操作员
  • 输入和输出操作符
  • 函数调用操作符
  • 比较运算符
  • 算术运算符
  • 阵列下标
  • 指针类型的操作符
  • 转换运算符
  • 重载新的和删除
  • (注意:这是一个Stack Overflow的C ++常见问题解答的入口,如果你想批评在这个表单中提供常见问题的想法,那么开始所有这些的meta上的贴子将成为这样做的地方。那个问题在C ++聊天室中进行监控,常见问题的想法首先出现在C ++聊天室中,所以你的答案很可能会被那些提出这个想法的人阅读。)


    常见的操作符超载

    大多数重载操作员的工作都是锅炉代码。 这并不奇怪,因为操作员只是语法上的糖,他们的实际工作可以通过简单的功能来完成(并且经常被转发)。 但是重要的是你能够正确地得到这个样板代码。 如果你失败了,你的运营商的代码不会编译,或者你的用户的代码不能编译,或者你的用户的代码会出乎意料地表现出来。

    作业操作员

    关于任务有很多要说的。 然而,其中大部分内容已经在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;
    }
    

    当实现operator>> ,手动设置流的状态只有在读取成功时才有必要,但结果不是预期的结果。

    函数调用操作符

    函数调用操作符,用于创建函数对象,也被称为仿函数,必须被定义为一个部件的功能,所以它总是有隐含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;
      }
    };
    

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

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

    2还要注意,后缀变体可以做更多的工作,因此使用效率比前缀变体的效率低。 这是一个很好的理由,通常更喜欢前缀增量超过后缀增量。 虽然编译器通常可以优化内置类型的后缀增量的额外工作,但它们可能无法对用户定义的类型(这可能看起来像列表迭代器那样天真)。 一旦你习惯了i++ ,当i不是内置类型时(除了更改类型时不得不更改代码),记住要做++i变得非常困难,所以最好是养成总是使用前缀增量的习惯,除非明确需要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=每个副本采用其参数的原因相同。

    位操作运算符~ & | 应该像算术运算符一样实现^ << >> 。 但是,除了输出和输入重载<<>>之外,很少有合理的用例将它们重载。

    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_typeclass (或structunion )类型,则会递归调用另一个operator->() ,直到operator->()返回非类类型的值。

    一元地址运算符不应超载。

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


    继续转换操作员


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

    当涉及到C ++中的运算符重载时, 应遵循三条基本规则 。 与所有这些规则一样,确实有例外。 有时人们偏离了他们,结果并不是错误的代码,但是这种积极的偏差却很少。 至少在我看到的100个这样的偏差中,有99个是不合理的。 但是,它也可能是1000个中的999个。所以你最好遵守以下规则。

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

  • 始终坚持运营商众所周知的语义。
    C ++对重载运算符的语义没有限制。 你的编译器会很乐意接受,实现了二进制代码+操作距离其右操作数减去。 然而,这样的操作者的用户决不会怀疑表达a + b减去ab 。 当然,这假设应用程序域中操作符的语义是无可争议的。

  • 始终提供一整套相关的操作。
    运营商彼此之间以及与其他运营相关。 如果你的类型支持a + b ,用户也期望能够调用a += b 。 如果它支持前缀增量++a ,他们也会期望a++能工作。 如果他们可以检查a < b ,他们肯定会期望也能够检查a > b 。 如果他们可以复制构建您的类型,他们希望分配工作。


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


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

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

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

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

  • 算术运算符: + - * / %+= -= *= /= %= (所有二进制中缀); + - (一元前缀); ++ -- (一元前缀和后缀)
  • 位操作: & | ^ << >>&= |= ^= <<= >>= (所有二进制中缀); ~ (一元前缀)
  • 布尔代数: == != < > <= >= || && (全部二进制中缀); ! (一元前缀)
  • 内存管理: new new[] delete delete[]
  • 隐式转换运算符
  • miscellany: = [] -> ->* , (all binary infix); * & (所有一元前缀) () (函数调用,n元中缀)
  • 然而,你可以重载所有这些的事实并不意味着你应该这样做。 请参阅运算符重载的基本规则。

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

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

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

    2本FAQ的后面部分将对此进行介绍。

    3 @不是C ++中的有效运算符,因此我将它用作占位符。

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


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

    链接地址: http://www.djcxy.com/p/12683.html

    上一篇: What are the basic rules and idioms for operator overloading?

    下一篇: Why would one replace default new and delete operators?