注:答案是按特定顺序给出的,但由于许多用户根据投票而不是给出的时间对答案进行排序,因此以下是按最合理顺序列出的答案索引:
- C++中运算符重载的一般语法
- C++中运算符重载的三个基本规则
- 会员与非会员之间的决定
- 重载的常用运算符
- 分配运算符
- 输入和输出运算符
- 函数调用运算符
- 比较运算符
- 算术运算符
- 数组订阅
- 类指针类型的运算符
- 转换运算符
- 重载新建和删除
(注:这意味着是堆栈溢出的C++FAQ的一个条目。如果你想批评在这个表单中提供一个常见问题解答的想法,那么在meta上发布的开始所有这一切的地方就是这样做的地方。这个问题的答案是在C++聊天室中进行监控的,FAQ的想法一开始就出现了,所以你的答案很可能会被那些想出这个想法的人读到。
- 如果我们将继续使用C++ -FAQ标签,这就是条目应该如何格式化。
- 我已经为德国C++社区编写了一系列关于运算符重载的短篇文章:第1部分:C++中的运算符重载覆盖了所有操作符的语义、典型用法和特殊性。它与您的答案有一些重叠,不过还有一些附加信息。第2部分和第3部分是关于使用boost.operators的教程。你想让我翻译它们并把它们作为答案添加吗?
- 哦,还有一个英语翻译:基础知识和常用做法
重载的常用运算符
超载操作中的大部分工作是锅炉板代码。这也就不足为奇了,因为操作符只是语法上的糖分,所以它们的实际工作可以通过(并且经常被转发到)普通函数来完成。但重要的是你要把这个锅炉的代码弄对。如果你失败了,要么你的操作员代码无法编译,要么你的用户代码无法编译,要么你的用户代码的行为会令人惊讶。好的。分配运算符
关于任务有很多话要说。不过,其中大部分已经在GMAN著名的复制和交换常见问题解答中提到过,因此我将跳过这里的大部分内容,只列出完美的分配运算符供参考:好的。
1 2 3 4 5
| X& X::operator=(X rhs)
{
swap(rhs);
return *this;
} |
位移位运算符(用于流I/O)
位移操作符<<和>>虽然仍然用于硬件接口,用于它们从C继承的位操作函数,但在大多数应用程序中,作为超负荷的流输入和输出操作符,它们变得更为普遍。有关作为位操作运算符的引导重载,请参阅下面有关二进制算术运算符的部分。要在对象与iostreams一起使用时实现自己的自定义格式和分析逻辑,请继续。好的。
流运算符是最常见的重载运算符之一,是二进制中缀运算符,其语法对它们应该是成员还是非成员没有任何限制。因为它们更改了左参数(它们更改了流的状态),所以根据经验法则,它们应该作为左操作数类型的成员实现。但是,它们的左侧操作数是来自标准库的流,虽然标准库定义的大多数流输出和输入运算符确实被定义为流类的成员,但是当您为自己的类型实现输出和输入操作时,不能更改标准库的流类型。这就是为什么您需要将这些运算符作为非成员函数实现为您自己的类型的原因。这两者的规范形式如下:好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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参数。除此之外,它可以重载以接受任何数量的附加参数,包括零。好的。
以下是语法示例:好的。
1 2 3 4 5 6 7
| class foo {
public:
// Overloaded call operator
int operator()(const std::string& y) {
// ...
}
}; |
用途:好的。
1 2
| foo f;
int a = f("hello"); |
在C++标准库中,函数对象总是被复制。因此,您自己的函数对象的复制成本应该很低。如果一个函数对象绝对需要使用复制成本很高的数据,最好将该数据存储在其他地方,并让函数对象引用它。好的。比较运算符
根据经验法则,二进制中缀比较运算符应实现为非成员函数1。一元前缀否定!应(根据相同规则)作为成员函数实现。(但通常超载不是一个好主意。)好的。
标准库的算法(如std::sort())和类型(如std::map)始终只希望出现operator<。但是,您类型的用户也希望所有其他运算符都存在,因此,如果定义operator<,请确保遵循运算符重载的第三个基本规则,并定义所有其他布尔比较运算符。实现它们的规范方法是:好的。
1 2 3 4 5 6
| 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。因此,作为成员函数实现的比较运算符必须具有此签名:好的。
1
| bool operator<(const X& rhs) const { /* do actual comparison with *this */ } |
(note the constat the end.)好的。2应该注意,||和&&的内置版本使用快捷语义。虽然用户定义的方法(因为它们是方法调用的语法糖)不使用快捷语义。用户希望这些运算符具有快捷语义,并且它们的代码可能依赖于它,因此强烈建议不要定义它们。好的。算术运算符单目运算符一元递增和递减运算符具有前缀和后缀风格。为了区分两者,后缀变量采用了一个额外的伪int参数。如果重载增量或减量,请确保始终实现前缀和后缀版本。这里是增量、减量的规范实现,遵循相同的规则:好的。
1 2 3 4 5 6 7 8 9 10 11 12 13
| class X {
X& operator++()
{
// do actual increment
return *this;
}
X operator++(int)
{
X tmp(*this);
operator++();
return tmp;
}
}; |
注意,postfix变量是根据前缀实现的。还要注意,postfix会额外复制一份。2好的。
重载一元减号和加号不是很常见,最好避免。如果需要,它们可能作为成员函数被重载。好的。
2还请注意,postfix变量的作用更大,因此比前缀变量的使用效率更低。这是一个很好的理由,通常优先选择前缀增量而不是后缀增量。虽然编译器通常可以为内置类型优化额外的后缀增量工作,但它们可能无法为用户定义的类型执行相同的工作(这可能是像列表迭代器那样的无辜操作)。一旦习惯了使用i++,当i不是内置类型(另外,在更改类型时必须更改代码)时,很难记住使用++i,因此最好养成始终使用前缀增量的习惯,除非明确需要后缀。好的。二进制算术运算符对于二元算术运算符,不要忘记遵守第三个基本规则运算符重载:如果提供+,也提供+=,如果提供-,不要省略-=等。据说Andrew Koenig是第一个观察到复合赋值运算符可以用作EIR非复合副本。也就是说,运营商+按+=执行,-按-=等执行。好的。
根据我们的经验法则,+及其同伴应为非成员,而其复合任务对应方(+=等)改变其左论点应为成员。下面是+=和+的示例代码,其他二进制算术运算符的实现方式应相同:好的。
1 2 3 4 5 6 7 8 9 10 11 12
| 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+通过复制而不是常量引用来获取其左操作数。这样做的原因与operator=在每份文件中提出论点的原因相同。好的。
位操作操作符~&|^<<>>的实现方式与算术运算符相同。但是,(除了输出和输入的<<和>>过载之外)很少有合理的使用案例来过载这些。好的。
3同样,要从中吸取的教训是,a += b通常比a + b更有效,如果可能的话,应该首选。好的。数组订阅数组下标运算符是必须作为类成员实现的二进制运算符。它用于类似容器的类型,这些类型允许通过键访问其数据元素。提供这些的标准形式是:好的。
1 2 3 4 5
| class X {
value_type& operator[](index_type idx);
const value_type& operator[](index_type idx) const;
// ...
}; |
除非您不希望类的用户能够更改由operator[]返回的数据元素(在这种情况下,您可以省略非常量变量),否则应该始终提供运算符的两个变量。好的。
如果已知值_type引用内置类型,则运算符的const变量应返回一个副本,而不是const引用。好的。类指针类型的运算符
为了定义自己的迭代器或智能指针,必须重载一元前缀取消引用运算符*和二进制中缀指针成员访问运算符->:好的。
1 2 3 4 5 6
| 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->()返回非类类型的值。好的。
绝不应重载运算符的一元地址。好的。
关于operator->*()见本问题。它很少使用,因此很少超载。实际上,即使是迭代器也不会重载它。好的。
继续转换运算符好的。好啊。
- 很好的常见问题解答,但是对于插入/提取操作符来说,提到EDOCX1(stackoverflow.com/questions/2298604/&hellip;)怎么样?我还将对->部分进行一些扩展,例如更清楚地说明如何解释返回值等。
- @马特奥:我一上午都坐着想这个,现在已经筋疲力尽了。你为什么不加上你自己的答案?你可以从我的拷贝任何你想要的,我们会把你的链接放进我的而不是我的文本?
- @SBI:我认为这不值得有一个全新的答案,只是对你的改进了一点,这已经很好了(我做的第一件事就是投反对票)。也许我会在后面的->部分添加一些内容,我对sentry的理解不够,无法写一些关于它的内容。
- 既然您编写了比较运算符,那么还必须对它们在实现时应该表示的语义进行注释,特别是,如果您有一个部分顺序呢?你认为使用比较运算符还可以吗?
- 同时,说"a+=b"比"a=a+b"更有效也不是明智之举。关于绩效的假设常常是错误的。
- 请完全分开处理作业。它是一个非常特殊的操作符,因为语义被破坏了。
- @SBI:这不是我为>所知道的规范形式,我通常使用:bool operator>(X const& lhs, X const& rhs) { return rhs < lhs; }也可能值得一提boost。这里的操作符,因为它处理好了这个样板代码并使其正确。)
- @马修:我期待你出现,并指出助推器。你为什么不加上它?正如我在其他地方所说,我从来没有机会使用这个,现在我不得不写C谋生。:(
- @马丁:我真的不明白这怎么可能是对的。如果b>a,那么a是正确的,这就是我最初的实现所做的。我完全不明白你的意思。如果operator< (rhs,lhs),那么operator!=(lhs,rhs)将始终保持不变。
- @令牌:那你就错了,我想。IIRC,PeteBecker(当时正在实现dinkumware的std-lib实现)曾经说过,如果为其类型重载operator&,并且仍然期望他们使用stl容器,那么应该通过实现一个stl来惩罚他们。
- @伊特特里尔:我写道,一般来说,a+=b比A=A+B更有效,因为后者的作用更大。我从没说过总是这样。(事实上,像这样的答案我通常会有很多热情。
- @我不知道你想要什么样的作业。这是在GMAN的复制和交换问题中处理的,我所做的就是链接到这个问题。一个疗程能得到多大的分离?
- operator->()实际上非常奇怪。它不需要返回一个value_type*——实际上,它可以返回另一个类类型,前提是类类型有一个operator->(),随后将调用它。对operator->()的递归调用继续进行,直到出现value_type*返回类型。疯狂!:)
- @随机黑客:我只是想不把太多的信息放进这些"基本规则"中。这个答案已经很长了。因此,我对你的评论投了反对票,而不是让它在评论噪音的其他部分之上可见。但是,如果你认为在答案中提到你的信息是正确的,你可以在答案中添加你的信息。
- 关于比较运算符,我认为std::rel_ops值得一提。
- @自动计算:这可能是真的,但请看我对马修的回答。请随意添加到我的答案或添加您自己的答案。在这个问题上我们可以提一下。
- @ SBI:+1。但在比较运算符部分,您最后列出了>,但早期的运算符依赖它来实现它们。当我复制和粘贴此代码时,它不会编译,因为没有定义">"(直到稍后)。
- 注意,在用operatorX=定义operatorX时,如果不可行,也有一些情况。对于矩阵和多项式(英语?)乘法的码因式分解则相反:operator*=应该用operator*来定义。
- @卢切米特:我还没有遇到这样一个情况,用另一种方式做会更有效,但我不怀疑它们的存在。毕竟,这是所有的经验法则。
- 这不完全是有效性的问题。这是关于我们不能用传统的惯用方法在少数情况下完成它:当我们计算结果时,两个操作数的定义需要保持不变。正如我所说,有两个经典的例子:矩阵乘法和多项式乘法。我们可以用*=来定义*,但这会很困难,因为*=的第一个操作之一将创建一个新的对象,计算结果。然后,在for ijk循环之后,我们将用*this交换这个临时对象。即1.副本,2.操作员*,3.交换
- 用operatorx=定义operatorx可防止前者成为constexpr,从而使其成为另一种从另一个派生的切换情况。
- 我不同意您的指针式运算符的常量/非常量版本,例如"const-value"type&;operator*()const;`-这就像在取消引用时让T* const返回const T&,而事实并非如此。或者换句话说:常量指针并不意味着常量指针。事实上,模仿T const *并不容易——这也是标准库中整个const_iterator内容的原因。结论:签名应为reference_type operator*() const; pointer_type operator->() const。
- @JKOR:如果你能进一步说明这样做的确切效果,我会很高兴地完全融入其中。或者,你可以自己做关于转换运算符的答案,在那里移动我的东西,并在C++ 11中加入EDCOX1的15度扭曲。我很乐意把这个答案的链接放到当前的目录中,这样它就会完全暴露出来。(类似于超载new和delete的答案。)
- @我很乐意这么做。我现在有点忙,但我会尽快完成的。谢谢你的邀请!
- 一个意见是:所建议的二进制算术运算符的实现不如它所能达到的效率。se boost operators headers simmetry注意:boost.org/doc/libs/1_54_0/libs/utility/operators.htm symmetr&zwnj;&8203;y如果使用第一个参数do+=的本地副本并返回本地副本,则可以避免使用其他副本。这将启用NRVO优化。
- 正如我在聊天中提到的,L <= R也可以表示为!(R < L),而不是!(L > R)。可能会在难以优化的表达式中节省额外的一层内联(这也是boost.operators实现它的方式)。
- 当经验法则不适用时尝试。==操作符将两个对象视为相等的,但肯定需要经常访问私有成员进行比较——当它是非成员时,这会起作用吗?
- @Thomthom:如果一个类没有一个可以公开访问的API来进入它的状态,那么您就必须使访问它的状态所需的一切都成为该类的一个成员或一个friend。当然,这对所有的操作人员也是如此。
- @Juanchopanza:我把那个卷了回去。您只需将一个优化(副本省略)与另一个优化(RVO)进行交换,而不给出这样做是改进的理由。(请随意讨论,也许你会说服我。)
- @SBI您是否有一个示例,说明您的实现何时会导致比我建议的副本少的副本?
- @Juanchopanza:网站目前不为我加载,但想要速度吗?传递价值!戴夫·亚伯拉罕斯是这方面的典型参考。(这似乎是一份副本。不过,我不能检查它的准确性。)
- @我读过了。但我没有看到一个令人信服的例子,只是很多理论化。我同意,如果您不想退回本地副本,这可能会有所帮助,但如果您这样做,我认为它最多与我的示例相同,但通常会产生更多副本。
- @胡安:我原以为这篇文章会给你一些,但现在你看:a+b+c。a+b的结果是一个右值,可以直接移动到第二次调用operator+的第一个参数中。
- @依靠类型有效移动的SBI。总之,我在这里总结了一些发现:juanchopanzacp.wordpress.com/2014/05/11/&hellip;我的观点是你应该证明你所选择的operator+的形式是正确的,而不是相反。
- 当超载operator<<()和operator>>()时,左侧必须是溪流,还是这是一种标准做法,难以推荐?
- @Mindandsky至少有一个参数必须是用户定义的类型。否则你是自由的,句法上的。但通常流过载,这就是我使用它作为示例的原因。
- @jbi juanchopanzacp.wordpress.com/2014/05/11/&hellip;
- @SBI:您是否考虑将<<和>>改为"位移位运算符(用于流I/O)"?它们作为流I/O的使用并不是它们的主要功能,也不符合它们的优先级,尽管这是新程序员通常首先遇到的一个功能。正如您在注释中看到的,它导致了一些关于这些操作符是否只能用于流(当然不是)的混淆。
- @本:我认为他们现在的主要用途是输入和输出,但我还是可以接受提议的更改。前进!
- @SBI我知道这是5年后的事了,但为什么数组订阅:"如果已知value_type引用内置类型,则运算符的const变量应返回一个副本而不是const引用。"?一直返回常量引用有什么问题?大多数时候,类都是模板化的,所以人们不知道类型,但是即使不知道,我也不认为通过const引用返回有什么问题。
- @这些规则反映了传递参数的规则。基本上,您通过每个const引用来避免复制的开销(有时还有语义)。对于内置的,这不是一个问题,因为复制它们和复制一个地址一样便宜(或比复制一个地址便宜),而且它们的复制语义也很简单。
- @SBI非常感谢,我就是这么想的。不过,阅读这篇文章似乎可以加强这一点,并且通过const引用返回不应该是一个问题。也就是说,如果您有一些模板,那么使用疯狂的std::enable_if来sfinae out the return by const reference是没有意义的。所以你可以说"总是通过常量引用,尽管对于pods来说没有什么区别"。总之,很棒的帖子(它成了我操作过载的参考)。
- @Vsofco:in a template,return by const reference.我原来的Thumb规则在通过论据答案(对于C+03,以后修改C+11)advices to use const无需你参考通行证类型。在一段时间里,你不知道你经历过什么类型的事情,所以原始的建议会迅速通过参考。
- @SBI get that,thanks,I just don't see why you have"except when they are of built-in types,which can be passed by copy."The"except when"enforces something.我只是想把那个变形器拿出来I just want to remove that altogether.是的,他们可以通过拷贝(无过头卷入),但如果你通过了这些参考资料,这也很好。我之所以这样说,是因为我看到了一些最近的问题/答案,所以促进了这类事情,比如测试你的类型是POD,if yes,return by value,if not,return by const ref,which IMO makes no sense.在C+11中,我同意改变,因为运动语义。
- 我错过了什么吗?比较运算符将不会被编译,如果在这张邮件中定义的话,那么"朋友"的关键词将被遗漏,最起码在某一类运算符中定义的时候。
- @Etienne:我不知道你在说什么。请在哪里输入friend
- @Etienne:read this faq.它解释了你为什么会陷入问题(和为什么会陷入问题),但也许不是你真正想要的。如果你在这样一个可怕的惊吓中,你甚至不可能在跳进密码之前读到一个简单的传真,C+++也许不适合你。(大多数人需要一本很好的书来学习。)我建议你从这本书开始,尝试了解它的一切,但如果你太懒了,读这本书。
- @Etienne:The reason this doesn't compile is that you're doing it wrong.你做错事的原因是你不明白你不明白的雨是你没有花时间去了解它。再来一次:我的密码是精细的。如果你正确地使用它。不想成为你想要的东西。
- 我完全理解朋友和非朋友之间的问题和区别。我最清楚地看到,比较运营商作为朋友的功能声明,这就是为什么我认为朋友的功能是典型的方式,并希望帮助改进这一回答。这是很好的留下它就像是自从我有一个典型的想法认为它是错误的,但没有必要叫我放松。
- 这是我放弃的时刻。手
- 执行中是否有效率损失???????????????????????????????????
- @Beauxq:not if the compiler can inline the forwarding calls.(given that they are trivial,this should not be a problem.)
- is a bit interesting argument than what is described above.作为一个例子,double operator->()seems to be a valid syntax(even if useless,of course).
- @skypjack:你可能想看看你和我的答案。自从第二次出现以来,从未有过这样的事情发生,自从这句话看起来像是我的错,我改变了它。如果你想去的话,感觉有改进的自由。
- @SBI just highlighting that the assignment operator here isn't the perfect example for 99%cases,as you don't want to change the RHS.对我来说,完美的人会有一个康斯特河。
- @Ukmonkey:你怎么用一个持续的对象交换你的状态??
- @SBI that's the point;the assignment operator shouldn't be swapping state,it should be assigning the LHS to the RHS,and not changing the RHS at all;except in rare situations,eg std:auto \ ptr
- @Ukmonkey:你为什么不按照我提供的链接做呢?在那里,GMAN用痛苦的篇幅解释了为什么你在这个假设下落后于技术水平10-15年。
- 我试着按照你的建议实现operator|,以遵循算术规则,但这并不能编译,因为我有错误数量的参数inline X operator|(X lhs, const X &rhs)?
- @乔恩:那就是自由函数的实现。你没有把这个放到课堂上,是吗?此处阅读:"应用于对象x和y的二进制中缀运算符@被称为operator@(x,y)或x.operator@(y)。"
- 比较运算符的一节需要一个更新来提及EDCOX1 7,当它是最终的(C++ 20)时。
- 确实是托比。谢谢你指出这一点。然而,本指南的目标是展示多年来收集的经验和共同智慧,任何关于operator<=>的文本,由于它是全新的,将达不到这一标准。但我想这没用。
- "还请注意,operator+通过复制而不是通过const引用获取其左操作数。这样做的原因与operator=在每个副本上取其参数的原因相同。"我想确保我理解:无论什么情况,都需要一个副本来重用operator+=,因此(1)我们通过让编译器这样做来保存键入,以及(2)编译器可以执行副本删除和移动构造以提高性能。"
- @安德鲁:那就是迪亚语(尽管在提出这个成语时,移动结构甚至没有出现在纸上)。
- @数组订阅上的sbi,如果您的索引类型的复制成本很高,您不应该通过const引用传递它吗?
C++中运算符重载的三个基本规则在C++中运算符重载时,有三条基本规则要遵循。像所有这些规则一样,确实有例外。有时人们已经偏离了他们,结果并不是不好的代码,但这种积极的偏离是少之又远之又少。至少,我所看到的100种偏差中有99种是不合理的。不过,这也可能只是千分之999。所以你最好遵守以下规则。
当一个运算符的含义不明显和毫无疑问时,就不应该重载它。相反,提供一个具有精心选择的名称的函数。基本上,重载操作符的第一条也是最重要的规则,在它的核心,是说:不要这样做。这可能看起来很奇怪,因为有很多关于运算符重载的知识,所以很多文章、书籍章节和其他文本都处理这些问题。但是,尽管这似乎是显而易见的证据,但只有少数情况下,运算符重载才是合适的。原因是,除非在应用程序域中使用操作符是众所周知的和无可争议的,否则实际上很难理解操作符应用程序背后的语义。与人们的普遍看法相反,这种情况很难发生。
始终遵循操作员的众所周知的语义。C++对重载运算符的语义没有限制。编译器很乐意接受实现二元+运算符以从其右操作数中减去的代码。然而,这种运算符的用户绝不会怀疑表达式a + b从b中减去a。当然,这假设应用程序域中的操作符的语义是无争议的。
始终提供一组相关操作中的所有操作。操作员之间以及与其他操作相关。如果您的类型支持a + b,那么用户也可以调用a += b。如果它支持前缀增量++a,那么它们也会期望a++工作。如果他们能检查a < b是否正常,他们肯定也能检查a > b是否正常。如果它们可以复制构造您的类型,那么它们也希望分配工作正常进行。
继续会员和非会员之间的决定。
- 我所知道的唯一违反这些规定的是boost::spiritlol。
- @比利:据一些人说,滥用+进行字符串连接是违反规定的,但到目前为止,它已经成为公认的惯例,所以看起来很自然。虽然我记得在90年代我看到的一个家庭BREW字符串类,它使用二进制&来实现这个目的(对于已建立的实践,请参阅basic)。但是,是的,把它放到std库中基本上就把它设置成了石头。同样,滥用<<和>>作为IO,btw。为什么左移位是明显的输出操作?因为当我们第一次看到"你好,世界"时,我们都知道了。应用。没有其他原因。
- "运算符的含义不明显且毫无疑问",您如何确定?
- @好奇的家伙:如果你必须解释的话,那就不明显了。同样,如果您需要讨论或防御过载。
- 那么,容器超载是一种滥用吗?
- 这似乎很有道理,但也有缺点:第1点将使Boost::Spirit不可能存在,这同样适用于iostream(难道不能<"hello world"shift bits?)第2点从字面上抑制了任何创新:刚发明的东西并不"众所周知",但我将来会成为这样的人。std::string使用+而不是"添加"。他们在STL发明之前并不"众所周知"。
- 埃米利奥:这些规则是针对普通C++开发人员的。当然,他们会禁止创建任何类似IO流或灵魂的东西,正如我上面所说的,甚至是一个体面的字符串类。再读一遍,我在这之前说过"有时候人们偏离了他们,结果也不坏,但这种积极的偏离是少之又远之又少。"注意:当你是一个初学者,严格遵守这些规则。当你是一个有经验的C++开发人员(几年后),你可能会形成自己的观点。当你偏离,比如说,精神,依靠同行评论来判断你的想法。
- @好奇的家伙:为什么?除了为了相等性而逐个元素地比较容器,它还能做什么?这不明显吗?
- @SBI:"同行评审"总是个好主意。对我来说,一个选择不当的操作符和一个选择不当的函数名没什么不同(我看到很多)。运算符只是函数。不多不少。规则是一样的。要理解一个想法是好的,最好的方法就是理解它需要多长时间。(因此,同行评审是必须的,但同行必须在没有教条和偏见的人之间进行选择。)
- @埃米利奥:我不知道这是为了什么而争论。你确定你在回复我的评论吗?
- @SBI"这不明显吗?"在我看来,即使两个容器不是等价物,它们也应该是相等的。
- 好奇的家伙:这是Java/C的方法。在C++中,容器保存值,而不是引用,因此"相等"和"等价"之间的差别是不成立的。(如果我正确理解了差异,那就是。)
- @对我来说,关于operator==唯一绝对明显和无可争辩的事实是它应该是一个等价关系(iow,你不应该使用非信令nan)。容器上有许多有用的等价关系。平等意味着什么?"a等于b,表示a和b具有相同的数学值。(非NaN)float的数学值的概念很清楚,但容器的数学值可以有许多不同(类型递归)的有用定义。平等最有力的定义是"他们是同一个对象",它是无用的。
- (…)第二个最强的定义是"持有相同的对象",对于值容器来说它是无用的。另一个强有力的定义是"两个对象是相等的,如果它们不能通过公共接口来区分"(地址比较不是"公共接口的一部分")。这与模拟和BI模拟有关。这个定义的细微差别来自允许您使用的接口部分。
- (…)这两个set>(1,<)和(1,>)是否相等?它们有相同的元素,但是1)如果你能"比较"它们的比较器,你就能够区分它们;2)如果你添加另一个元素2,它们作为序列的元素就不一样了。两个元素顺序相同的hash_set是否相等?这两个问题都不明显(至少对我来说)。
- (…)另外,operator= (x)应该做什么?它是否应与复制构造等效?是否与assign(x.begin(),x.end())相当?两者都是非常合理的选择。我觉得两者都有强有力的论据。这似乎有点反对提供这个操作员。
- @SBI:这在概念上是反对的,但承认一个好的观点。本质上,我不相信运算符和函数之间的区别:如果+表示"串联"或一元*表示"keen star"或~表示"正交"或"转置"可能有问题,则std::string::empty也有问题(用于检查状态的命令动词????难道不是空的更好吗?(…)
- (…)在任何情况下,无论在何处定义符号名,都要定义该名称与其表示的内容之间的约定。除了你之外,任何人都不能明显地看到这个约定,除非你没有向你的用户公开足够的"信息"(从"信息理论"的意义上说)(可能是一个注释、一个文档或一个微不足道的用例,或者原始源代码——当非常简单的时候)。但我必须看到一个明确的矛盾(甚至可能是一个"杂合种族主义"——注意引言——在背景中:说"当你是一个初学者,严格遵守那些规则",将击败任何出现的和蔼。(…)
- (…)真正的事实是,当你是一个初学者时,第一个重载yopu的示例是cout <<"hello world",这只是违反规则(<<应该移位位!)这是老师隐藏的谎言,呈现一个"插入操作"。现在1只能是真的:1)"插入操作符"是碱液,它违反了规则的第一堂课(lol!)或者…2)<<只是一个符号,它的意义取决于它应用于什么,因此它的意义不应该存在偏见。(…)
- (…)插入运算符用于流,移位运算符用于整数。这两个方面都是偶然的。因此,我总是把<<教成"左双箭头",把+教成"十字形"。"cross"是"add"还是"concat",还是"merge"等等,这是我在谈论与之相关的类型时要教的内容。很抱歉有人这么长的评论。
- @好奇心的家伙:不,你让这变得复杂了。在异构容器(aka struct上,operator==()的明显实现是,对于其所有元素(aka"成员"),当应用于同一类型的另一个struct中的对应元素时,operator==()返回true。非异类容器的operator==()的明显含义是,对于容器中的所有元素,当应用于同一类型的另一个容器中的相应对象时,operator==()返回true。
- @埃米利奥:我允许违反这些规则,我把IO流和字符串(以及spirit)列为成功违反这些规则的明显例子。但这并不意味着一个C++新手对字符串的过度加载!可能是一个很好的想法。IO流是在30年前设计的,当运算符重载是C++中最新的东西时,会带来很多历史权重,正如你所指出的,新手在将其作为移位运算符之前,通常会将EDOCX1×5的含义作为输出运算符学习。
- @Emilio:EDOCX1 indicative 0,despite all its shortcomings,unified the C++world which was split into those of different string classes.That alone was a great deed,and the designers would have been hard-pressed to do anything else so wrong for it to not be seen as a success-despite the fact that some disagreed with its(AB)use of EDOCX1 penal for concateration.
- @Emilio:and spirit deliberately set out to abuse operator overloading in order to create an embedded domain-specific language.That's not necessarily the first thing a newbie does,it might well be one of the most often legitional reasons to abuse a programming language,and a very hard task to succeed with unless you have the back of a strong community.
- @SBI:Your all arguments reduce to"don't do unless some body else already did it a number of times".Apply this to"make children"and to"any one",and humanity will extincish in one generation time!It's a hard dirty work,but some has to begin.Newbie or Experts,is not the point.The idea is the point.如果有足够的"believers"的想法GOES,其他人就会死亡。Don't be"racist"against newses:instead to tell them to"go away from water",teach them"how to swim"!(You may be surprised!)
- @Emilio:yes,my suggestion to those seeking advice is not to invent some new thing.在过去的20年里,我发现了一种滥用操作员控制贷款的行为,而且很小的正当用途。(and I have been guilty of this,too,in my first few years of C+)consequency,my advice is to stick to well-known paradigms,unless you know enough of operator over loading that you would uldn't need advice in the first place.(as for the rest of your argument:the comparisons fall so awkwardly short,I won't spend time on them.)
- Please take extended discussions to stack overflow chat,in particular this room
- @SBI so,are you trying to say that EDOCX1 substantial 2 commonline for containers is ill-specified,or are you just saying it without actually trying to say that?
- @SBI"in the last 20 years,I have seen a lot of abuse of operator overloading,and very little justified use."What kind of abuses have seen?Mostly subtle abuses,or ridicous abuses?
- @Curiousguy:operator=On operator=On std::auto_ptrcomes to mind.
- @Benvoigt after EDOCX1 indicative 5.The value/state of EDOCX1 original 6.is equal to/equivalent with the value/state of EDOCX1 indicatoriginal 7 before,so it is not totally crazy.Also,the C++language allows both EDOCX1 simplication 8 and EDOCX1 original 9 as copy-ctor signatures,and EDOCX1 original 10 as well as EDOCX1 indicator signature,so the core language quite explicitly supported this design too.
- @Curiousguy:The language allows defining EDOCX1 commercial 12 as subtraction also.这是一个非常坏的主意,因为它的检查。EDOCX1(音译)5(音译)不应更改7(音译)It does not do so for any built-in type.Support for EDOCX1 penographic 15 is needed to support things like reference-counted pointers,where internal state may change but the user-visible state does.字母名称4
- @Benvoigt"The language allows defining operator+as subtraction also."Actually the code language doesn't know addition from subtraction,so it couldn't tell.Otoh,The core language knows quite well what a copy ctor means."Support for C::Operator=(C&;)is needed to support things like reference-counted pointers,where internal State may change but the user-visual state does."No it is not.
- 是啊,我同意这可能是<Hello World>的话违反了这些规则。我同意,这是作为一个说明什么运营商可以进行贷款的说明,而我们从来没有打算在现实世界中使用它。
- 你为什么要写这样一篇文章:比较操作员应该作为非成员的职能来执行?原因是什么?
- @塞科里:我把这个写在这个答案里了吗?不管怎样,这里解释了:如果一个二元运算符将两个操作数相等地处理(它保持它们不变),则将该运算符实现为非成员函数。你到底想知道什么?
- 对于"当一个运算符的含义不明显且毫无疑问时,它不应该被重载",对于一个向量类比较长度的重载<和>是否违反了这一原则?
- @罗宾逊:STL将容器的比较定义为词典比较,而不是长度。因此,每一个关注v1 < v2的人都会期望它也这样做。为了你想要的东西,这里有v1.size() < v2.size()。
- 赋值运算符在默认情况下是重载的,对吗?
- @Moiz:如果您想知道编译器是否默认生成赋值操作符,请参阅这个问题及其答案。
- 结合这些规则,我个人认为重载操作符的最佳原因是提供与类似类的一致性。例如,如果您正在创建一个与数学相关的类,则可以合理地重载赋值、算术和比较运算符,以便与该类一起使用,将转换运算符转换为内置和/或标准数学类型,以及I/O运算符;而不是太多数组、新的/删除或类似指针的运算符。相反,如果您正在创建一个视频游戏角色类,那么重载算术运算符来修改角色的级别是不合理的。
- 对于管道,如范围内适配器,您对operator|有何看法?
- @丹尼尔:你看,如果没有人为IO设计过载的<<和>>,而你现在就设计出来了,这会让人不高兴的。因为它是30年前完成的,所以现在它是域的一部分。其他不常用的东西也一样:如果你或我在我们的项目中这样做,我们的同事很可能会抱怨。如果它是在一个流行的Boost库中完成的,并且适合代码领域(使用|进行管道是众所周知的),那么这可能是不同的。
C++中运算符重载的一般语法不能更改C++中内置类型的运算符的含义,运算符只能重载用户定义的类型1。也就是说,至少一个操作数必须是用户定义的类型。与其他重载函数一样,只能为一组特定参数重载一次运算符。
并非所有的运算符都可以在C++中重载。在不可重载的运算符中有:.::sizeoftypeidEDCOX1〔4〕,而C++中唯一的三元运算符,EDCOX1〔5〕。
在C++中可以重载的运算符是:
- 算术运算符:+-*/%和+=-=*=/=%=(全二进制中缀);+**/%和+=-=*=/=%=(全二进制中缀);++-前缀;EDOCX11〔7〕一元前缀;EDOCX11〔18〕++++EDOCX1〔(一元前缀和后缀)
- 位操作:&|^<<>>和&=|=^=<<=>>=(全二进制插入);~单前缀。
- 布尔代数:==!=<><=>=||&&全二进;!一元前缀。
- 内存管理:newnew[]deletedelete[]。
- 隐式转换运算符
- 其它:=[]->->*,(全二进制中缀);*&(全一元前缀)()(函数调用,N元中缀)
然而,事实上你可以超载所有这些并不意味着你应该这样做。请参见运算符重载的基本规则。
在C++中,操作符以具有特殊名称的函数的形式重载。与其他函数一样,重载运算符通常可以作为左操作数类型的成员函数或非成员函数实现。一元运算符@3应用于对象x,可以作为operator@(x)或x.operator@()调用。应用于对象x和y的二进制中缀运算符@称为operator@(x,y)或x.operator@(y)4。
作为非成员函数实现的运算符有时是其操作数类型的朋友。
1术语"用户定义"可能有点误导性。C++对内置类型和用户定义类型进行区分。前者属于例如int、char和double;后者属于所有struct、class、union和enum类型,包括来自标准库的类型,即使它们不是,如这样,由用户定义。
2这在本常见问题解答的后面部分介绍。
3 EDCOX1→52 }不是C++中的有效运算符,这就是为什么我使用它作为占位符。
4 C++中唯一的三元运算符不能重载,并且只有n元运算符必须始终作为成员函数实现。
继续C++中运算符重载的三个基本规则。
- %=不是"位操作"运算符
- ~是一元前缀,不是二进制中缀。
- 不可重载运算符列表中缺少.*。
- @凯尔特人:事实上,4.5年来没有人注意到……谢谢你指出,我把它放进去了。
- 不可过载!所以,在那里有.和::。而且,这可能是因为大多数人从不使用指向成员的指针。
- @西提克明斯特:我在读/写.*,但我在想->*,因此忽略了你的不可重载。无论如何,不可重载运算符的列表并不意味着是详尽的("在不能重载的运算符中…"),但是由于缺少->*,我将在中编辑它。
- 使用@而不是一元/二元运算符的实际示例,这不是有点令人困惑吗?有些人可能错误地认为@实际上是一个可重载的运算符。(编辑:我知道你已经在附言中指出了这一点,但是没有人读附言。)
- @Mateen我想用一个占位符而不是一个真正的操作符来明确这不是一个特殊的操作符,而是适用于所有操作符。而且,如果你想成为一个C++程序员,你应该学会注意甚至小字体。:)
- ->*列为不能重载的运算符,在"杂项"下列为可以重载的运算符。
- @罗里:几天前,布鲁兹在他的编辑中这样做了,我错过了。谢谢你指出!我把它修好了。
- 我是一个初学者,我使用代码:块。当我使用operator+(x,y)来重载加号运算符时,会得到一个错误,即该运算符只能接受一个参数!:(
- @H.R.:如果你读过这本指南,你就会知道出了什么问题。我一般建议你阅读与这个问题相关的前三个答案。这不应该超过你半小时的生活,并给你一个基本的理解。稍后可以查找的特定于运算符的语法。您的具体问题建议您尝试将operator+()作为成员函数重载,但给它一个自由函数的签名。请看这里。
- @SBI:我已经读过这三篇文章的第一篇,谢谢你写的。:)我会尝试解决这个问题,否则我认为最好单独问一个问题。再次感谢你让我们的生活如此轻松!D
会员与非会员之间的决定
二进制运算符=(赋值)、[](数组订阅)、->(成员访问)以及n元()运算符(函数调用)必须始终作为成员函数实现,因为语言的语法要求它们这样做。
其他运算符可以作为成员或非成员实现。但是,其中一些函数通常必须作为非成员函数实现,因为您不能修改它们的左操作数。其中最突出的是输入和输出运算符<<和>>,其左侧操作数是来自标准库的流类,您无法更改。
对于必须选择将它们实现为成员函数或非成员函数的所有运算符,请使用以下经验规则来决定:
如果它是一元运算符,则将其作为成员函数实现。
如果一个二元运算符对两个操作数的处理相同(保持不变),则将此运算符实现为非成员函数。
如果一个二元运算符不能同时处理两个操作数(通常它会更改其左操作数),如果它必须访问操作数的私有部分,则使其成为其左操作数类型的成员函数可能很有用。
当然,就像所有的经验法则一样,也有例外。如果你有一个类型
1
| enum Month {Jan, Feb, ..., Nov, Dec} |
而且,您希望为它增加增量和递减运算符,不能将其作为成员函数来执行,因为在C++中,枚举类型不能具有成员函数。所以你必须把它作为一个自由函数重载。对于嵌套在类模板中的类模板,当在类定义中作为内嵌的成员函数执行时,operator<()更容易编写和读取。但这些确实是罕见的例外。
(但是,如果您例外,请不要忘记操作数的const性问题,对于成员函数,它将成为隐式this参数。如果作为非成员函数的运算符将其最左边的参数作为const引用,则作为成员函数的同一个运算符的末尾需要有一个const以使*this成为const引用。)
继续普通操作人员超载。
- 草本萨特的项目在有效C++(或是C++编码标准?)表示应该更喜欢非成员非友元函数而不是成员函数,以增加类的封装。imho,封装原因优先于您的经验法则,但不会降低经验法则的质量值。
- PaSerCEBA:有效的C++是由迈尔斯、C++编写的萨特标准编写的。你指的是哪一个?不管怎样,我不喜欢这样的想法,比如说,operator+=()不是会员。它必须改变它的左手操作数,所以根据定义,它必须深入到内部。如果不成为会员,你会得到什么?
- PauleCeBar你指的是迈尔斯的有效C++,而不是萨特。
- @polybos:哪个项目是EC++?
- SBI:C++编码标准(萨特)中的条目44更喜欢编写非成员非友元函数,当然,它仅适用于仅使用类的公共接口编写此函数。如果你不能(或者可以,但是它会严重阻碍你的表现),那么你必须让它成为你的会员或者朋友。
- @SBI:哦,有效,特别……难怪我把名字弄混了。无论如何,增益是尽可能地限制可以访问对象私有/受保护数据的函数的数量。通过这种方式,您可以增加类的封装,使其维护/测试/演进更容易。
- @SBI:一个例子。假设您正在用operator +=和append方法编写一个字符串类。append方法更为完整,因为您可以将索引i中的参数的子字符串附加到索引n-1:append(string, start, end)中,用start = 0和end = string.size附加+=调用似乎是合乎逻辑的。此时,append可以是一个成员方法,但operator +=不需要是成员,使其成为非成员将减少使用字符串内部的代码播放量,因此这是一件好事……^ ^…
- PaelCeBael:(你知道还有高效的C++吗?)我懂了。如果字符串类有一个append()方法,那么+=确实可以是非成员。但是,首先,使用+和+=作为字符串充其量是有问题的(+不应该是交换的吗?)只是因为它是基于现有的实践。另外,尽管我同意Sutter的观点(顺便说一句,我认为是Meyers第一次发表了一篇关于非成员的文章,实际上改善了封装,因此我感到困惑),但我仍然可能以同样的理由使+=成为成员:现有的实践。
- @斯比:我知道萨特和迈耶斯的书:我几乎拥有所有的书。
- @交换性不应该是一个标准。例如,矩阵乘法不是交换的,而数字乘法是交换的。是否应该禁止仅仅因为C++本机整数*是可交换的而对矩阵过载EDOCX1?6?不。这里,最不意外的原则适用,我们必须根据它的上下文(即它的参数)分析一个操作符。字符串和运算符+也是如此(扩展名为+=)。事实上,没有人期望+操作符在字符串上是可交换的,那么,这有什么问题呢?
- @SBI:现在,关于一个操作符的现有实践可以应用于一个接口(例如,由于用户希望,在字符串上提供+和+=操作符),它不应该用于决定实现细节imho(例如,决定+=是成员还是非成员)。这个实现细节并不是关于个人偏好的:减少代码访问私有数据(即封装)的数量是一件好事,可以衡量,因此应该尽可能地追求(当然,只要代码保持清晰、可读和正确)。
- 由于paercebal给出的原因,最好是在类外执行+或+=(对于其他操作符@)。这里有一个想法:由于+=通常效率更高,所以在类内实现+=,然后有一个模板化的operator+(),使用enable_if只对定义了want_helper_ops的类T启用。把那个锅炉盘给核了!:)
- @paercebal:是的,使用<<和>>进行IO,使用+进行字符串连接是建立的实践。我不反对他们中的任何一个。我要说的是,有人反对这样做,而且在它被确立之前,根据我的三条基本规则,不应该这样做。(是的,我知道除非你违反了规则1,否则它不能成为既定的实践。)至于让+=成为非成员:是的,这似乎合乎逻辑,但我从来没有想到过,因为我用这三个规则超负荷运行了十多年。这种长期养成的习惯很难改掉。
- @随机黑客:问题不在于他们中的一个是否应该是非会员,我认为这是一致明确的。问题是他们是否都应该。见我之前对paercebal的评论。
- 这条规则听起来很奇怪:If a binary operator does not treat both of its operands equally (usually it will change its left operand), it should be a member function of its left operand’s type.。使其成为成员的动机并不是两个操作数被平等对待。我认为你应该把那个项目重新加工一下。执行数字除法的operator/将处理左操作数与右操作数不同的操作数。不过,它应该作为非成员编写。
- @斯比:"这么长时间养成的习惯很难消逝。":我知道……我正在努力摆脱匈牙利符号的习惯…:
- @约翰内斯:如果一个操作符改变了它的左手操作数,它需要访问它的内部。
- @SBI:"如果一个操作符改变了它的左手操作数,它需要访问它的内部。"否:操作员可以使用方法,因此不需要访问受保护/私有成员。同样,在我的字符串示例中,operator +=不需要访问私有成员。它只需要使用正确的参数调用public append方法。
- @我稍微改变了措辞。您现在批准还是需要进一步更改?
- @SBI:考虑之后,列表应该是:1。"如果可能,使其成为非会员非朋友",2.以下运算符只能用作成员函数:…"。…其他一切似乎都是品位问题。我自己对这个问题的看法与你的观点相似,但这仍然是一个品味问题,这个优秀的问答集不应该被个人品味所玷污。它应该是一个参考,只由岩石硬的原因驱动。
- @但我认为"如果可能的话…"对于经验法则来说太模糊了。这就是我想说的:一个简单易记的经验法则,即使你记不清所有的推理,你也能记住。现在,我的规则基本上是说"让它成为非成员,如果它改变了左操作数,就考虑成为成员"。如果你们中的大多数人不认为这是足够的,我会改变。但是"如果可能的话…"不足以改善我现在的状况。
- @对我来说,"如果可能的话…"是指"如果它编译了…"。某些运算符必须是成员函数。对于其他函数,我想不管怎样,应该优先使用非成员函数,即使friend,因为这些函数授权将某个值转换为类值(例如,让代码MyInteger m(42), n ; n = 25 + m ;工作)。
- @SBI:在您的授权下,我可以考虑一个备选项目列表,并在文章结尾处将其写为"建议"(这些评论不适合这个冗长的讨论)。另一种可能是通过电子邮件联系我(在我的标识符上附加"@"+"gmail"+"+"+"com")。
- PauleCeBa:如果评论太长,我们为什么不在C++聊天中讨论这个问题呢?这就是所有常见问题的想法开始的地方。现在那里很安静(我也不能在网上讨论),但在5-10小时内,情况通常会大不一样。(如果你想的话,你现在可以把你的想法贴在那里,不管怎样,他们不会迷路的。)
- @SBI我只看到了这条线,我希望这个问题能得到解答,尽管这条线现在已经快2岁了。关于二元运算符的重载,如果运算符对两个操作数的处理相同(或不更改其内部),为什么它不应是成员函数?
- @AndrewFlanga此评论回答了您的问题:stackoverflow.com/questions/4421706/operator overloading/&hellip;
- 有没有建议放置助手(非成员)功能的地方?在与类相同的文件中,还是在与类相同的命名空间中的单独文件中?
- @丹尼斯:如果它们是类的接口的一部分(如widget operator+(const widget&, const widget&)或void normalize(widget&),它们应该在类的命名空间中,靠近类的声明。如果它们仅仅是私人帮助者,则将它们隐藏在.cpp文件中(未命名的命名空间),或者,如果所有内容都在头文件中,则隐藏在某些namespace details文件中。
- 我可能是错的,但我觉得一元减号运算符的情况被遗忘了(-a)…经验法则规定,"如果它是一元运算符,则将其作为成员函数实现。",但一元减号运算符是一元运算符,但将此运算符作为非成员函数实现更符合逻辑(因为毕竟,它离开了t操作数不变)。顺便说一句,一元加运算符也是如此。
- @据我所知,我甚至没有提到一元减号,除了要重载的操作符列表。这显然是一元负的疏忽,因为一元负一点用处都没有。事实上,我不相信在我20多年来看到的严重滥用运算符重载的情况中,只有一元负运算符重载一例。不过,感谢您指出这一点。我现在至少提到过他们。按照规定,我已经让他们成为会员。我觉得很合适。
- @SBI,一元减号运算符有时在添加两个对象时很有用,我们只需编写一次加法,然后通过编写a+(-b)来实现减号运算符。这会阻止代码复制。
- 此外,我不确定规则是否合乎逻辑,因为一元减号运算符保持操作数不变,因此与加法的方式相同,它应该是非成员函数,对吗?
- @ MLPO:1。您也可以使用a.data + (-b.data)实现减法,而不需要向类中添加减法运算符。(当然,如果类应该是数字类型,那么根据这里的规则3,operator-()和operator+()应该是接口的一部分。)2.根据上面的规则1,一元运算符应该是成员,不管它们是否更改对象。(除了前缀递增/递减之外,我想不出有任何这样的功能。)这是因为您很少希望转换等应用于它们。
转换运算符(也称为用户定义的转换)
在C++中,您可以创建转换运算符,允许编译器在类型和其他定义类型之间转换的运算符。有两种类型的转换运算符,隐式和显式。
隐式转换运算符(C++ 98/C++ 03和C++ 11)
隐式转换运算符允许编译器将用户定义类型的值隐式转换为其他类型(如int和long之间的转换)。
下面是一个带有隐式转换运算符的简单类:
1 2 3 4 5 6
| class my_string {
public:
operator const char*() const {return data_;} // This is the conversion operator
private:
const char* data_;
}; |
隐式转换运算符(如一个参数构造函数)是用户定义的转换。当试图匹配对重载函数的调用时,编译器将授予一个用户定义的转换。
1 2 3 4
| void f(const char*);
my_string str;
f(str); // same as f( str.operator const char*() ) |
起初,这似乎很有帮助,但问题在于,隐式转换甚至在预期不到的时候开始。在以下代码中,将调用void f(const char*),因为my_string()不是左值,因此第一个值不匹配:
1 2 3 4
| void f(my_string&);
void f(const char*);
f(my_string()); |
初学者很容易出错,甚至有经验的C++程序员有时会感到惊讶,因为编译器选择了他们没有怀疑的过载。这些问题可以通过显式转换运算符来减轻。
显式转换运算符(C++ 11)
与隐式转换操作符不同,显式转换操作符永远不会在您不期望它们出现的时候出现。下面是一个带有显式转换运算符的简单类:
1 2 3 4 5 6
| class my_string {
public:
explicit operator const char*() const {return data_;}
private:
const char* data_;
}; |
注意explicit。现在,当您尝试从隐式转换运算符执行意外的代码时,会得到一个编译器错误:
1 2 3 4 5 6 7
| 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之后,不允许再进行一次隐式转换(一次允许编译器进行两次隐式转换,但最多只能进行一次用户定义的转换)。
因为编译器不会强制转换"past"bool,所以显式转换操作符现在不再需要安全的bool习惯用法。例如,C++ 11之前的智能指针使用安全BooL习语来防止转换成整数类型。在C++ 11中,智能指针使用显式运算符,因为编译器在显式将类型转换为BoOL后不允许隐式转换为整型。
继续超载new和delete。
超载
new和
delete。
注意:这只处理重载new和delete的语法,而不处理此类重载运算符的实现。我认为重载new和delete的语义应该有它们自己的常见问题,在操作符重载的主题中,我永远也做不到公正的处理。
基础
在C++中,当您编写一个新的表达式,如EDCOX1,6,当这个表达式被评估时发生两件事:首先调用EDCOX1(7),以获得原始内存,然后调用EDCOX1 OR 8的适当构造函数将原始内存转换为有效对象。同样,当您删除一个对象时,首先调用它的析构函数,然后将内存返回到operator delete。C++允许您调整这两种操作:内存管理和对象在分配内存中的构建/销毁。后者是通过为类编写构造函数和析构函数来完成的。微调内存管理是通过编写自己的operator new和operator delete来完成的。
运算符重载的第一个基本规则(不要这样做)尤其适用于重载new和delete。几乎导致这些运算符过载的唯一原因是性能问题和内存限制,在许多情况下,其他操作(如对所用算法的更改)将比尝试调整内存管理提供更高的成本/收益比。
C++标准库附带一组预定义的EDCOX1、0和EDCOX1,1个操作符。最重要的是:
1 2 3 4
| 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 new匹配的operator delete中,以便分配内存来创建对象。如果不提供匹配的operator delete,则调用缺省值,这几乎总是错误的。如果重载new和delete,也应该考虑重载数组变量。
放置
new。
C++允许新的和删除运算符接受额外的参数。所谓的Placement New允许您在某个地址创建一个对象,该对象将传递给:
1 2 3 4 5 6 7 8
| class X { /* ... */ };
char buffer[ sizeof(X) ];
void f()
{
X* p = new(buffer) X(/*...*/);
// ...
p->~X(); // call destructor
} |
标准库为此提供了适当的new和delete操作符重载:
1 2 3 4
| 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(); |
注意,在上面给出的用于放置new的示例代码中,除非x的构造函数抛出异常,否则从不调用operator delete。
您还可以使用其他参数重载new和delete。与placement new的附加参数一样,这些参数也列在关键字new后面的括号中。仅仅出于历史原因,这种变体通常也被称为新的放置,即使它们的论点不是为了将对象放置在特定的地址。
类特定的新建和删除
最常见的情况是,您希望对内存管理进行微调,因为测量表明,特定类或一组相关类的实例经常被创建和销毁,并且针对一般性能进行优化的运行时系统的默认内存管理在这种特定情况下效率低下。要改进这一点,可以为特定类重载new和delete:
1 2 3 4 5 6 7 8 9
| 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,只需将标准库的预定义操作符替换为我们自己的操作符。然而,很少需要这样做。
- 我也不同意将全局操作符new和delete替换为performance:相反,它通常用于bug跟踪。
- 还应该注意,如果使用重载的新运算符,则还需要提供带有匹配参数的删除运算符。你可以在全球新建/删除一节中这样说,而这一节对它不太感兴趣。
- @你指的是哪个泰特尔?这个答案的标题是"重载新建和删除"。这是如何自相矛盾的?"混乱的描述"是一个相当广泛的批评。我怎么知道你的期望?
- @YTTril:IME重载new和delete很少用于查找实际的错误。但这可能是因为我努力在我工作的地方使用RAII,从而完全消除了此类错误。
- @YTTril:我在"基础"一节中介绍了如何为每个新增的操作符提供匹配的操作符删除。在关于全局新建/删除的部分中,只有两个句子,其中的non指的是这个。我错过了什么?
- @你把事情搞混了。意思变得超负荷了。"运算符重载"的意思是指重载。这并不意味着字面上的函数被重载,特别是新的操作符不会重载标准的版本。@SBI并不主张相反的观点。通常称之为"重载新的",就像通常称之为"重载加法运算符"。
- 那么nothrow新的呢?经验法则是,每当您重载新函数时,必须编写12个函数:[array] [{ placement | nothrow }] { new | delete }。
- 亚历山大:这整个FAQ条目是从我用来指导C++的指南中提炼出来的,因为EDCOX1的7个词或多或少只是为了遗留的东西,我甚至从来没有提到过它。你建议在这里写些什么?
- @SBI:请参阅(或更好,链接至)gotw.ca/publications/mill15.htm。这只是对有时使用nothrownew的人的良好做法。
- new(buffer) X(/*...*/)什么实际保证buffer与X正确对齐?
- 这部分常见问题需要改进。有一点:过载operator new和delete的主要原因不是为了性能,而是因为调试和内存系统的极度受限。
- @unixman83:内存性能也是性能。以东十一〔0〕说真的:你说得对,我会在回答中加上这一点。谢谢你指出。不过,我不同意调试。IME使用第三方工具比自己制作业余工具要好得多。另外,我认为内存泄漏更多的是您的编程风格&180;的问题,而不是您的工具。我不必去追查记忆泄露,现在大概有十年了。
- "如果不提供匹配的删除操作,则默认的操作称为"->实际上,如果添加任何参数而不创建匹配的删除操作,则根本不会调用任何删除操作,并且会出现内存泄漏。(15.2.2,只有在适当的情况下,对象所占用的存储才被释放…找到删除操作员)
为什么operator<<函数不能将对象流到std::cout或文件中?
假设你有:
1 2 3 4 5 6 7 8 9 10
| struct Foo
{
int a;
double b;
std::ostream& operator<<(std::ostream& out) const
{
return out << a <<"" << b;
}
}; |
鉴于此,您不能使用:
1 2
| Foo f = {10, 20.0};
std::cout << f; |
由于operator<<作为Foo的成员函数被重载,因此运算符的lhs必须是Foo对象。也就是说,您需要使用:
1 2
| Foo f = {10, 20.0};
f << std::cout |
这是非常不直观的。
如果将其定义为非成员函数,
1 2 3 4 5 6 7 8 9 10
| struct Foo
{
int a;
double b;
};
std::ostream& operator<<(std::ostream& out, Foo const& f)
{
return out << f.a <<"" << f.b;
} |
您将能够使用:
1 2
| Foo f = {10, 20.0};
std::cout << f; |
这是非常直观的。