注意:答案是以特定顺序给出的,但由于许多用户根据投票而不是给出的时间对答案进行排序,因此这里的 索引答案 按照他们最有意义的顺序:
<子> (注意:这是Stack Overflow's C++ FAQ的一个条目。如果您想批评在此表单中提供常见问题解答的想法,那么the posting on meta that started all this就是这样做的地方。在C++ chatroom中监控了这个问题,首先是FAQ的想法,所以你的答案很可能会被那些提出想法的人阅读。)
答案 0 :(得分:968)
答案 1 :(得分:465)
当谈到C ++中的运算符重载时,你应该遵循 三个基本规则 。与所有此类规则一样,确实存在例外情况。有时人们偏离了他们,结果并不是糟糕的代码,但这种积极的偏差很少。至少,我所看到的100个这样的偏差中有99个是没有道理的。但是,它可能只有1000中的999。所以你最好坚持以下规则。
每当操作员的意义不明显且无可争议时,就不应该重载。 相反,提供一个井的功能 - 选择名称。
基本上,重载运营商的第一个也是最重要的规则是:不要这样做。这可能看起来很奇怪,因为有很多关于运算符重载的知识,因此很多文章,书籍章节和其他文本都涉及到这一切。但是,尽管有这些看似明显的证据,只有极少数情况下运算符重载是合适的。原因是实际上很难理解运算符应用背后的语义,除非在应用程序域中使用运算符是众所周知且无可争议的。与普遍看法相反,情况并非如此。
始终坚持运营商众所周知的语义。
C ++对重载运算符的语义没有限制。您的编译器将很乐意接受实现二进制+
运算符的代码,以从其右操作数中减去。但是,此类运算符的用户绝不会怀疑表达式a + b
从a
中减去b
。当然,这假设应用程序域中的运算符的语义是无可争议的。
始终提供一系列相关操作。
运营商彼此相关和其他运营。如果您的类型支持a + b
,则用户也可以拨打a += b
。如果它支持前缀增量++a
,那么它们也会期望a++
也能正常工作。如果他们可以检查是否a < b
,他们肯定也希望能够检查是否a > b
。如果他们可以复制构造您的类型,他们希望分配也可以工作。
答案 2 :(得分:245)
您无法在C ++中更改内置类型的运算符的含义,只能为用户定义的类型 1 重载运算符。也就是说,至少一个操作数必须是用户定义的类型。与其他重载函数一样,运算符只能为一组参数重载一次。
并非所有运算符都可以在C ++中重载。无法重载的运算符包括:.
::
sizeof
typeid
.*
和C ++中唯一的三元运算符?:
可以在C ++中重载的运算符包括:
+
-
*
/
%
和+=
-=
*=
{{1 } /=
(所有二进制中缀); %=
+
(一元前缀); -
++
(一元前缀和后缀)--
&
|
^
<<
和>>
&=
|=
{{1 } ^=
(所有二进制中缀); <<=
(一元前缀)>>=
~
==
!=
<
>
<=
>=
(所有二进制中缀) ); ||
(一元前缀)&&
!
new
new[]
delete
delete[]
=
[]
->
(所有二进制中缀); ->*
,
(所有一元前缀)*
(函数调用,n-ary中缀)但是,你可以重载所有这些并不意味着你应该这样做。请参阅运算符重载的基本规则。
在C ++中,运算符以 函数的形式重载,具有特殊名称 。与其他函数一样,重载运算符通常可以实现为左操作数类型的 成员函数 或 非成员函数 即可。您是否可以自由选择或使用其中任何一个取决于几个标准。 2 应用于对象x的一元运算符&
3 被调用作为()
或@
。应用于对象operator@(x)
和x.operator@()
的二进制中缀运算符@
称为x
或y
。 4
作为非成员函数实现的运算符有时是其操作数类型的朋友。
1 术语“用户定义”可能略有误导。 C ++区分内置类型和用户定义类型。前者属于例如int,char和double;后者属于所有struct,class,union和enum类型,包括来自标准库的类型,即使它们不是由用户定义的。
2 本常见问题解答的a later part涵盖了此内容。
3 operator@(x,y)
不是C ++中的有效运算符,这就是我将其用作占位符的原因。
4 C ++中唯一的三元运算符不能重载,唯一的n-ary运算符必须始终作为成员函数实现。
答案 3 :(得分:228)
二元运算符=
(赋值),[]
(数组预订),->
(成员访问),以及n-ary ()
(函数调用) )运算符,必须始终实现为 成员函数 ,因为语言的语法要求它们。
其他运营商可以作为成员或非成员实施。但是,其中一些通常必须作为非成员函数实现,因为它们的左操作数不能被您修改。其中最突出的是输入和输出运算符<<
和>>
,其左操作数是标准库中的流类,您无法更改。
对于您必须选择将其实现为成员函数或非成员函数的所有运算符, 使用以下经验法则 来决定:< / p>
当然,与所有经验法则一样,也有例外。如果您有类型
enum Month {Jan, Feb, ..., Nov, Dec}
并且你想为它重载递增和递减运算符,你不能将它作为成员函数来执行,因为在C ++中,枚举类型不能具有成员函数。所以你必须将它作为一个自由函数重载。嵌套在类模板中的类模板的operator<()
在类定义中作为成员函数内联完成时更容易编写和读取。但这些确实是罕见的例外。
(但是,如果你做了一个例外,不要忘记操作数的const
- 问题,对于成员函数,它成为隐式this
参数如果作为非成员函数的运算符将其最左边的参数作为const
引用,则与成员函数相同的运算符最后需要const
来生成{{1} } *this
引用。)
答案 4 :(得分:154)
在C ++中,您可以创建转换运算符,这些运算符允许编译器在您的类型和其他已定义类型之间进行转换。转换运算符有两种类型,隐式和显式运算符。
隐式转换运算符允许编译器将用户定义类型的值隐式转换(如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()
不是lvalue,因此第一个不匹配:
void f(my_string&);
void f(const char*);
f(my_string());
初学者很容易弄错,甚至经验丰富的C ++程序员有时会感到惊讶,因为编译器选择了他们不怀疑的过载。显式转换运算符可以缓解这些问题。
与隐式转换运算符不同,显式转换运算符在您不期望它们时永远不会启动。以下是一个带有显式转换运算符的简单类:
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之后,不允许编译器隐式转换为整数类型。
答案 5 :(得分:139)
new
和delete
注意: 这只涉及重载new
的 语法 和delete
,而不是此类重载运算符的 实现 。我认为重载 new
and delete
deserve their own FAQ 的语义,在运算符重载的主题中我永远不会公正。
在C ++中,当您编写 新表达式 ,如new T(arg)
时,在评估此表达式时会发生两件事情:首先 {{调用1}} 来获取原始内存,然后调用operator new
的相应构造函数将此原始内存转换为有效对象。同样,当您删除对象时,首先调用其析构函数,然后将内存返回到T
。
C ++允许您调整这两个操作:内存管理以及在分配的内存中构造/销毁对象。后者是通过为类编写构造函数和析构函数来完成的。通过编写自己的operator delete
和operator new
来完成内存管理的微调。
运算符重载的第一个基本规则 - 不执行 - 尤其适用于重载operator delete
和new
。导致这些运算符超载的几乎唯一原因是 性能问题 和 内存约束 ,并且在很多情况下,还有其他操作与所使用的算法的更改一样,将比尝试调整内存管理提供更多 更高的成本/增益比 。
C ++标准库附带了一组预定义的delete
和new
运算符。最重要的是:
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
,则会调用默认值,这几乎总是错误的
如果重载new
和delete
,则应考虑重载数组变体。
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的构造函数抛出异常。
您还可以使用其他参数重载new
和delete
。与放置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;
非常直观。