C++ Primer:Sec 14:重载与类型转换

Sec14 重载运算与类型转换

当运算符作用于类类型的运算对象时,可以通过运算符重载重新定义该运算符的含义。

14.1 基本概念

重载运算符是具有特殊名字的函数!名字由关键字operator和其后要定义的运算符号共同组成。重载的运算符也包含返回类型、参数列表以及函数体
重载运算符函数的参数数量与该运算符作用的运算对象一样多了。
如果一个运算符函数是成员函数,则它的第一个左侧运算对象绑定到隐式的this指针上。因此,成员运算符函数的显示参数数量比运算符的运算对象总数少一个。

// 错误,不能为int重定义内置的运算符
int operator+(int, int);
  • 直接调用一个重载的运算符函数

    // 一个非成员运算符函数的等价调用
    data1 + data2;				// 普通的表达式
    operator+(data1, data2);	// 等价的函数调用
    
  • 某些运算符不应该被重载
    关于运算对象求职顺序的规则无法应用到重载的运算符上。比如,逻辑与,逻辑或,逗号运算符
    因为上述运算符的重载版本无法保留求值顺序,和/或 短路求值属性,故不建议重载他们。

  • 使用内置类型一致的含义

  • 赋值和复合赋值运算符
    =运算符应该返回它左侧运算对象的一个引用!
    重载的赋值运算应该继承而非违背其内置版本的含义

  • 选择成员或者非成员
    当我们定义重载运算符时,必须首先决定是将其声明为类的成员函数还是声明为一个普通的非成员函数。
    规则:

    • = [] -> 必须是成员
    • 复合赋值运算符一般来说应该是成员,但并非是必须
    • 改变对象状态的运算符或者与给定类型密切相关的运算符,如算数,相等性,关系和位运算符等。通常应该是成员
    • 具有对称性的运算符可能转换任意一端的运算对象。例如算数,相等性,关系和位运算符。通常应该是普通的非成员函数

    我们把运算符定义成成员函数时,它的左侧运算对象必须是运算符所属类的一个对象!
    但如果不是成员类型,则可以任意位置!
    比如string,只要有一个是string类型就行,因为都等价于operator+函数调用!

14.2 输入输出运算符

  • <<
    第一个形参是ostream对象的引用。
    第二个形参一般是一个常量的引用,是我们想要打印的类的类型。
    为了与常规一致,operator<<一般要返回它的ostream形参

    ostream &operator<<(ostream &os, const Sales_data &item) {
        os << item.isbn() << " " << item.units_sold << " "
            << item.revenue << " " << item.avg_price();
      	return os;
    }
    
    • 输出运算符尽量减少格式化操作
      比如像内置的<<一样,不要打印换行符!

    • 输入输出运算符必须是非成员函数
      否则,左侧运算对象将是我们的类的一个对象

      Sales_data data;
      data << cout;	// 如果operator<<是Sales_data的成员
      

      显然,上述代码很不好懂

  • >>

    istream &operator>>(istream &is, Sales_data &item) {
        double price;
        is >> item.bookNo >> item.units_sold >> price;
        if(is)	// 检查输入是否成功
        	item.revenue = item.units_sold * price;
        else 
            item = Sales_data();
       	return is;
    }
    
    • 输入时的错误
      • 流含有错误类型的数据
      • 文件末尾或者其他流错误
  • 算术和关系运算符

    Sales_data
    operator+(const Sales_data &lhs, const Sales_data &rhs) {
        Sales_data sum = lhs;
        sum += rhs;
        return sum;
    }
    
  • 相等运算符

    bool operator==(const Sales_data &lhs, const Sales_data &rhs) {
        return lhs.isbn() == rhs.isbn() &&
            	lhs.units_sold == rhs.units_sold &&
            	lhs.revenue == rhs.revenue;
    }
    bool operator!=(const Sales_data &lhs, const Sales)data &rhs) {
        return !(lhs == rhs);
    }
    
  • 关系运算符
    通常情况下,关系运算符应该:

    1. 定义顺序关系
    2. 若类同时含有==运算符,则定义一种关系令其与==保持一致
  • 赋值运算符

    class StrVec {
    public:
        StrVec &operator=(std::initializer_list<std::string>);
        // 其他成员定义....
    };
    
    StrVec &StrVec::operator=(initializer_list<std::string> il) {
        // alloc_n_copy分配内存空间并从给定范围内拷贝元素
        auto data = alloc_n_copy(il.begin(), il.end());
        free();
        elements = data.first;
        first_free = cap = data.second;
        return *this;
    }
    
    • 复合赋值运算符
      复合赋值运算符不非得是类的成员。但还是倾向于在类里面定义。也得返回左侧运算对象的引用

      Sales_data& Sales_data::operator+=(const Sales_data &rhs) {
          units_sold += rhs.units_sold;
          revenue += rhs.revenue;
          return *this;
      }
      
  • [] 下标运算符

    class StrVec {
    public:
        std::string& operator[](std::size_t n)
        	{ return elements[n]; }
        const std::string& operator[](std::size_t n) const
       		{ return elemenets[n]; }
    private:
        std::string *elements;
    }
    
  • ++ --递增和递减运算符
    并不要求递增和递减运算符必须是类的成员
    但因为它们改变的正好是操作对象的状态,所以建议将其设为成员函数

    • 定义前置递增/递减运算符

      class StrBlobPtr {
      public:
          StrBlobPtr& operator++();
          StrBlobPtr& operator--();
      }
      

      为了与内置版本保持一致,前置运算符应该返回递增或者递减后的对象引用。

      StrBlobPtr& StrBlobPtr::operator++() {
          check(curr, "increment past end of StrBlobPtr.");
          ++curr;
          return *this;
      }
      StrBlobPtr& StrBlobPtr::operator--() {
      	--curr;
          check(curr, "decrement past begin of StrBlobPtr");
          return *this;
      }
      
    • 区分前置和后置运算符
      普通的重载形式无法区分这俩种情况。
      为解决这个问题,后置版本接受一个额外的(不被使用的)int类型的形参。当使用后置运算符时,编译器为这个形参提供一个值为0的形参

      class StrBlobPtr {
      public:
          StrBlobPtr operator++(int);
          StrBlobPtr operator--(int);
      };
      

      为与内置版本保持一致,后置运算符应该返回对象的原值,返回的形式是一个值而非引用

      StrBlobPtr StrBlobPtr::operator++(int) {
          StrBlobPtr ret = *this;
          ++*this;	// 使用定义的前置++
          return ret;
      }
      StrBlobPtr StrBlobPtr::operator--(int) {
          StrBlobPtr ret = *this;
          --*this;	// 使用定义的前置++
          return ret;
      }
      
      • 显示调用前置后置

        StrBlobPtr p(a1);
        p.operator++(0);
        p.operator++();
        
  • -> *成员访问运算符

    class StrBlobPtr {
    public:
        std::string& operator*() const {
            auto p = check(curr, "dereference past end.");
            return (*p)[curr];
        }
        std::string* operator->() const {
            return  &this->operator*();
        }
    }
    
    • 箭头运算符返回值的限定:
      永远不能丢掉成员访问这个最基本的定义,当重载箭头的时候,可以改变的是箭头从哪个对象中获取成员,而箭头获取成员这一事实则永远不变
      对于形如point->mem的表达式来说,point必须是指向类对象的指针或者是一个重载了operator->类的对象。根据point类型的不同,point->mem分别等价于:

      (*point).mem;			// 内置指针
      point.operator()->mem;	// point是一个类的对象
      
      • 若point是指针,则应用内置的箭头运算符
      • 若point是定义了->的类,则我们使用point.operator->()的结果来获取mem。
  • 函数调用运算符
    如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象。因为这样的类同时可以存储状态,所以比普通函数相比更加灵活

    struct absInt {
      	int operator()(int val) const {
            return val < 0 ? -val : val;
        }  
    };
    // 使用
    int i = -42;
    absInt absObj;
    int ui = absObj(i);
    

    函数调用运算符必须是成员函数!

    如果类定义了调用运算符,则该类的对象称为函数对象(function object)

    • class PrintString {
      public:
          PrintString(ostream &o = cout, char c = ' '):
          	os(o), sep(c) { }
          void operator() (const string &s) const { os << s << sep; }
      private:
          ostream &os;
          char sep;
      };
      // 例子:
      PrintString printer;
      printer(s);
      PrintString errors(cerr, '\n');
      errors(s);
      

      函数对象可以作为泛型算法的实参。

    • lambda是函数对象
      当我们编写了一个lambda后,编译器将该表达式翻译成一个未命名类的未命名对象,在lambda表达式产生的类中含有一个重载的函数调用运算符

      stable_sort(words.begin()., words.end(),
                 [](const string &a, const string &b)
                  {return a.size() < b.size();});
      // 等价于
      class ShorterString {
      public:
          bool operator() (const string &s1, const string &s2) const
          	{ return s1.size() < s2.size(); }
      };
      // 调用
      stable_sort(words.begin(), words.end(), ShroterString());
      
      • 表示lambda及相应捕获行为的类
        当一个lambda表达式通过引用捕获变量的时候,将由程序负责福报lambda执行时引用所引用的对象确实存在,编译器可以直接使用该引用而无需在lambda产生的类中将其存储为数据成员。
    • 标准库定义的函数对象

      • 例如:
        plus类定义了一个函数调用运算符用于对一堆运算对象进行+的caozuo
        modulus类定义了%
        equal_to定义了==

        plus<string>;
        plus<int>;
        negate<int>;
        // 使用
        plus<int> intAdd;
        sum = intAdd(10, 10)
        
      • 标准库对象

        // 算术
        plus<Type>; minus<Type>; multiplies<Type>; divides<Type>; modulus<Type>; negate<Type>;
        // 关系
        equal_to<Type>; not_equal<Type>; greater<Type>; greater_equal<Type>; less<Type>; less_equal<Type>;
        // 逻辑
        logical_and<Type>; logical_or<Type>; logical_not<Type>;
        

        使用例子:

        sort(svec.begin(), svec.end(), greater<string>());
        

        注意,标准库规定其函数对象对于指针同样使用!

        vector<string *> nameTable;
        // 错误!
        sort(nameTable.begin(), nameTable.end(),
            [](string *a, string *b) { return a < b; });
        // 正确
        sort(nameTable.begin(), nameTable.end(),
            less<string*>());
        
    • 可调用对象与function
      例子:构造一个计算器:使用函数表来存储这些可调用对象的指针

      // 普通函数
      int add(int i, int j) { return i+j; }
      // lambda,产生一个未命名的函数对象类
      auto mod = [](int i, int j) { return i % j; };
      // 函数对象类
      struct divide {
          int operator() (int denominator, int divisor) {
              return denominator / divisor;
          }
      }
      map<string, int(*)(int, int)> binops;
      // 添加
      binops.insert({"+", add});
      // 错误!mod不是一个函数指针
      biops.insert({"%", mod});
      
      • 标准库function类型
        定义在头文件

        function<T> f;
        function<T> f(nullptr);
        function<T> f(obj);	// f中存储可调用对象obj的副本
        f;	// 可调用则真,否则假
        f(args);	// 调用
        // 参数
        result_type;
        argument_type;
        first_argument_type;
        second_argument_type;
        

        例子:

        function<int(int, int)> f1 = add;
        function<int(int, int)> f2 = divide();	// 函数对象类的对象
        function<int(int, int)> f3 = // ... lambda函数
        

        则可以重新定义map

        map<string, function<int(int, int)>> binops = {
            {"+", add},		// 函数指针
            {"-", std::minus<int>()},	// 标准库函数对象
            {"/", divide()},	// 用户定义的函数对象
            {"*", [](int i, int j) {return i*j;}},
            {"%", mod}		// 命名了的lambda对象
        };
        

        我们不能直接将重载函数的名字存入function类型的对象中!
        如果想解决二义性,则需要存储函数指针!而非函数名字。也可以用lambda来消除二义性

    14.9 重载、类型转换与运算符

    类型转换运算符 conversion operator 是类的一种特殊成员函数,它负责将一个类类型的值转换为其他类型。
    operator type() const;

    type表示某种类型,类型转换运算符可以面向任意类型进行定义。只要该类型可以作为函数的返回类型。
    类型转换运算符既没有显示的返回类型,也没有形参,必须定义为类的成员函数。而且通常不应该改变转换对象的内容,故通常定义为const成员!

    例子:

    class SmallInt {
    public:
        SmallInt(int i = 0) : val(i)
        {
            if (i < 0 || i >> 255)
                throw std::out_of_range("Bad Small Int value!");
        }
        operator int() const { return val; }
    private:
        std::size_t val;
    }
    SmallInt si;
    si = 4;	// 隐式转换为SmallInt,然后调用SmallInt::operator=
    

    注意:避免过度使用类型转换函数!

posted @ 2022-12-21 11:19  M1kanN  阅读(31)  评论(0编辑  收藏  举报