// 重载运算符Op// T1: 返回类型,最好和内置的Op返回类型一样。比如&&运算返回bool,=、+=运算返回引用// t1 t2 ...: 形参列表,1元运算符1个参数,2元运算符2个参数,3元对应3个参数。T1 operatorOp(T1 t1, T2 t2, ...){}
运算符也是函数,形参就是运算对象,所以运算符也可以重载。和一般函数重载的区别是函数名字比较特殊,operator后面紧接运算符组成函数名。用法和普通重载一样。
但是不能为内置类型重载运算符,这些都是已经内部定义了。
除了重载的函数调用运算符operator()之外,其他重载运算符不能含有默认实参。
如果一个运算符函数是成员函数,则它的第一个( 左侧)运算对象绑定到隐式的this指针上。
重载运算符应该继承内置版本的含义,如string的+运算是连接两个string,非常容易被理解。
只能重载已有运算符,不能发明运算符 。
对于运算符重载的参数,不能全部是内置类型参数,至少有一个类成员或者类类型参数。
a + b; // 可以看成是operator+(a, b)// 重载非成员运算符data1 + data2; // 等价operator+(data1, data2); // 等价// 重载成员运算符data1 += data2 ; // 等价,实参列表是(data1,data2)data1.operator+=(data2); // 等价// 这里也隐含了一条信息:重载成员运算符,左侧运算对象必须是类对象,例子如下:strings = "world";string t = s + "!"; // 正确string u ="hi" + s; // 如果+是string的成员,则产生错误T1 operator&&(T2, T3); // 会丢失短路求值属性T1 operator||(T2, T3); // 会丢失短路求值属性// 重载=运算符:应该和内置版本一样返回左侧对象的引用。T1& operator=(T1, T2);// 重载+=复合赋值运算,应该和内置版本一样,先+,再=T1& operator+=(T1, T2);int operator+(int, int) // 错误,不能重载int类型的+号运算符。两个int至少有一个是类类型或者类成员{}
可重载运算符
成员还是非成员?
成员还是非成员?运算符是作为普通函数,还是成员函数好,可以根据下面的准则:
- 成员
- =、[]、()、->,必须是成员。
- +=、&=一般是成员。
- —、++、*解引用等改变实参状态的,一般是成员。
- 非成员
// os: 是非const的引用,因为os会改变状态,且ostream不可复制。 // t: 是const的引用,因为一般不会改变t的内容。 // return: 是ostream的引用,流不可复制。 ostream& operator<<(ostream &os, const T &t); // 正确。 ostream operator<<(ostream &os, const T &t); // 错误:流不可复制,应该返回引用。 ostream operator<<(ostream &os, const T t); // 不建议:没必要拷贝t
ostream &operator<<(ostream &os, const T &t) {
os << t.a << “ “ << t.b << “ “;
// 内部不需要有\n、endl之类的格式化操作。完全可以由用户自行完成。// 重载输出,我们只关注应该输出什么,而不是输出成什么样。os << t.a << "\n" << t.b << "\n" << endl;return os;
}
<a name="6Ddsx"></a># 输入运算符>>```cpp// os: 是非const的引用,因为os会改变状态,且ostream不可复制。// t: 非const引用,因为要对t写入数据嘛。// return: 是ostream的引用,流不可复制。ostream& operator>>(ostream &os, T & t){......// 要考虑流操作失败的情况。// 1、比如读取的类型和t.b不符合,则t.b和t.c的输入都会失败// 2、os到达文件结尾,后续操作会全部失败os >> t.a >> t.b >> t.c;if(os){ // 前面的流操作全部成功// 处理数据// 也可能发现数据错误,我们应该手动设置流的条件状态。// 一般应该使用标准库的标志信息。// eofbit表示到达文件尾、badbit表示流被破坏,一般就用failbitos.setstate(os.rdstate() & os.failbit);}else{ //前面的流操作出现失败,需要处理好t内部数据,可能部分有数据。......}return os;}
算术运算符
对称运算符,一般定义成非成员函数。
// 形参一般都是常量引用,因为不会去修改// 返回是值传递,拷贝一份副本。// 算术运算符一般要成双成对出现,比如重载+,就要重载+=T1 operator+(const T2 &l, const T3 &r){T2 temp = l;temp += r; // 建议使用对应的复合运算符来完成。return temp;}T1 operator+=(const T2 &l, const T3 &r){}T1 operator&&(const T2 &l, const T3 &r){}
关系运算符
一般定义成非成员函数。
// 形参一般都是常量引用,因为不会去修改// 返回类型:一般都是bool,继承内置的。bool operator<(const T2 &l, const T3 &r){bool ret = false;...return ret;}
一般的关系运算符应该完成的工作
- 1、定义顺序关系
- 令其与关联容器中对关键字的要求一致
2、如果类同时也含有==运算符的话,则定义一种关系令其与==保持一致。特别是,如果两个对象是!=的,那么一个对象应该<另外一个 。
==相等运算符
判断两个对象数据是否相等。重载==的设计准则:
对象有比较操作,那就赶紧重载==。
- ==应该能判断数据(成员)是否相同。
- ==应该有传递性。
- ==和!=应该成双成对出现。用其中一个去实现另外一个,只有一个是实现具体比较工作。
- <运算符是被经常使用到的,比如标准库容器的默认使用的关系运算符基本都是<,所以重载operater<非常有必要。 ```cpp
// 1、重载了==一半就要重载!= // 2、其中一个运算符的实现可以用另外一个运算符完成,这样只需要实现其中一个即可。 bool operator==(const T1 &l, const T2& r){ return !(l != r); }
bool operator!=(const T1 &l, const T2& r){
}
<a name="2TdFD"></a># =赋值运算符必须定义为成员函数。重载=运算符的逻辑应该和类的拷贝赋值、移动赋值一样。```cppStrVec& StrVec::operator=(initialized_list<string> il){// 第一步,分配新空间并拷贝元素auto data = alloc_n_copy(il.begin(), il.end());// 第二步,销毁旧数据free();// 第三步,赋值新数据elements = data.first;first_free = cap = data.second;return *this;}T t = {"a", "b", "c"};// ********************************************************// 复合赋值运算符// 作为成员的二元运算符:左侧运算对象绑定到隐式的 this 指针// 假定两个对象表示的是同一本书T& T::operator+=(const T &rhs){......return *this;}
下标运算符
下标运算符,必须是成员函数,内置的索引类型是std::size_t,为了与内置的统一,最好返回值类型也是引用。
// 一般要定义两个版本:// 1、非常量版本int& T::operator[](std::size_t n){}// 2、常量版本const T& T::operator[](std::size_t n) const {}T t;const T ct;auto i = t[0]; // 非常量版本auto ci = ct[0]; // 常量版本
++递增,—递减运算符
应该定义成类的成员。
有前置、后置版本,所以我们也应该重载两个版本。这里有个技巧,一般用前置的版本来实现后置。
从下面的重载实现,我们加深对i++和++i的理解。
class T {public://前置版本T& operator++();T& operator--();//后置版本:int仅用于区分。T operator++(int);T operator--(int);//T p(a1);//p.operator++(0); // 调用后置版本,这个0是为了让编译器知道。//p.operator++(); // 调用前置版本public:int curr;}T& T::operator++(){++curr; //将curr在当前状态下向前移动一个元素return *this;}T& T::operator--(){--curr;return *this;}T T::operator++(int){T ret = *this; //记录当前的值++*this;return ret; //返回之前记录的状态}T T::operator--(int){//此处无须检查有效性,调用前置递减运算时才需要检查T ret = *this; //记录当前的值--*this;return ret; //返回之前记录的状态}
成员访问运算符
->必须是成员,*解引用一般是成员。
class T{string& operator*() const;string& operator->() const;};string& T::operator*() const{}string* T::operator->() const{return & this->operator*(); //*解引用运算来实现->运算。}T *point;(*point).mem; // point 是一个内置的指针类型point.operator()->mem; // point 是类的一个对象
函数调用运算符
必须定义成成员。定义了调用运算符的类的对象,叫函数对象,常常用于泛型算法的实参。lambda是函数对象。
struct T {int operator()(int val) const {return val < 0 ? -val : val;} ;}int i = -42 ;T t;int ui = T(i);/****************Comp函数对象代替lambda******************/struct Comp{Comp(std::size_t sz);bool operator()( const string &s){return s.size() >= m_sz;}std::size_t m_sz;};std::vector<string> words;std::size_t sz = 10;find_if(words.begin(), words.end(), //找出第一个不小于sz长度的word[sz] (const string &s){return s.size() >= sz;});stable_sort(words.begin(), words.end(),Comp(sz)); //用Comp代替lambda。
标准库函数对象
标准库在functional头文件中定义了如下函数对象。
| 算术 | 关系 | 逻辑 |
|---|---|---|
| plus |
equal_to |
logical_and |
| minus |
not_equal_to |
logical_or |
| multiplies |
greater |
logical_not |
| divides |
greater_equal |
|
| modulus |
less |
|
| negate |
less_equal |
plus<int> intAdd; // 可执行int加法的函数对象negate<int> intNegate; // 可对int值取反的函数对象int sum = intAdd(10, 20); // 等价于sum=30sum = intNegate(intAdd(10,20)); // 等价于sum=-30sum = intAdd(10, intNegate(10)); // sum=0/*************************在算法中使用函数对象***************************/// 传入一个临时的函数对象用于执行两个string对象的>比较运算sort(svec.begin(), svec.end(), greater<string>());vector<string*> nameTable; // 指针的vector// 错误:nameTable中的指针彼此之间没有关系,所以<将产生未定义的行为sort(nameTable.begin(), nameTable.end(),[](string*a, string*b){return a < b;});// 正确:标准库规定指针的less是定义良好的sort(nameTable.begin(), nameTable.end(), less<string*>());
function
定义在functional头文件中。是模板,必须提供可调用对象的调用形式(call signature)。
int add(int a, int b){return a + b;}//add函数的调用形式就是int(int, int),或者叫签名。
function描述的是函数对象的类型,只要调用形式(签名)相同,那么在function看来他们就是相同类型的函数对象。
支持的操作
function<T> f; // f是一个用来存储可调用对象的空function,// 这些可调用对象的调用形式应该与函数类型T相同function<T> f(nullptr); // 显式地构造一个空functionfunction<T> f(obj); // 在f中存储可调用对象obj的副本f // 将f作为条件:当f含有一个可调用对象时为真;否则为假f(args) // 调用f中的对象,参数是args// 定义为function<T>的成员的类型result_type // 可调用对象的返回类型。argument_type // 只有一个实参时,就是第一个实参的类型,和first同义。first_argument_type // 第一个实参的类型,如果有的话。second_argument_type // 第二个实参的类型,如果有的话。
操作例子
// 每个可调用对象表示计算器的一种算法。加减乘除等。int add(int a, int b){ return a + b; }; // 函数auto minus = [](int a, int b){ return a - b; }; // lambdastruct multi{ // 函数对象int operator()(int a, int b){ return a * b; }};function<int(int, int)>function<int(int, int)> f1 = add; // 函数指针function<int(int, int)> f3 = minus; // lambdafunction<int(int, int)> f2 = multi(); // 函数对象类的对象cout << f1(4, 2) << endl;cout << f2(4, 2) << endl;cout << f3(4, 2) << endl;// map存储这些不同类型的可调用对象map<string, function<int(int, int)>> binops;binops.insert({"+", add}); // 函数指针binops.insert({"-", minus}); // lambdabinops.insert({"x", multi()}); // 函数对象binops.insert({"%", std::modulus<int>}); // 标准库的函数对象binops.insert({"/", [](int a, int b){ return a / b;}); // 未命名的lambdabinops["+"](10, 5);binops["-"](10, 5);binops["x")(10, 5);binops["/"](10, 5);binops["%"](10, 5);// 需要注意重载函数的情况。int add(int i, int j) { return i + j; };T add(const T&, const T&);binops.insert({"+", add}); // 二义性错误,不知道是哪个addint (*fp)(int, int) = add;binops.insert({"+", fp}) ; // 正确:fp指向一个正确的add版本
类型转换运算符
转换构造函数和类型转换运算符共同定义了类类型转换 ( class-type conversions ),这样的转换有时也被称作用户定义的类型转换 ( user-defined conversions) 。
// type:任意类型,除了void,只要能作为函数的返回类型。// 参数:必须为空// 返回值:不能有返回值// 引用限定:一般是const,不修改转换对象。operator type() const;operator int() const;int operator int() const; // 错误:指定了返回类型operator int(int = 0) const; // 错误:参数列表不为空
class SmallInt {public:// 非显式转换构造函数SmallInt(int i = 0); // 将int转换为SamllInt// 非显式的类型转换运算符operator int() const { return val; } // 将SmallInt转换为intprivate:std::size_t val;}SmallInt::SmallInt(int i = 0):val(i){if (i < 0 || i > 255){error();}}SmallInt si;si = 4; // 首先将4隐式地转换成Smalllnt,然后调用Smallint::operator=si + 3; // 类型转换运算是非explicit时,才正确,否则错误。SmallInt si = 3.14; // 1、内置类型转换将double实参转换成int// 2、调用SmallInt()构造函数si + 3.14; // 1、SmallInt 的类型转换运算符将 si 转换成 int// 2、内置类型转换将所得的值让继续转换成 double
显式、隐式类型转换
class SmallInt {public:// 非显式的类型转换运算符operator int() const { return val; } // 将SmallInt转换为int//显式的类型转换运算符,下面可以看到这两种显式的差别。explicit operator int() const { return val; } // 将SmallInt转换为intprivate:std::size_t val;}si + 3; // 类型转换运算是非explicit时,才正确,否则错误。static_cast<int>(si) + 3; // 类型转换运算是explicit的int i = 42;cin << i; // 如果iostream的类型转换是隐式的,// 则cin会被转换成bool,bool转成int 1// 最后变成:1 << i !!while(std::cin >> value){ // 隐式转换成了bool}
所以我们最好声明为显式的,也有例外,编译器会用隐式转换代替显式转换:
- if、while、do的条件判断
- for中的条件判断
- 逻辑运算符的运算对象
- 条件运算符的条件表达式
重载运算符的函数匹配
重载运算符要时刻记住,有内置版本的运算符,如果你的类型能隐式转换成内置类型(类型转换),那么类对象和内置类型之间发生运算,就不知道要用哪个版本的运算符了。看下面例子。
class Smallint {friend Smallint operator+(const Smallint& , const Smallint&) ;public:Smallint(int = 0); // int转Smallintoperator int() const { return val; } // Samllint转intprivate:std::size_t val;}Smallint s1, s2;Smallint s3 = s1 + s2; // 使用重载operator+int i = s3 + 0; // 二义性错误// 有两个版本的+运算符(重载的和内置的+(int, int))// s3能转成int,用内置的+// 0能转成Smallint,用重载的。
所以,我们对同一个类既提供了转换目标是算术类型的类型转换,也提供了重载的运算符, 则将会遇到重载运算符与内置运算符的二义性问题。
