重载运算符

一、概述:
重载运算符参数数量一般和运算符作用的运算对象数量一样多,作为成员函数的重载运算符其显式参数数量比运算对象总数少1个。
重载运算符本质是一次函数调用,求值顺序一般不会保留,所以不应该重载逻辑与、或以及逗号和取地址运算符(&& || , &)。
算术或位运算最好配套其符合赋值运算符。

成员or非成员:
(1)成员:有些必须是,还有些改变对象状态,或是与给定类型密切相关——(= [ ] ( ) -> 必须是成员) (++ - - *) (复合赋值一般是成员,与赋值不同)——要求左侧对象是类类型。
(2)非成员:具有对称性的运算符,它有可能对任意一端的运算对象进行转换,所以一般是非成员——(算术、相等、关系、位运算符) (输入、输出运算符必须是非成员,因左侧不能是我们定义的类对象)——要求至少有一个运算对象是类类型,记得声明成类的友元。
二、具体运算符重载:
1.输入输出运算符的重载(非成员):
(1)输出运算符:

class T //后面的例子如无特殊说明都是用这个class
{
  friend ostream& operator<<(ostream&, const T&);
  //..
public:
  T() = default;
  T(int i1, string s1) : i(i1), s(s1) {}
  T(const T& t) : i(t.i), s(t.s) {}
  T(T&& t) noexpect : i(t.i),s(t.s) {}
  //..
private:
  int i;
  string s;  
};
//参数1:引用——因为无法copy ostream对象,非常量——因为向其写入内容会改变状态
//参数2:引用——避免copy,常量——通常输出不会改变其内容
ostream& operator<<(ostream& os, T& t)
{
  os << t.i << " " << t.s;//输出运算符尽量减少格式化控制的使用,应该将输出的格式控制交给用户
  return os; 
}

(2)输入运算符:

class T
{
  friend istream& operator>>(istream&, T&);
  //..
};
//参数2:非常量引用——要存入读取的数据。
istream& operator>>(istream& is, T& t)
{
  is >> t.i >> t.s;
  if(is)
  {
    //操作
  }
  else   //输入运算符要检查输入流的合法性,这样才能确保输入的对象被合法使用,
  {
    t = T();     //如果发生输入错误,输入运算符要负责将对象置为合法状态!
  }
  return is;
}  

2.算术运算符与复合赋值运算符:
(1)复合赋值运算符:一般作为类的成员(非必须)

class T
{
  //..
public:
  T& operator+=(const T& t) ;//返回左侧对象引用,显式参数只有一个
  //..
};
T& T::operator+=(const T& t)
 {
    i += t.i;
    s += t.s;
    return *this;
  }

(2)算术运算符:必须为非成员,两侧运算对象对称,允许对其左侧或右侧对象进行转换,一般不需改变运算对象的状态,形参为常量引用,返回运算后局部对象的拷贝。定义算术运算符一般由与其配套的复合赋值运算符来实现比较简单。

class T
{
  friend T operator+(const T& lhs, const T& rhs);
  //..
};  
T operator+(const T& lhs, const T& rhs)
{
  T temp = lhs;
  lhs += rhs; //调用复合赋值运算符
  return temp;
}

3.关系运算符(==,!=, <):非成员,由对称性允许某一端进行类型转换,返回bool类型,参数是const引用

class T
{
  //!=(如果需要>)应该由已定义的运算符实现
  //定义这类运算符必须确保在逻辑上有可靠的定义,且相互之间不应该冲突,
  //否则可能就是画蛇添足
  friend bool operator==(const T& lhs, const T& rhs);
  friend bool operator!=(const T& lhs, const T& rhs);
  friend bool operator<(const T& lhs, const T& rhs);
  //..
};
bool operator==(const T& lhs, const T& rhs)
{
  return lhs.i == rhs.i && 
         lhs.s == rhs.s;
}
bool operator!=(const T& lhs, const T& rhs)
{
  return !(lhs == rhs);
}
bool operator<(const T& lhs, const T& rhs)
{
  if (rhs.s < lhs.s || rhs.s == lhs.s && rhs.i <= lhs.i)
    return false;
  else
    return true;
}

4.赋值运算符:必须为成员,返回类类型对象的引用,参数形式有多种

class T
{
  //..
public:
  T& operator=(const T& rhs);    //拷贝赋值,额外参数须有默认实参
  T& operator=(T&& rhs) noexcept;//移动赋值,额外参数须有默认实参,
                                 //不抛出异常须声明为noexcept
  T& operator=(/*其他类型参数*/); //如vector类可接受一个
                                 //initializer_list类型对象
};
T& T::operator=(const T& rhs)
{
  i = rhs.i;
  s = rhs.s;
  return *this;
}
T& T::operator=(T&& rhs) noexcept
{
  i = rhs.i;
  s = rhs.s;
  return *this;
}

5.下标运算符:必须是成员函数,返回所访问元素类型的引用,接受一个序号参数(size_t),一般同时定义常量和非常量版本。

class T //假设T含有指向一个int数组首元素的指针
{
    //..
public:
    T(int* arr, size_t i) : parr(arr), length(i) {}
    int& operator[](size_t n);
    const int& operator[](size_t n) const;
    //..
private:
    int* parr; //指向数组首元素的指针
    size_t length; //数组长度
};
int& T::operator[](size_t n)
{
    if (n < 0 || n >= length)
        throw invalid_argument("n out of range!");
    return parr[n];
}
const int& T::operator[](size_t n) const
{
    if (n < 0 || n >= length)
        throw invalid_argument("n out of range!");
    return parr[n];
}
//应用:
int arr[] = { 1,2,3,4 };
T t(arr,4);
t[2];//调用非常量版的operator[]

6.递增/递减运算符:改变类对象的状态,一般定义成类的成员,需要区分前置版本和后置版本。

class T
{
  //..
public:
  T& operator++();//前置版本返回调用对象的引用(已经递增),
                //因为对象调用前置版本得到一个左值。
  T operator++(int);//接受一个int参数以便在显式调用时与前置版本区分,
                  //参数无实际意义,后置版本返回的是调用对象的副本(未递增),
                  //因为调用后置版本得到一个右值,不能返回其引用。
  //..
};
T& T::operator++()
{
  ++i;
  return *this;
}
T T::operator++(int) //调用后置版本时,记得传入一个int参数
{
  T ret = *this;
  ++*this;//调用前置版本自增运算符
  return ret;
}

7.解引用运算符与箭头运算符:箭头运算符必须是类成员,解引用运算符一般也定义为类成员。箭头运算符接受一个对象和一个成员名,对对象解引用以获取成员。其实箭头操作符是一元操作符,没有显式形参(而且是类成员,唯一隐式形参是this)。->的右操作数不是表达式,而是对应类成员的一个标识符,由编译器处理获取成员工作(编译器对重载箭头操作符所做的事情,比其它重载操作符要多,这里也正是复杂的地方)。
对重载箭头的返回值的约束:
调用箭头运算符的对象必须是指向类对象的指针或重载了箭头运算符的类的对象。重载箭头操作符必须返回指向类类型的指针,或者返回定义了自己的箭头操作符的类类型对象。
如果返回类型是指针,则内置箭头操作符可用于该指针,编译器对该指针解引用并从结果对象获取指定成员。如果被指向的类型没有定义那个成员,则编译器产生一个错误。
如果返回类型是类类型的其他对象(或是这种对象的引用),则将递归应用该操作符。编译器检查返回对象所属类型是否具有成员箭头,如果有,就应用那个操作符;否则,编译器产生一个错误。这个过程继续下去,直到返回一个指向带有指定成员的的对象的指针,或者返回某些其他值,在后一种情况下,代码出错。

class T //假设T有一个vector<string>成员
{
    friend class TPtr;
public:
    T(vector<string> v) : vs(v) {}
    //..
private:
    vector<string> vs;
};

class TPtr //T的伴随类,成员为指向T类型对象vector<string>成员的指针及一个索引号
{
public:
    TPtr() = default;
    TPtr(const T& t) : tptr(make_shared<vector<string>>(t.vs)), n(0) {}
    TPtr(const TPtr& tp) : tptr(tp.tptr) {}
    //..
    string& operator*() const;
    string* operator->() const; 
private:
    shared_ptr<vector<string>> tptr;
    size_t n;
};
string& TPtr::operator*() const
{
    return (*tptr)[n];
}
string* TPtr::operator->() const
{
    return &this -> operator*(); //实际操作调用operator*()函数
}

8.函数调用运算符:必须是成员函数。定义了调用运算符的类的对象称为函数对象(function object),可以调用它,“行为像函数一样”。一个类可以定义多个不同版本的调用运算符,互相之间应该在参数数量或类型上有所区别。

//一个简单例子:
struct Abs
{
  int operator()(int val)
  {
    return val < 0 ? -val : val;
  }
};
//这样使用:
int i = -1;
double d = -3.14;
Abs obj;
int ui = obj(i);
double ud = obj(d);

关于函数对象:
lambda是函数对象,它被翻译成一个未命名类的未命名对象,该类中含有一个重载的函数调用运算符,且默认情况下是一个const成员,若lambda被声明成mutable的,则为非const。

标准库以模板形式定义了一组表示算术、关系、逻辑运算符的类

#include<functional> //在这个头文件里
//例如用于泛型算法中:
sort(vec_string.begin(), vec_string.end(), greater<string>());
//有趣的是,标准库定义的函数对象适用于指针!
//直接比较两个无关的指针是未定义行为,但是使用标准库函数对象却可以
vector<string*> v_ps;
sort(v_ps.begin(), v_ps.end(), greater<string*>());// OK!

至此,我们的可调用对象有:函数,函数指针,lambda,bind创建的对象(bind(/*A function object, pointer to function or pointer to member*/,arg_list)),重载调用运算符的类。

标准库function类型能帮我们整合这些调用对象。

//其形式是:
function<retType(args)> f;c
//例:
int add(int v1, int v2)
{
    return v1 + v2;
}
auto minu = [](int v1, int v2)->int {return v1 - v2; };
struct multi
{
    int operator()(int v1, int v2)
    {
        return v1 * v2;
    }
};
auto divide = bind(divides<int>(), _1, _2);
//用function整合
function<int(int, int)> f1{ add }, f2{ minu }, f3{ multi() }, f4{ divide };
//调用形式:
f1(1,2);// 3
f2(1,2);// -1
f3(1,2);// 2
f4(1,2);// 0

9.类型转换运算符(conversion operator):必须为成员函数,将一个类类型转换为其他任何类型——只要其能作为函数的返回类型,因此数组、函数类型不被包括。
没有返回类型,形参列表必须为空,通常为const成员。

class T //简单的类
{
public:
  T(int i = 0) : val(i) {} //只接受一个参数的构造函数隐含一个转换:将int类型转换成类类型
  operator int() const; //类型转换运算符
private:
  int val;
};
T::operator int() const //将类类型转换成int类型
{
  return val;
}

//应用:
T t;
t = 3; //int先转换成T类型,调用T::operator=
t + 4; //调用类型转换运算符将t转成int,再执行整数加法

类型转换运算符是隐式执行的,但每个类型转换函数都会返回一个对应类型的值。
可以定义显式类型转换运算符来防止隐式转换带来的不便:

class T
{
public:
  T(int i = 0):val(i) {}
  explicit operator int() const {return val;} //显示类型转换运算符
private:
  int val;
};    
T t = 1;//OK,构造函数非显式
t + 2; //error,要将t转换为int,但类型转换运算符是显式的
static_cast<int>(t) + 2; //OK,显式请求转换
//表达式若被用作条件,编译器会自动为其应用显式类型转换!
if (t) //..OK

IO标准库正是定义了显式的向bool转换的运算符,类似while(cin >> val) 这样的调用才是合法的。
一般的应用就是显式的定义向bool的类型转换,此外应尽量避免定义类型转换函数。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值