【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>
注意:答案是按特定顺序给出的,但是由于许多用户是根据投票而不是给出时间来对答案进行排序的,因此以下是最有意义的顺序索引 :
- C ++中运算符重载的通用语法
- C ++中运算符重载的三个基本规则
- 会员与非会员之间的决定
- 普通运算符重载
- 赋值运算符
- 输入和输出运算符
- 函数调用运算符
- 比较运算符
- 算术运算符
- 数组下标
- 指针类型的运算符
- 转换运算符
- 重载新的和删除的
(注意:这本来是Stack Overflow的C ++ FAQ的条目。如果您想批评以这种形式提供FAQ的想法,那么在所有这些都开始的meta上的张贴将是这样做的地方。该问题在C ++聊天室中进行监控,该问题最初是从FAQ想法开始的,所以提出这个想法的人很可能会读懂您的答案。)
#1楼
转换运算符(也称为用户定义的转换)
在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_;
};
注意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
运算符,必须使用static_cast
,C样式static_cast
或构造函数样式static_cast
(即T(value)
)。
但是,有一个例外:允许编译器隐式转换为bool
。 此外,编译器在转换为bool
之后,不允许进行其他隐式转换(编译器一次只能进行2次隐式转换,但最多只能进行1个用户定义的转换)。
因为编译器不会bool
转换过去,所以显式转换运算符现在无需使用Safe Bool习惯用法 。 例如,C ++ 11之前的智能指针使用Safe Bool习惯用法来防止转换为整数类型。 在C ++ 11中,智能指针使用显式运算符代替,因为在将类型显式转换为bool之后,不允许编译器隐式转换为整数。
继续重载new
和delete
。
#2楼
为什么operator<<
函数无法将对象流式传输到std::cout
或文件中,成为成员函数?
假设您有:
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
的成员函数被重载,因此该operator的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;
这是非常直观的。
#3楼
C ++中运算符重载的三个基本规则
对于C ++中的运算符重载, 应遵循三个基本规则 。 与所有此类规则一样,确实存在例外。 有时人们偏离了他们,结果不是不好的代码,但是这种积极的偏差很少而且相差甚远。 至少在我看到的100个此类偏差中,有99个是不合理的。 但是,也有可能是1000分之999。因此,您最好遵循以下规则。
只要运算符的含义不明确且无可争辩,就不应重载。 而是提供一个功能齐全的名称。
基本上,重载运算符的首要原则就是说: 不要这样做 。 这似乎很奇怪,因为关于运算符重载有很多已知的知识,因此许多文章,书籍章节和其他文本都涉及到这一切。 但是,尽管有这些看似显而易见的证据, 但在极少数情况下,才适合使用运算符重载 。 原因是实际上很难理解运算符应用程序背后的语义,除非在应用程序域中对运算符的使用是众所周知且无可争议的。 与普遍的看法相反,几乎没有这种情况。始终遵守操作员的众所周知的语义。
C ++对重载运算符的语义没有任何限制。 您的编译器将很乐意接受实现二进制+
运算符的代码,以从其右操作数中减去。 然而,这样的操作者的用户决不会怀疑表达a + b
减去a
从b
。 当然,这假定在应用程序域中运算符的语义是无可争议的。始终提供一组相关操作中的所有内容。
运算符彼此相关,并且与其他操作相关。 如果您的类型支持a + b
,那么用户也希望能够调用a += b
。 如果它支持前缀增量++a
,他们将期望a++
能正常工作。 如果他们能够检查a < b
,那么他们肯定会期望也能够检查a > b
。 如果他们可以复制构造您的类型,他们希望分配也能正常工作。
继续进行成员与非成员之间的决定 。
#4楼
C ++中运算符重载的通用语法
您不能更改C ++中内置类型的运算符的含义,只能对用户定义的类型1重载运算符。 即,至少一个操作数必须是用户定义的类型。 与其他重载函数一样,运算符只能对一组特定参数重载一次。
并非所有运算符都可以在C ++中重载。 不能重载的运算符包括: .
::
sizeof
typeid
.*
和C ++中唯一的三元运算符?:
在C ++中可以重载的运算符包括:
- 算术运算符:
+
-
*
/
%
和+=
-=
*=
/=
%=
(所有二进制中缀);+
-
(一元前缀);++
--
(一元前缀和后缀) - 位操作:
&
|
^
<<
>>
和&=
|=
^=
<<=
>>=
(所有二进制中缀);~
(一元前缀) - 布尔代数:
==
!=
<
>
<=
>=
||
&&
(所有二进制中缀);!
(一元前缀) - 内存管理:
new
new[]
delete
delete[]
- 隐式转换运算符
- 其他:
=
[]
->
->*
,
(所有二进制中缀);*
&
(所有一元前缀)()
(函数调用,n元中缀)
但是,您可以重载所有这些事实并不意味着您应该这样做。 请参阅运算符重载的基本规则。
在C ++中,运算符以具有特殊名称的函数的形式被重载。 与其他函数一样,重载运算符通常可以实现为其左操作数类型的成员函数,也可以实现为非成员函数 。 您是自由选择还是必须使用其中之一,取决于多个条件。 2应用于对象x的一元运算符@
3被作为operator@(x)
或x.operator@()
调用。 应用于对象x
和y
的二进制中缀运算符@
称为operator@(x,y)
或x.operator@(y)
。 4
实施为非成员函数的运算符有时是其操作数类型的朋友。
1“用户定义”一词可能会引起误解。C ++区分内置类型和用户定义类型。前者属于int,char和double。后者属于所有struct,class,union和enum类型,包括那些来自标准库的类型,即使它们不是由用户定义的。
2此常见问题的后续部分将对此进行介绍。
3@
在C ++中不是有效的运算符,这就是为什么我将其用作占位符。
4C ++中唯一的三元运算符不能重载,并且唯一的n元运算符必须始终实现为成员函数。
#5楼
普通运算符重载
重载操作员中的大部分工作是样板代码。 这也就不足为奇了,由于运算符仅仅是语法糖,它们的实际工作可以由(通常转发给)普通函数来完成。 但是,重要的是要正确编写此样板代码。 如果失败,则操作员的代码将无法编译,或者用户的代码将无法编译,或者用户的代码将表现出惊人的性能。
赋值运算符
关于任务有很多要说的。 但是,大多数内容已经在GMan著名的复制和交换FAQ中进行了介绍 ,因此在此我将跳过大部分内容,仅列出完美的赋值运算符以供参考:
X& X::operator=(X rhs)
{
swap(rhs);
return *this;
}
位移位运算符(用于流I / O)
尽管移位移位运算符<<
和>>
仍用于它们从C继承的位处理函数的硬件接口中,但在大多数应用程序中,它们已作为重载流输入和输出运算符而变得更加普遍。 有关作为位操作运算符的指导超载,请参见下面有关二进制算术运算符的部分。 当对象与iostream一起使用时,要实现自己的自定义格式和解析逻辑,请继续。
在最常见的重载运算符中,流运算符是二进制中缀运算符,其语法对它们应为成员还是非成员不加限制。 由于它们更改了左参数(它们更改了流的状态),因此应根据经验法则将其实现为其左操作数类型的成员。 但是,它们的左操作数是标准库中的流,尽管标准库定义的大多数流输出和输入运算符的确定义为流类的成员,但是当您为自己的类型实现输出和输入操作时,无法更改标准库的流类型。 这就是为什么您需要将自己的类型的这些运算符实现为非成员函数。 两种的规范形式是:
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应该注意的是||
的内置版本和&&
使用快捷方式语义。虽然用户定义的语法(因为它们是方法调用的语法糖),却不使用快捷方式语义。用户将期望这些运算符具有捷径语义,并且它们的代码可能依赖于此,因此,强烈建议不要定义它们。
算术运算符
一元算术运算符
一元递增和递减运算符具有前缀和后缀形式。 为了彼此区分,postfix变体采用了另一个哑int参数。 如果您使增量或减量过载,请确保始终同时实现前缀和后缀版本。 这是递增的规范实现,递减遵循相同的规则:
class X {
X& operator++()
{
// do actual increment
return *this;
}
X operator++(int)
{
X tmp(*this);
operator++();
return tmp;
}
};
请注意,后缀变体是根据前缀实现的。 另请注意,后缀会额外复制。 2
一元负号和加号的重载不是很常见,最好避免。 如果需要,它们可能应该作为成员函数重载。
2还请注意,postfix变体比前缀变体执行更多的工作,因此使用效率较低。这是一个很好的理由,通常优先选择前缀增量而不是后缀增量。尽管编译器通常可以优化内置类型的后缀增量的其他工作,但对于用户定义的类型,它们可能无法做到相同(这可能看起来像列表迭代器一样无辜)。一旦您习惯使用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+
必须返回一个新值的原因。 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[]
返回的数据元素(在这种情况下,您可以忽略non-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;
};
注意,这些也几乎总是需要const版本和非const版本。 对于->
运算符,如果value_type
是class
(或struct
或union
)类型,则将递归调用另一个operator->()
,直到operator->()
返回非类类型的值。
一元地址运算符绝对不能重载。
对于operator->*()
请参阅此问题 。 它很少使用,因此也很少过载。 实际上,即使迭代器也不会使它过载。
继续向转换运算符
来源:oschina
链接:https://my.oschina.net/stackoom/blog/3148909