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

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

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

(注:这意味着是一个进入堆栈溢出的C++ FAQ。如果你想在这个表单中提供一个常见问题的想法,那么在Meta上发布所有这些都是要做的。在C++聊天室中,这个问题的答案被监控,首先是FAQ思想,所以你的答案很可能是让想出这个主意的人读一读。)

常见的运算符重载

重载操作符的大部分工作都是锅炉板代码。这也就不足为奇了,因为操作符只是语法上的糖,它们的实际工作可以由完成(并且经常被转发给)普通函数。但重要的是你要正确使用这个锅炉板代码。如果你失败了,要么你的操作员代码无法编译,要么你的用户代码无法编译,要么你的用户代码行为异常

赋值运算符

关于分配有很多要说的。但是,GMan著名的复制和交换FAQ中已经提到了大部分内容,所以我将在这里跳过大部分内容,只列出完美的分配运算符供参考:

X&X::operator=(X rhs)
{
掉期(rhs);
归还*这个;
}

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

位移位运算符<>虽然仍然用于从C继承的位操作函数的硬件接口,但在大多数应用程序中,作为重载流输入和输出运算符已变得越来越普遍。有关作为位操作运算符的指导重载,请参阅以下部分当对象与iostreams一起使用时,要实现您自己的自定义格式和解析逻辑,请继续

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

std::ostream&operator<<(std::ostream&os,const T&obj)
{
//将obj写入流
返回操作系统;
}
std::istream&operator>;(std::istream&is,T&obj)
{
//从流中读取obj
if(/*在流*/中找不到有效的T对象)
is.setstate(std::ios::failbit);
回报是;
}

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

函数调用运算符

用于创建函数对象(也称为函子)的函数调用运算符必须定义为成员函数,因此它始终具有成员函数的隐式this参数。除此之外,它可以重载以接受任意数量的附加参数,包括零

下面是一个语法示例:

类foo{
公众:
//过载呼叫接线员
int运算符()(常量std::string&y){
// ...
}
};

用法:

foo-f;
INTA=f(“你好”);

整个C++标准库中,函数对象总是被复制。因此,您自己的函数对象应该是廉价的复制。如果函数对象需要使用复制的代价很高的数据,最好把数据存储在别处,并用函数对象引用它。

比较运算符

根据经验法则,二进制中缀比较运算符应实现为非成员函数1。一元前缀否定应(根据相同规则)实现为成员函数(但通常不建议重载它)

标准库的算法(例如std::sort())和类型(例如std::map)将始终只希望运算符<出现。但是,您类型的用户也希望所有其他运算符出现,因此如果定义运算符<,请确保遵循运算符重载的第三个基本规则,并定义所有其他布尔比较运算符。规范实现这些目标的方法如下:

内联布尔运算符==(常量X&lhs,常量X&rhs){/*进行实际比较*/}
内联布尔运算符!=(常数X&lhs,常数X&rhs){返回!运算符==(lhs,rhs);}
内联布尔运算符<(常量X&lhs,常量X&rhs){/*执行实际比较*/}
内联布尔运算符>(常量X&lhs,常量X&rhs){返回运算符<(rhs,lhs);}
内联布尔运算符<=(常量X&lhs,常量X&rhs){return!运算符>(lhs,rhs);}
内联布尔运算符>=(常量X&lhs,常量X&rhs){return!运算符<(lhs,rhs);}

这里需要注意的重要一点是,这些运算符中只有两个实际执行任何操作,其他运算符只是将其参数转发给这两个运算符中的任何一个来执行实际操作

重载剩余的二进制布尔运算符(|和<amp;)的语法遵循比较运算符的规则。但是,您不太可能为这些2找到合理的用例

1与所有经验法则一样,有时也可能有理由打破这一规则。如果是这样,请不要忘记,二进制比较运算符的左操作数(对于成员函数将是*this)也需要是常量。因此,作为成员函数实现的比较运算符将具有e要有此签名:

bool运算符&lt;(const X&amp;rhs)const{/*与*this*/}进行实际比较

(注意结尾处的常量)。

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

算术运算符

一元算术运算符

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

X类{
X&amp;运算符++()
{
//做实际增量
归还*这个;
}
X运算符++(int)
{
X tmp(*本);
运算符++();
返回tmp;
}
};

请注意,后缀变量是根据前缀实现的。还请注意,后缀有一个额外的副本。2

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

2还请注意,后缀变量比前缀变量做更多的工作,因此使用效率较低。这是一个很好的理由,通常首选前缀增量而不是后缀增量。虽然编译器通常可以优化内置类型的后缀增量的额外工作,但他们可能无法做到这一点对于用户定义的类型也是一样(可能看起来像列表迭代器一样天真)。一旦你习惯了i++,当i不是内置类型时,你就很难记得去做++i(另外,在更改类型时你必须更改代码),因此最好养成始终使用前缀增量的习惯,除非明确需要后缀。

二进制算术运算符

对于二进制算术运算符,不要忘记遵守运算符重载的第三个基本规则:如果您提供+,也提供+=,如果您提供-,请不要忽略-=,等等。据说Andrew Koenig是第一个观察到复合赋值运算符可以用作也就是说,运算符+根据+=实现,-根据-=等实现

根据我们的经验法则,+及其同伴应该是非成员,而他们的复合赋值对手(+=等)在更改其左参数时应该是membe

发表评论