注:这些答案是按照_特定的顺序给出的,但由于许多用户是按照投票而不是给出的时间来对答案进行排序的,因此这里是按照最合理的顺序列出的答案的_索引。
(Note: This is meant to be an entry to Stack Overflow's C++ FAQ.如果你想批判以这种形式提供FAQ的想法,那么开始这一切的meta上的帖子将是做这个的地方。对这个问题的回答会在C++聊天室中被监控,FAQ的想法最初就是从这里开始的,所以你的回答很可能会被那些提出这个想法的人看到。)
大部分重载操作符的工作都是模板代码。这并不奇怪,因为操作符只是语法上的糖,它们的实际工作可以由普通函数来完成(而且经常被转发给普通函数)。但重要的是,你要把这些模板代码写对。如果你失败了,要么你的操作符的代码不能编译,要么你的用户的代码不能编译,要么你的用户的代码会表现得很惊讶。
关于赋值,有很多东西要讲。然而,大部分内容已经在GMan著名的Copy-And-Swap FAQ中说过了,所以我在这里跳过大部分内容,只列出完美的赋值运算符供参考。
X& X::operator=(X rhs)
{
swap(rhs);
return *this;
}
位移操作符<<
和`>>虽然仍然用于硬件接口,用于它们从C语言继承的位操纵功能,但在大多数应用中,作为重载的流输入和输出操作符,已经变得更加普遍。 关于作为位操纵运算符的重载指导,请参见下面的二进制算术运算符部分。 对于在你的对象与iostreams一起使用时实现你自己的自定义格式和解析逻辑,继续。
流操作符是最常见的重载操作符之一,是二进制的infix操作符,语法上没有规定它们应该是成员还是非成员的限制。
由于它们改变了左边的参数(它们改变了流的状态),根据经验法则,它们应该作为其左边操作数的类型的成员来实现。然而,它们的左边操作数是来自标准库的流,虽然标准库定义的大多数流输出和输入操作数确实被定义为流类的成员,但当你为自己的类型实现输出和输入操作时,你不能改变标准库的流类型。这就是为什么你需要为你自己的类型实现这些操作符作为非成员函数。
这两者的典型形式是这样的。
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>>
时,只有当读取本身成功,但结果与预期不符时,才需要手动设置流的状态。
函数调用操作符,用于创建函数对象,也被称为functors,必须被定义为成员函数,所以它总是具有成员函数的隐含的this
参数。除此之外,它可以被重载以接受任何数量的额外参数,包括零。
下面是一个语法的例子。
class foo {
public:
// Overloaded call operator
int operator()(const std::string& y) {
// ...
}
};
使用方法。
foo f;
int a = f("hello");
在整个C++标准库中,函数对象总是被复制的。因此,你自己的函数对象应该是便宜的复制。如果一个函数对象绝对需要使用复制成本较高的数据,最好将该数据存储在其他地方,并让函数对象引用它。
根据经验法则,二进制infix比较运算符应该作为非成员函数实现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')</sub>。 <sup>2</sup> <sub>应该注意的是,内置版本的
||和
&&`使用快捷语义。而用户定义的(因为它们是方法调用的语法糖)不使用快捷语义。用户会期望这些操作符具有快捷语义,他们的代码可能依赖于此,因此强烈建议不要定义它们。
单元递增和递减运算符有前缀和后缀之分。为了区分这两种运算符,后缀运算符需要一个额外的假int参数。如果你重载增量或减量,一定要同时实现前缀和后缀版本。 下面是increment的典型实现,decrement也遵循同样的规则。
class X {
X& operator++()
{
// do actual increment
return *this;
}
X operator++(int)
{
X tmp(*this);
operator++();
return tmp;
}
};
请注意,后缀的变体是以前缀为单位实现的。还要注意的是,后缀会做一个额外的拷贝。2
重载单项的减号和加号并不常见,最好避免。如果需要,它们可能应该作为成员函数被重载。
2 还要注意的是,后缀变体做了更多的工作,因此使用效率比前缀变体低。这是一个很好的理由,一般来说,前缀增量比后缀增量更适合。虽然编译器通常可以优化掉内置类型的后缀增量的额外工作,但对于用户定义的类型(可能是像列表迭代器这样看起来很无辜的东西),他们可能无法做到这一点。一旦你习惯了做i++
,当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+
返回其结果的副本。当然,返回一个引用通常比返回一个副本更有效率,但在operator+
的情况下,没有办法绕过复制。当你写a + b
时,你希望结果是一个新值,这就是为什么operator+
必须返回一个新值。
还需要注意的是,operator+
通过复制而不是通过常量引用来获取其左边的操作数_。其原因与operator=
通过拷贝获取其参数的原因相同。
位操作符~``&``|`````^```<<``>``应该以与算术操作符相同的方式实现。然而,(除了为输出和输入重载
<<和
>>),很少有合理的用例来重载这些。
3 同样,从这里得到的教训是,一般来说,a += b'比
a + b'更有效率,如果可能的话,应该优先考虑。
数组下标运算符是一个二进制运算符,必须作为一个类成员来实现。它用于允许通过一个键来访问其数据元素的容器类类型。 提供这些的典型形式是这样的。
class X {
value_type& operator[](index_type idx);
const value_type& operator[](index_type idx) const;
// ...
};
除非你不希望你的类的用户能够改变由operator[]
返回的数据元素(在这种情况下,你可以省略非const的变体),否则你应该始终提供operator的两个变体。
如果已知value_type指的是一个内置类型,那么操作符的常量变体最好返回一个副本,而不是常量引用。
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
)类型,则会递归调用另一个operator->()
,直到一个operator->()
返回一个非class类型的值。
单元操作符的地址不应该被重载。
关于operator->*()
见[本题](https://stackoverflow.com/q/8777845/140719)。它很少被使用,因此也很少被重载。事实上,即使是迭代器也不会重载它。继续浏览转换操作符
当涉及到C++中的操作符重载时,有三条基本规则你应该遵循。和所有这些规则一样,确实有例外。有时人们偏离了这些规则,其结果并不是坏的代码,但这种积极的偏离是很少的。至少,在我所看到的100个这样的偏离中,有99个是没有理由的。然而,这也可能是1000个中的999个。所以你最好坚持以下规则。
1.1.当一个操作符的含义不明显且无争议时,不应该被重载。 基本上,重载运算符的第一条也是最重要的一条规则,在其核心部分,说的是。不要这样做_。这可能看起来很奇怪,因为有很多关于操作符重载的知识,所以有很多文章、书中的章节和其他文本都是关于这些的。但是,尽管有这些看似明显的证据,只有令人惊讶的少数情况下,运算符重载是合适的。原因是,实际上很难理解运算符应用背后的语义,除非该运算符在应用领域的使用是众所周知和无可争议的。与流行的看法相反,这种情况几乎不存在。
1.1.总是坚持使用运算符的知名语义。
C++对重载运算符的语义没有任何限制。你的编译器会很高兴地接受实现二进制+
运算符的代码,以从其右操作数中减去。然而,这样的运算符的用户永远不会怀疑a + b
的表达式是从b
中减去a
。当然,这是以该运算符在应用领域中的语义是无可争议的为前提的。
1.1. ___总是提供一组相关操作中的所有操作。
操作符是相互关联的_也是与其他操作相关的。如果你的类型支持 "a + b",用户会希望也能调用 "a += b"。如果它支持前缀增量++a
,他们会期望a++
也能工作。如果他们能够检查a < b
,他们肯定希望也能够检查a > b
。如果他们能够复制构建你的类型,他们也希望赋值能够工作。
继续阅读成员和非成员之间的决定。
你不能改变C++中内置类型的操作符的含义,操作符只能为用户定义的类型重载1。也就是说,至少有一个操作数必须是用户定义的类型。与其他重载函数一样,操作符只能为某一组参数重载一次。
在C++中,并非所有的操作符都可以被重载。不能被重载的操作符包括.``:``sizeof```typeid``.*``和C++中唯一的三元运算符
?
在C++中可以被重载的运算符有
+
- *
/`%
和+= ``-=
*= /=
%(所有二进制下缀);
+ `-
(单数前缀);++
- `(单数前缀和后缀)。&``|``^````<<``>
和&=```|=```^=```<<```>``(所有二进制下标);
~`(单数前缀(所有二进制下标);
(单数前缀)。new``new[]``delete``delete[]
。=```[]```->```,``(所有二进制英缀);
*``(所有单进制英缀) `()
(函数调用,n-ary英缀)然而,你可以重载所有这些的事实并不意味着你应该这样做。参见操作符重载的基本规则。
在C++中,运算符是以具有特殊名称的函数形式进行重载的。与其他函数一样,重载的运算符一般可以作为其左手操作数类型的成员函数或作为非成员函数来实现。2 一个单数运算符@
3,应用于一个对象x,被调用为operator@(x)
或x.operator@()
。应用于对象x
和y
的二进制运算符@
,可以作为operator@(x,y)
或x.operator@(y)
调用。
作为非成员函数实现的操作符有时是其操作数类型的朋友。
1 "用户定义 "这个术语可能有点误导。C++对内置类型和用户定义类型进行了区分。例如,前者属于int、char和double;后者属于所有的struct、class、union和enum类型,包括那些来自标准库的类型,尽管它们本身不是由用户定义的。
2 这一点在本FAQ的后一部分中有所涉及。
3 @
在C++中不是一个有效的操作符,这就是为什么我用它作为一个占位符。
4 C++中唯一的三元运算符不能被重载,唯一的n-ary运算符必须始终作为成员函数来实现。
继续阅读【C++中操作符重载的三个基本规则】(https://stackoverflow.com/questions/4421706/operator-overloading-in-c/4421708#4421708)。