Part III: Tools for Class Authors
Chapter 14. Overloaded Operations and Conversions
当运算符应用于类类型的对象时,运算符重载 (operator overloading) 允许我们定义它的含义。
重载的运算符是具有特殊名字的函数:关键字 operator
后面跟着要定义的运算符符号。
重载运算符有返回类型、形参列表和函数体。
重载运算符函数中形参的个数与运算符的运算对象的个数一样。
在二元运算符中,左侧运算对象传递给第一个形参,右侧运算对象传递给第二个。
除了重载的函数调用运算符 operator()
以外,重载的运算符没有默认实参。
如果一个运算符函数是成员函数,第一个(左侧)运算对象绑定到隐式 this 指针。因此,成员运算符函数的(显式)形参的个数比运算符的运算对象的个数要少一个。
运算符函数要么是类的成员,要么至少有一个类类型的形参。
// error: cannot redefine the built-in operator for ints
int operator+(int, int);
此限制意味着,当将运算符符应用于内置类型的运算对象时,我们无法更改其含义。
可以重载大多数,但不是所有,运算符。
可以重载的运算符:
&#43; - * / % ^ & | ~ ! , &#61; < > <&#61; >&#61; &#43;&#43; -- << >>
&#61;&#61; !&#61; && || &#43;&#61; -&#61; /&#61; %&#61; ^&#61; &&#61; |&#61; *&#61; <<&#61; >>&#61;
[] () -> ->* new new[] delete delete[]
不能重载的运算符&#xff1a;
:: .* . ?:
只能重载已有的运算符&#xff0c;不能发明新的运算符符号。
重载运算符的优先级和结合律与其对应的内置运算符相同。
直接调用一个重载运算符函数
通常情况下&#xff0c;通过在正确类型的实参上使用运算符&#xff0c;可以直接“调用”重载运算符函数。然而&#xff0c;也可以像调用普通函数那样直接调用重载运算符函数。
// equivalent calls to a nonmember operator function
data1 &#43; data2;
// normal expression
operator&#43;(data1, data2); // equivalent function call
显式调用成员运算符函数的方式与其他成员函数相同。
data1 &#43;&#61; data2; // expression-based "call"
data1.operator&#43;&#61;(data2); // equivalent call to a member operator function
一些运算符不应该被重载
一些运算符可以保证运算对象的求值顺序。因为使用重载运算符实际上是一个函数调用&#xff0c;所以这些保证不能应用到重载运算符。
特别是&#xff0c;逻辑与、逻辑或、逗号运算符的运算对象求值顺序的保证不能保留。
而且&#xff0c;&&
或 ||
运算符的重载版本不保留内置运算符的短路求值属性。
因为这些运算符的重载版本不能保留求值顺序和/或短路求值&#xff0c;因此通常不建议重载它们。
不重载逗号的另一个原因&#xff0c;也是不重载取地址运算符的原因&#xff1a;与大多数运算符不同&#xff0c;C&#43;&#43;语言定义了逗号和取地址运算符在应用与类类型时的含义。因为这些运算符有内置含义&#xff0c;它们通常不应该被重载。
使用的定义与内置含义一致
当设计一个类时&#xff0c;应该总是先考虑该类将提供哪些操作。只有在知道需要什么操作之后&#xff0c;才考虑将每个操作定义为普通函数还是重载运算符。如果操作在逻辑上可以映射到运算符上&#xff0c;那么将其定义为重载运算符是不错的选择&#xff1a;
operator&#61;&#61;
。如果类具有operator&#61;&#61;
&#xff0c;则通常也应该具有operator!&#61;
。operator<
。如果类具有 operator<
&#xff0c;则它可能应该具有所有关系运算符。赋值与复合赋值运算符
赋值运算符的行为应类似于合成运算符&#xff1a;赋值后&#xff0c;左侧和右侧运算对象应具有相同的值&#xff0c;并且运算符应返回对其左侧运算对象的引用。
如果一个类具有算术运算符或位运算符&#xff0c;那么通常也应提供相应的复合赋值运算符。&#43;&#61;
运算符应定义为具有与内置运算符相同的行为&#xff1a;先执行 &#43;
&#xff0c;后执行 &#61;
。
选择作为成员或非成员实现
下面的准则有助于决定使运算符作为成员或普通非成员函数&#xff1a;
&#61;
、下标 []
、调用 ()
、成员访问箭头 ->
运算符必须定义为成员。程序员希望能够在混合类型的表达式中使用对称性运算符。例如&#xff0c;可以将一个 int 和一个 double 相加。这种加法是对称的&#xff0c;因为可以将任一类型用作左侧或右侧运算对象。如果想提供涉及类对象的相似混合类型表达式&#xff0c;则必须将运算符定义为非成员函数。
如果将运算符定义为成员函数&#xff0c;则左侧运算对象必须是该运算符所属类的对象。
string s &#61; "world";
string t &#61; s &#43; "!"; // ok: we can add a const char* to a string
string u &#61; "hi" &#43; s; // would be an error if &#43; were a member of string
因为 string 将 &#43;
定义为普通的非成员函数&#xff0c;所以 "hi" &#43; s
等同于 operator&#43;("hi", s)
。与任何函数调用一样&#xff0c;任何一个实参都可以转换为形参类型。唯一的要求是至少一个运算对象具有类类型&#xff0c;并且两个运算对象都可以&#xff08;明确地&#xff09;转换为字符串。
建议&#xff1a;仅当运算符对用户来说无二义性时&#xff0c;才使用它。若运算符有多个合理的解释&#xff0c;则该运算符具有二义性。
通常情况下&#xff0c;输出运算符的第一个形参是一个指向非const ostream 对象的引用。
第二个形参一般应该是对将要打印的类类型的 const 引用。
为了与其他输出运算符一致&#xff0c;operator<<
一般返回其 ostream
形参。
Sales_data 输出运算符
ostream &operator<<(ostream &os, const Sales_data &item) {os << item.isbn() << " " << item.units_sold << " " << item.revenue << " " << item.avg_price();return os;
}
通常&#xff0c;输出运算符打印对象的内容时应该尽量减少格式化操作。它们不应该打印换行符。
IO运算符必须是非成员函数
Sales_data data;
data << cout; // if operator<
IO运算符通常需要读写非public 数据成员&#xff0c;所以IO运算符通常应该声明为友元。
通常&#xff0c;输入运算符的第一个形参是对将要读取的流的引用&#xff0c;第二个形参是对读入到的非const对象的引用。运算符通常返回对其给定流的引用。
Sales_data 输入运算符
istream &operator>>(istream &is, Sales_data &item) {double price; // no need to initialize; we&#39;ll read into price before we use itis >> item.bookNo >> item.units_sold >> price;if (is) // check that the inputs succeededitem.revenue &#61; item.units_sold * price;elseitem &#61; Sales_data(); // input failed: give the object the default statereturn is;
}
注意&#xff1a;输入运算符必须处理输入失败的可能性&#xff1b;输出运算符一般不需要。
输入时的错误
输入运算符可能发生的错误&#xff1a;
实践&#xff1a;如果有错误&#xff0c;输入运算符应该决定如何进行错误恢复。
标示错误
一些输入运算符需要做额外的数据验证。输入运算符可能需要设置流的条件状态来标明错误&#xff0c;即使从技术来将实际的IO是正确的。通常输入运算符只设置 failbit。而 eofbit、badbit 等错误最好留给IO库自己标示。
通常情况下&#xff0c;为了允许对左侧或右侧运算对象进行转换&#xff0c;将算术与关系运算符定义为非成员函数。
这些运算符应该不需要改变运算对象的状态&#xff0c;所以形参通常是指向const的引用。
算术运算符通常生成一个新值&#xff0c;它是两个运算对象计算的结果。
定义算术运算符的类一般也会定义对应的复合赋值运算符。
实践&#xff1a;同时定义了算术运算符和相关复合赋值的类&#xff0c;通常应使用复合赋值来实现算术运算符。
// assumes that both objects refer to the same book
Sales_data operator&#43;(const Sales_data &lhs, const Sales_data &rhs) {Sales_data sum &#61; lhs; // copy data members from lhs into sumsum &#43;&#61; rhs; // add rhs into sumreturn sum;
}
bool operator&#61;&#61;(const Sales_data &lhs, const Sales_data &rhs) {return lhs.isbn() &#61;&#61; rhs.isbn() && lhs.units_sold &#61;&#61; rhs.units_sold && lhs.revenue &#61;&#61; rhs.revenue;
}
bool operator!&#61;(const Sales_data &lhs, const Sales_data &rhs) {return !(lhs &#61;&#61; rhs);
}
设计准则&#xff1a;
operator&#61;&#61;
而不是命名函数&#xff1a;用户无需学习记忆一个新操作名称&#xff1b;使用库容器和算法更方便。operator&#61;&#61;
&#xff0c;此运算符一般应该确定给定的对象是否包含相等的数据。operator&#61;&#61;
&#xff0c;它应该也定义 operator!&#61;
。定义了相等运算符的类通常&#xff08;但不总是&#xff09;定义关系运算符。特别地&#xff0c;因为关联容器和一些算法使用小于运算符&#xff0c;定义 operator<
会有用。
一般情况下关系运算符应该&#xff1a;
&#61;&#61;
&#xff0c;定义关系要与其一致。特别是&#xff0c;如果两个对象 !&#61;
&#xff0c;那么一个对象应该 <
另一个。如果 <
存在唯一的逻辑定义&#xff0c;则类通常应定义 <
运算符。但是&#xff0c;如果类也具有 &#61;&#61;
&#xff0c;则仅当 <
和 &#61;&#61;
的定义产生一致的结果时才定义 <
。
除了复制赋值和移动赋值运算符&#xff08;将类类型的一个对象赋值给相同类型的另一个对象&#xff09;之外&#xff0c;类还可以定义允许其他类型作为右侧运算对象的其他赋值运算符。
例如&#xff0c;除了复制赋值和移动赋值运算符之外&#xff0c;库 vector 类还定义了第三个赋值运算符&#xff0c;该运算符接受一个使用花括号括起来的元素列表。可以如下使用该运算符&#xff1a;
vector<string> v;
v &#61; {"a", "an", "the"};
可以把这个运算符加到 StrVec 类中&#xff1a;
class StrVec {
public:StrVec &operator&#61;(std::initializer_list<std::string>);// other members as in § 13.5
};StrVec &StrVec::operator&#61;(initializer_list<string> il) {// alloc_n_copy allocates space and copies elements from the given rangeauto data &#61; alloc_n_copy(il.begin(), il.end());free(); // destroy the elements in this object and free the spaceelements &#61; data.first; // update data members to point to the new spacefirst_free &#61; cap &#61; data.second;return *this;
}
复合赋值运算符
复合赋值运算符不需要是成员。但是&#xff0c;我们倾向于在类内定义所有的赋值&#xff0c;包括复合赋值。
复合赋值运算符应该返回指向其左侧运算对象的引用。
// member binary operator: left-hand operand is bound to the implicit this pointer
// assumes that both objects refer to the same book
Sales_data& Sales_data::operator&#43;&#61;(const Sales_data &rhs) {units_sold &#43;&#61; rhs.units_sold;revenue &#43;&#61; rhs.revenue;return *this;
}
可以通过位置提取元素的表示容器的类&#xff0c;通常定义下标运算符 operator[]
。
下标运算符必须是成员函数。
为了与下标的普通含义兼容&#xff0c;下标运算符通常返回对所获取元素的引用。通过返回引用&#xff0c;下标可以在赋值的任何一侧使用。
如果类有下标运算符&#xff0c;通常应该定义两种版本&#xff1a;一种返回普通引用&#xff0c;另一种是 const 成员&#xff0c;返回 const 引用。当下标应用于 const 对象时&#xff0c;下标应返回对 const 的引用&#xff0c;这样就不能将赋值给返回的对象。
class StrVec {
public:std::string& operator[](std::size_t n) { return elements[n]; }const std::string& operator[](std::size_t n) const { return elements[n]; }// other members as in § 13.5
private:std::string *elements; // pointer to the first element in the array
};// assume svec is a StrVec
const StrVec cvec &#61; svec; // copy elements from svec into cvec
// if svec has any elements, run the string empty function on the first one
if (svec.size() && svec[0].empty()) {svec[0] &#61; "zero"; // ok: subscript returns a reference to a stringcvec[0] &#61; "Zip"; // error: subscripting cvec returns a reference to const
}
定义了递增或递减运算符的类应该同时定义前置和后置版本。这些运算符通常应该定义为成员。
定义前置递增/递减运算符
class StrBlobPtr {
public:// increment and decrementStrBlobPtr& operator&#43;&#43;(); // prefix operatorsStrBlobPtr& operator--();// other members as before
};
为了与内置运算符一致&#xff0c;前置运算符应该返回指向递增或递减后对象的引用。
// prefix: return a reference to the incremented/decremented object
StrBlobPtr& StrBlobPtr::operator&#43;&#43;() {// if curr already points past the end of the container, can&#39;t increment itcheck(curr, "increment past end of StrBlobPtr");&#43;&#43;curr; // advance the current statereturn *this;
}StrBlobPtr& StrBlobPtr::operator--() {// if curr is zero, decrementing it will yield an invalid subscript--curr; // move the current state back one elementcheck(-1, "decrement past begin of StrBlobPtr");return *this;
}
区分前置与后置运算符
为了与前置版本区分&#xff0c;后置版本接受一个额外的 int 类型的形参。当使用后置运算符时&#xff0c;编译器为这个形参提供 0 作为实参。正常情况下&#xff0c;后置运算符执行的工作不需要该参数。其唯一目的是将后置函数与前置版本区分开。
class StrBlobPtr {
public:// increment and decrementStrBlobPtr operator&#43;&#43;(int); // postfix operatorsStrBlobPtr operator--(int);// other members as before
};
为了与内置运算符一致&#xff0c;后置运算符应该返回原值&#xff08;递增或递减之前的值&#xff09;&#xff0c;返回形式是值&#xff0c;而不是引用。
// postfix: increment/decrement the object but return the unchanged value
StrBlobPtr StrBlobPtr::operator&#43;&#43;(int) {// no check needed here; the call to prefix increment will do the checkStrBlobPtr ret &#61; *this; // save the current value&#43;&#43;*this; // advance one element; prefix &#43;&#43; checks the incrementreturn ret; // return the saved state
}StrBlobPtr StrBlobPtr::operator--(int) {// no check needed here; the call to prefix decrement will do the checkStrBlobPtr ret &#61; *this; // save the current value--*this; // move backward one element; prefix -- checks the decrementreturn ret; // return the saved state
}
因为 int 形参不会被使用&#xff0c;所以不需要为它命名。
显式调用后置运算符
StrBlobPtr p(a1); // p points to the vector inside a1
p.operator&#43;&#43;(0); // call postfix operator&#43;&#43;
p.operator&#43;&#43;(); // call prefix operator&#43;&#43;
解引用 *
和箭头 ->
运算符通常用于表示迭代器的类和智能指针类中。
箭头运算符必须是成员。解引用运算符不需要是成员&#xff0c;但通常应该也是成员。
class StrBlobPtr {
public:std::string& operator*() const{auto p &#61; check(curr, "dereference past end");return (*p)[curr]; // (*p) is the vector to which this object points}std::string* operator->() const{// delegate the real work to the dereference operatorreturn & this->operator*(); // 返回解引用运算符返回的元素的地址}// other members as before
};
注意&#xff1a;这两个运算符被定义成 const 成员。获取一个元素不需要改变 StrBlobPtr 的状态。
StrBlob a1 &#61; {"hi", "bye", "now"};
StrBlobPtr p(a1); // p points to the vector inside a1
*p &#61; "okay"; // assigns to the first element in a1
cout << p->size() << endl; // prints 4, the size of the first element in a1
cout << (*p).size() << endl; // equivalent to p->size()
对箭头运算符返回值的限定
与大多数其他运算符一样&#xff0c;可以定义 operator*
来执行我们指定的任何操作&#xff0c;尽管这样做不太好。比如&#xff0c;可以定义 operator*
来返回固定值&#xff0c;或打印对象的内容&#xff0c;或者其他。
对于重载箭头&#xff0c;情况并非如此。箭头运算符永远不会失去其成员访问的基本含义。当重载箭头时&#xff0c;可以改变的是箭头从那个对象中获取指定成员。而箭头获取成员的事实不能改变。
当编写 point->mem
时&#xff0c;point 必须是指向类对象的指针&#xff0c;或者是带有重载的 operator->
的类的对象。根据 point 的类型&#xff0c;point->mem
等效于&#xff1a;
(*point).mem; // point is a built-in pointer type
point.operator()->mem; // point is an object of class type
否则代码是错误的。
注意&#xff1a;重载的箭头运算符必须返回一个指向类类型的指针&#xff0c;或者一个定义了自己的箭头运算符的类类型的对象。
【C&#43;&#43; primer】目录