????超载运营商的大部分工作都是锅炉板代码。这并不奇怪,因为操作符只是语法糖,它们的实际工作可以通过(通常转发到)普通函数来完成。但重要的是你要正确使用这种锅炉板代码。如果您失败,您的运营商代码将赢得编译或您的用户代码不会编译或您的用户代码将出乎意料地行事。 ??
????关于任务有很多话要说。但是,大部分内容已经在说过了 ???? ??????GMan着名的Copy-And-Swap常见问题解答 ???? ????,所以我将在这里跳过大部分内容,仅列出完美的赋值运算符以供参考: ??
X& X::operator=(X rhs) { swap(rhs); return *this; }
????bitshift运算符 ????? << ?????和 ????? >> ????尽管仍然在硬件接口中用于从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>> ????,只有当读取本身成功时才需要手动设置流状态,但结果不是预期的结果。 ??
operator>>
????用于创建函数对象的函数调用操作符(也称为函子)必须定义为a ????的 ?????? ????????会员 ?????? ????强> ?????函数,所以它总是有隐含的 ????? this ?????成员职能的论点。除此之外,它可以重载以获取任意数量的附加参数,包括零。 ??
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< ????,一定要遵循运算符重载的第三个基本规则,并定义所有其他布尔比较运算符。实现它们的规范方法是: ??
std::sort()
std::map
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 ??????也是。因此,作为成员函数实现的比较运算符必须具有以下签名: ????子> ??
*this
const
bool operator<(const X& rhs) const { /* do actual comparison with *this */ }
????<子> ??????(注意 ??????? const ???????在末尾。) ????子> ??
???? ??????2 ???? ????<子> ??????应该注意的是内置版本 ??????? || ???????和 ??????? && ???????使用快捷语义。虽然用户定义的(因为它们是方法调用的语法糖)不使用快捷语义。用户希望这些运算符具有快捷语义,并且它们的代码可能依赖于它,因此强烈建议永远不要定义它们。 ????子> ??
????一元递增和递减运算符有前缀和后缀两种风格。为了告诉另一个,后缀变体采用额外的伪int参数。如果重载增量或减量,请确保始终实现前缀和后缀版本。 这是增量的规范实现,减量遵循相同的规则: ??
class X { X& operator++() { // do actual increment return *this; } X operator++(int) { X tmp(*this); operator++(); return tmp; } };
????请注意,后缀变体是根据前缀实现的。另请注意,postfix会额外复制。 ???? ??????2 ???? ??
????重载一元减号和加号不是很常见,可能最好避免。如果需要,它们可能应该作为成员函数重载。 ??
???? ??????2 ???? ????<子> ??????另请注意,后缀变体功能更多,因此使用效率低于前缀变量。这是一个很好的理由,通常更喜欢前缀增量而不是后缀增量。虽然编译器通常可以优化内置类型的后缀增量的额外工作,但是它们可能无法对用户定义的类型执行相同的操作(这可能是像列表迭代器那样无辜地看起来的东西)。一旦你习惯了 ??????? i++ ??????,记得这么做很难 ??????? ++i ???????相反 ??????? i ???????不是内置类型(加上你必须在更改类型时更改代码),所以最好养成一直使用前缀增量的习惯,除非明确需要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= ?????每个副本采取其论点。 ??
operator+=
operator+
a + b
operator=
????位操作运算符 ????? ~ ????? & ????? | ????? ^ ????? << ????? >> ?????应该以与算术运算符相同的方式实现。但是,(超载除外 ????? << ?????和 ????? >> ?????对于输出和输入,很少有合理的用例来重载这些。 ??
~
&
|
^
??????3 ???? ????<子> ??????同样,从中可以得到的教训是 ??????? a += b ???????通常,效率高于 ??????? 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引用内置类型,则运算符的const变量应返回副本而不是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->() ?????返回非类类型的值。 ??
value_type
class
struct
union
operator->()
????一元地址运算符永远不应该重载。 ??
????对于 ????? operator->*() ?????看到 ???? ??????这个问题 ???? ????。它很少使用,因此很少超载。事实上,即使是迭代器也不会使它超载。 ??
operator->*()
????继续 ???? ??????转换运算符 ???? ??
new
delete
???? ??????的 ???????? ??????????注意: ???????? ??????强> ???????这只涉及到 ??????的 ???????? ??????????句法 ???????? ??????强> ???????超载 ??????? new ???????和 ??????? delete ??????,而不是 ??????的 ???????? ??????????履行 ???????? ??????强> ???????这样的重载运营商。我认为重载的语义 ??????的 ???????? ?????????? ????????????? new ?????????????和 ????????????? delete ?????????????值得拥有自己的FAQ ?????????? ???????? ??????强> ??????在运算符重载的主题内,我永远不能正义。 ???? ??
????在C ++中,当你写一个 ????的 ?????? ????????新的表达 ?????? ????强> ?????喜欢 ????? new T(arg) ?????评估此表达式时会发生两件事:第一 ????的 ?????? ????????? operator new ?????? ????强> ?????被调用以获取原始内存,然后是相应的构造函数 ????? T ?????被调用以将此原始内存转换为有效对象。同样,当你删除一个对象时,首先调用它的析构函数,然后返回内存 ????? operator delete ????。 ???? ???? C ++允许您调整这两个操作:内存管理以及在分配的内存中构造/销毁对象。后者是通过为类编写构造函数和析构函数来完成的。微调内存管理是通过编写自己的内容来完成的 ????? operator new ?????和 ????? operator delete ????。 ??
new T(arg)
operator new
T
operator delete
????运算符重载的第一个基本规则是 ???? ??????不要这样做 ???? ?????特别适用于超载 ????? new ?????和 ????? delete ????。几乎是使这些运算符超载的唯一原因是 ????的 ?????? ????????性能问题 ?????? ????强> ?????和 ????的 ?????? ????????记忆约束 ?????? ????强> ????,在许多情况下,其他行动,如 ???? ??????更改算法 ???? ?????用过,会提供很多 ????的 ?????? ????????更高的成本/收益率 ?????? ????强> ?????而不是试图调整内存管理。 ??
????C ++标准库附带一组预定义的 ????? 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 ????,你也应该考虑重载数组变体。 ??
????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();
????请注意,在上面给出的新安置示例代码中, ????? 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) ????。但是,这些运算符也被称为动态分配的对象 ????的 ?????? ????????派生类 ?????? ????强> ????,在这种情况下,它可能大于那个。 ??
my_class
std::size_t
sizeof(my_class)
????要重载全局new和delete,只需用我们自己的标准库替换标准库的预定义运算符。但是,这很少需要完成。 ??
???? ??????为什么不能 ??????? operator<< ???????流对象的功能 ??????? std::cout ???????或者文件是会员功能? ???? ??
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 ????,运营商的LHS必须是 ????? Foo ?????宾语。这意味着,您将被要求使用: ??
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; }
????您将能够使用: ??
????这非常直观。 ??