1. // 重载运算符Op
  2. // T1: 返回类型,最好和内置的Op返回类型一样。比如&&运算返回bool,=、+=运算返回引用
  3. // t1 t2 ...: 形参列表,1元运算符1个参数,2元运算符2个参数,3元对应3个参数。
  4. T1 operatorOp(T1 t1, T2 t2, ...)
  5. {
  6. }

运算符也是函数,形参就是运算对象,所以运算符也可以重载。和一般函数重载的区别是函数名字比较特殊,operator后面紧接运算符组成函数名。用法和普通重载一样。
但是不能为内置类型重载运算符,这些都是已经内部定义了。
除了重载的函数调用运算符operator()之外,其他重载运算符不能含有默认实参。
如果一个运算符函数是成员函数,则它的第一个( 左侧)运算对象绑定到隐式的this指针上。
重载运算符应该继承内置版本的含义,如string的+运算是连接两个string,非常容易被理解。
只能重载已有运算符,不能发明运算符 。
对于运算符重载的参数,不能全部是内置类型参数,至少有一个类成员或者类类型参数。

  1. a + b; // 可以看成是operator+(a, b)
  2. // 重载非成员运算符
  3. data1 + data2; // 等价
  4. operator+(data1, data2); // 等价
  5. // 重载成员运算符
  6. data1 += data2 ; // 等价,实参列表是(data1,data2)
  7. data1.operator+=(data2); // 等价
  8. // 这里也隐含了一条信息:重载成员运算符,左侧运算对象必须是类对象,例子如下:
  9. strings = "world";
  10. string t = s + "!"; // 正确
  11. string u ="hi" + s; // 如果+是string的成员,则产生错误
  12. T1 operator&&(T2, T3); // 会丢失短路求值属性
  13. T1 operator||(T2, T3); // 会丢失短路求值属性
  14. // 重载=运算符:应该和内置版本一样返回左侧对象的引用。
  15. T1& operator=(T1, T2);
  16. // 重载+=复合赋值运算,应该和内置版本一样,先+,再=
  17. T1& operator+=(T1, T2);
  18. int operator+(int, int) // 错误,不能重载int类型的+号运算符。两个int至少有一个是类类型或者类成员
  19. {
  20. }

可重载运算符

image.png

成员还是非成员?

成员还是非成员?运算符是作为普通函数,还是成员函数好,可以根据下面的准则:

  • 成员
    • =、[]、()、->,必须是成员。
    • +=、&=一般是成员。
    • —、++、*解引用等改变实参状态的,一般是成员。
  • 非成员
    • 对称性的运算比如+、-、==等关系运算符。
    • 输出<<、输入>>必须是非成员。自定义类的输入输出,将其定义成友元函数。

      输出运算符<<

      I/O标准库分别使用>>执行输入、<<执行输出操作。
      输出<<、输入>>必须是非成员。自定义类的输入输出,将其定义成友元函数。 ```cpp

// 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 << “ “;

  1. // 内部不需要有\n、endl之类的格式化操作。完全可以由用户自行完成。
  2. // 重载输出,我们只关注应该输出什么,而不是输出成什么样。
  3. os << t.a << "\n" << t.b << "\n" << endl;
  4. return os;

}

  1. <a name="6Ddsx"></a>
  2. # 输入运算符>>
  3. ```cpp
  4. // os: 是非const的引用,因为os会改变状态,且ostream不可复制。
  5. // t: 非const引用,因为要对t写入数据嘛。
  6. // return: 是ostream的引用,流不可复制。
  7. ostream& operator>>(ostream &os, T & t){
  8. ......
  9. // 要考虑流操作失败的情况。
  10. // 1、比如读取的类型和t.b不符合,则t.b和t.c的输入都会失败
  11. // 2、os到达文件结尾,后续操作会全部失败
  12. os >> t.a >> t.b >> t.c;
  13. if(os){ // 前面的流操作全部成功
  14. // 处理数据
  15. // 也可能发现数据错误,我们应该手动设置流的条件状态。
  16. // 一般应该使用标准库的标志信息。
  17. // eofbit表示到达文件尾、badbit表示流被破坏,一般就用failbit
  18. os.setstate(os.rdstate() & os.failbit);
  19. }
  20. else{ //前面的流操作出现失败,需要处理好t内部数据,可能部分有数据。
  21. ......
  22. }
  23. return os;
  24. }

算术运算符

对称运算符,一般定义成非成员函数。

  1. // 形参一般都是常量引用,因为不会去修改
  2. // 返回是值传递,拷贝一份副本。
  3. // 算术运算符一般要成双成对出现,比如重载+,就要重载+=
  4. T1 operator+(const T2 &l, const T3 &r){
  5. T2 temp = l;
  6. temp += r; // 建议使用对应的复合运算符来完成。
  7. return temp;
  8. }
  9. T1 operator+=(const T2 &l, const T3 &r){
  10. }
  11. T1 operator&&(const T2 &l, const T3 &r){
  12. }

关系运算符

一般定义成非成员函数。

  1. // 形参一般都是常量引用,因为不会去修改
  2. // 返回类型:一般都是bool,继承内置的。
  3. bool operator<(const T2 &l, const T3 &r){
  4. bool ret = false;
  5. ...
  6. return ret;
  7. }

一般的关系运算符应该完成的工作

  • 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){

}

  1. <a name="2TdFD"></a>
  2. # =赋值运算符
  3. 必须定义为成员函数。重载=运算符的逻辑应该和类的拷贝赋值、移动赋值一样。
  4. ```cpp
  5. StrVec& StrVec::operator=(initialized_list<string> il){
  6. // 第一步,分配新空间并拷贝元素
  7. auto data = alloc_n_copy(il.begin(), il.end());
  8. // 第二步,销毁旧数据
  9. free();
  10. // 第三步,赋值新数据
  11. elements = data.first;
  12. first_free = cap = data.second;
  13. return *this;
  14. }
  15. T t = {"a", "b", "c"};
  16. // ********************************************************
  17. // 复合赋值运算符
  18. // 作为成员的二元运算符:左侧运算对象绑定到隐式的 this 指针
  19. // 假定两个对象表示的是同一本书
  20. T& T::operator+=(const T &rhs){
  21. ......
  22. return *this;
  23. }

下标运算符

下标运算符,必须是成员函数,内置的索引类型是std::size_t,为了与内置的统一,最好返回值类型也是引用。

  1. // 一般要定义两个版本:
  2. // 1、非常量版本
  3. int& T::operator[](std::size_t n){
  4. }
  5. // 2、常量版本
  6. const T& T::operator[](std::size_t n) const {
  7. }
  8. T t;
  9. const T ct;
  10. auto i = t[0]; // 非常量版本
  11. auto ci = ct[0]; // 常量版本

++递增,—递减运算符

应该定义成类的成员。
有前置、后置版本,所以我们也应该重载两个版本。这里有个技巧,一般用前置的版本来实现后置。
从下面的重载实现,我们加深对i++和++i的理解。

  1. class T {
  2. public:
  3. //前置版本
  4. T& operator++();
  5. T& operator--();
  6. //后置版本:int仅用于区分。
  7. T operator++(int);
  8. T operator--(int);
  9. //T p(a1);
  10. //p.operator++(0); // 调用后置版本,这个0是为了让编译器知道。
  11. //p.operator++(); // 调用前置版本
  12. public:
  13. int curr;
  14. }
  15. T& T::operator++(){
  16. ++curr; //将curr在当前状态下向前移动一个元素
  17. return *this;
  18. }
  19. T& T::operator--(){
  20. --curr;
  21. return *this;
  22. }
  23. T T::operator++(int){
  24. T ret = *this; //记录当前的值
  25. ++*this;
  26. return ret; //返回之前记录的状态
  27. }
  28. T T::operator--(int){
  29. //此处无须检查有效性,调用前置递减运算时才需要检查
  30. T ret = *this; //记录当前的值
  31. --*this;
  32. return ret; //返回之前记录的状态
  33. }

成员访问运算符

->必须是成员,*解引用一般是成员。

  1. class T{
  2. string& operator*() const;
  3. string& operator->() const;
  4. };
  5. string& T::operator*() const{
  6. }
  7. string* T::operator->() const{
  8. return & this->operator*(); //*解引用运算来实现->运算。
  9. }
  10. T *point;
  11. (*point).mem; // point 是一个内置的指针类型
  12. point.operator()->mem; // point 是类的一个对象

函数调用运算符

必须定义成成员。定义了调用运算符的类的对象,叫函数对象,常常用于泛型算法的实参。lambda是函数对象

  1. struct T {
  2. int operator()(int val) const {
  3. return val < 0 ? -val : val;
  4. } ;
  5. }
  6. int i = -42 ;
  7. T t;
  8. int ui = T(i);
  9. /****************Comp函数对象代替lambda******************/
  10. struct Comp{
  11. Comp(std::size_t sz);
  12. bool operator()( const string &s){
  13. return s.size() >= m_sz;
  14. }
  15. std::size_t m_sz;
  16. };
  17. std::vector<string> words;
  18. std::size_t sz = 10;
  19. find_if(words.begin(), words.end(), //找出第一个不小于sz长度的word
  20. [sz] (const string &s){
  21. return s.size() >= sz;
  22. });
  23. 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
  1. plus<int> intAdd; // 可执行int加法的函数对象
  2. negate<int> intNegate; // 可对int值取反的函数对象
  3. int sum = intAdd(10, 20); // 等价于sum=30
  4. sum = intNegate(intAdd(10,20)); // 等价于sum=-30
  5. sum = intAdd(10, intNegate(10)); // sum=0
  6. /*************************在算法中使用函数对象***************************/
  7. // 传入一个临时的函数对象用于执行两个string对象的>比较运算
  8. sort(svec.begin(), svec.end(), greater<string>());
  9. vector<string*> nameTable; // 指针的vector
  10. // 错误:nameTable中的指针彼此之间没有关系,所以<将产生未定义的行为
  11. sort(nameTable.begin(), nameTable.end(),
  12. [](string*a, string*b){
  13. return a < b;
  14. });
  15. // 正确:标准库规定指针的less是定义良好的
  16. sort(nameTable.begin(), nameTable.end(), less<string*>());

function

定义在functional头文件中。是模板,必须提供可调用对象的调用形式(call signature)。

  1. int add(int a, int b){
  2. return a + b;
  3. }
  4. //add函数的调用形式就是int(int, int),或者叫签名。

function描述的是函数对象的类型,只要调用形式(签名)相同,那么在function看来他们就是相同类型的函数对象。

支持的操作

  1. function<T> f; // f是一个用来存储可调用对象的空function,
  2. // 这些可调用对象的调用形式应该与函数类型T相同
  3. function<T> f(nullptr); // 显式地构造一个空function
  4. function<T> f(obj); // 在f中存储可调用对象obj的副本
  5. f // 将f作为条件:当f含有一个可调用对象时为真;否则为假
  6. f(args) // 调用f中的对象,参数是args
  7. // 定义为function<T>的成员的类型
  8. result_type // 可调用对象的返回类型。
  9. argument_type // 只有一个实参时,就是第一个实参的类型,和first同义。
  10. first_argument_type // 第一个实参的类型,如果有的话。
  11. second_argument_type // 第二个实参的类型,如果有的话。

操作例子

  1. // 每个可调用对象表示计算器的一种算法。加减乘除等。
  2. int add(int a, int b){ return a + b; }; // 函数
  3. auto minus = [](int a, int b){ return a - b; }; // lambda
  4. struct multi{ // 函数对象
  5. int operator()(int a, int b){ return a * b; }
  6. };
  7. function<int(int, int)>
  8. function<int(int, int)> f1 = add; // 函数指针
  9. function<int(int, int)> f3 = minus; // lambda
  10. function<int(int, int)> f2 = multi(); // 函数对象类的对象
  11. cout << f1(4, 2) << endl;
  12. cout << f2(4, 2) << endl;
  13. cout << f3(4, 2) << endl;
  14. // map存储这些不同类型的可调用对象
  15. map<string, function<int(int, int)>> binops;
  16. binops.insert({"+", add}); // 函数指针
  17. binops.insert({"-", minus}); // lambda
  18. binops.insert({"x", multi()}); // 函数对象
  19. binops.insert({"%", std::modulus<int>}); // 标准库的函数对象
  20. binops.insert({"/", [](int a, int b){ return a / b;}); // 未命名的lambda
  21. binops["+"](10, 5);
  22. binops["-"](10, 5);
  23. binops["x")(10, 5);
  24. binops["/"](10, 5);
  25. binops["%"](10, 5);
  26. // 需要注意重载函数的情况。
  27. int add(int i, int j) { return i + j; };
  28. T add(const T&, const T&);
  29. binops.insert({"+", add}); // 二义性错误,不知道是哪个add
  30. int (*fp)(int, int) = add;
  31. binops.insert({"+", fp}) ; // 正确:fp指向一个正确的add版本

类型转换运算符

转换构造函数和类型转换运算符共同定义了类类型转换 ( class-type conversions ),这样的转换有时也被称作用户定义的类型转换 ( user-defined conversions) 。

  1. // type:任意类型,除了void,只要能作为函数的返回类型。
  2. // 参数:必须为空
  3. // 返回值:不能有返回值
  4. // 引用限定:一般是const,不修改转换对象。
  5. operator type() const;
  6. operator int() const;
  7. int operator int() const; // 错误:指定了返回类型
  8. operator int(int = 0) const; // 错误:参数列表不为空
  1. class SmallInt {
  2. public:
  3. // 非显式转换构造函数
  4. SmallInt(int i = 0); // 将int转换为SamllInt
  5. // 非显式的类型转换运算符
  6. operator int() const { return val; } // 将SmallInt转换为int
  7. private:
  8. std::size_t val;
  9. }
  10. SmallInt::SmallInt(int i = 0)
  11. :val(i){
  12. if (i < 0 || i > 255){
  13. error();
  14. }
  15. }
  16. SmallInt si;
  17. si = 4; // 首先将4隐式地转换成Smalllnt,然后调用Smallint::operator=
  18. si + 3; // 类型转换运算是非explicit时,才正确,否则错误。
  19. SmallInt si = 3.14; // 1、内置类型转换将double实参转换成int
  20. // 2、调用SmallInt()构造函数
  21. si + 3.14; // 1、SmallInt 的类型转换运算符将 si 转换成 int
  22. // 2、内置类型转换将所得的值让继续转换成 double

显式、隐式类型转换

  1. class SmallInt {
  2. public:
  3. // 非显式的类型转换运算符
  4. operator int() const { return val; } // 将SmallInt转换为int
  5. //显式的类型转换运算符,下面可以看到这两种显式的差别。
  6. explicit operator int() const { return val; } // 将SmallInt转换为int
  7. private:
  8. std::size_t val;
  9. }
  10. si + 3; // 类型转换运算是非explicit时,才正确,否则错误。
  11. static_cast<int>(si) + 3; // 类型转换运算是explicit的
  12. int i = 42;
  13. cin << i; // 如果iostream的类型转换是隐式的,
  14. // 则cin会被转换成bool,bool转成int 1
  15. // 最后变成:1 << i !!
  16. while(std::cin >> value){ // 隐式转换成了bool
  17. }

所以我们最好声明为显式的,也有例外,编译器会用隐式转换代替显式转换:

  • if、while、do的条件判断
  • for中的条件判断
  • 逻辑运算符的运算对象
  • 条件运算符的条件表达式

重载运算符的函数匹配
重载运算符要时刻记住,有内置版本的运算符,如果你的类型能隐式转换成内置类型(类型转换),那么类对象和内置类型之间发生运算,就不知道要用哪个版本的运算符了。看下面例子。

  1. class Smallint {
  2. friend Smallint operator+(const Smallint& , const Smallint&) ;
  3. public:
  4. Smallint(int = 0); // int转Smallint
  5. operator int() const { return val; } // Samllint转int
  6. private:
  7. std::size_t val;
  8. }
  9. Smallint s1, s2;
  10. Smallint s3 = s1 + s2; // 使用重载operator+
  11. int i = s3 + 0; // 二义性错误
  12. // 有两个版本的+运算符(重载的和内置的+(int, int))
  13. // s3能转成int,用内置的+
  14. // 0能转成Smallint,用重载的。

所以,我们对同一个类既提供了转换目标是算术类型的类型转换,也提供了重载的运算符, 则将会遇到重载运算符与内置运算符的二义性问题。