14操作重载与类型转换


   重载的运算符是具有特殊名字的函数:他们的名字是由关键字operator和其后定义的运算符号共同组成。重载运算符也包含返回类型参数列表和函数体

  重载运算符的参数数量与该运算符作用的运算对象数量一样多。一元运算符有一个参数、二元运算符有两个参数。对于二元运算符来说,左侧对象传递给第一个参数,而右侧对象传递给第二个参数。除了operator(),其他重载运算符不能含有默认实参。

  如果一个运算符函数是成员函数,则它的第一个(左侧)运算对象绑定到隐式的this指针上,因此成员运算符函数的参数数量比运算符的运算对象总数少一个。

  对于一个运算符函数来说,它或者是类的成员,或者至少含有一个类类型的参数

能被重载的运算符列表如下: 

  


 二、调用重载运算符的方式:

  • 将运算符作用于类型正确的实参,以间接方式调用重载的运算符函数。 例如:data1 + data2
  • 我们也能像调用普通函数一样直接调用运算符函数.例如:operator+(data1+ data2)
  • 我们像使用其他成员函数一样显式调用成员运算符函数.例如:  
  data1 += data2; //基于“调用”的表达式
  data1.operator+=(data2); //对成员运算符函数的等价调用

  上述两条语句都调用了成员函数operator+=,将this指针绑定到datal的地址、data2作为实参传入了函数。最好不要重载逻辑与、逻辑或、逗号运算符、&&、||、取地址运算符、


三、重载赋值运算符:

  重载赋值运算符,赋值之后,左侧运算对象和右侧运算对象的值相等,并且运算符应该返回它左侧运算对象的一个引用。


四、选择作为成员还是非成员:

  • 赋值(=)、下标([ ])、调用(())和成员访问箭头运算符必须是成员。
  • 复合赋值运算符(例如+=、>>=)一般来说应该是成员,但并非必须,这一点与赋值运算符符略有不同。
  • 改变对象状态的运算符或者与给定类型密切相关的运算符,如递增、递减、和解引用运算符,通常来讲应该是成员。
  • 具有对称性的运算符可能转换成任意一端的运算对象,例如算术、相等性、关系和位运算符等,因此它们应该是普通的非成员函数。

重载运算符由关键字operator与其后要定义的运算符号共同组成。

  • 注意点:
  1. 当一个重载的运算符是成员函数时,this绑定到左侧运算对象。成员运算符函数的显式参数数量比运算对象的数量少一个。
  2. 当运算符作用于内置类型的运算对象时,我们无法改变运算符的含义。
int operator+(int,int); //错误 不能为int重定义内置的运算符

3.只能重载已有的运算符,无权发明新的运算符号。
4.+、-、*、&既是一元运算符,也是二元运算符,所有这些运算符都能被重载,从参数数量我们可以推断到底定义的是哪种运算符。
5.对于重载运算符来说,其优先律和结合律与对应的内置运算符一致。

调用重载的运算符函数

  1. 非成员运算符函数的等价调用
data1 + data2;  //普通的表达式
operator+(data1,data2); //等价的函数调用

两次调用都调用了非成员函数operator+,传入data1作为第一个实参,data2作为第二个实参。

  1. 成员运算符函数的调用
data1 += data2; //基于“调用”的表达式
data1.operator+=(data2);    //对成员运算函数的等价调用

两次调用都调用了成员函数operator+=,将this绑定到data1的地址,将data2作为实参传入了函数。

通常情况下不重载逗号取地址逻辑与逻辑或运算符。

重载运算符设计的准则:确定类需要哪些操作,考虑应该把类操作设计成普通函数还是重载的运算符;如果某些操作在逻辑上与运算符相关,则它们适合于定义成重载的运算符。
只有当操作的含义对于用户来说清晰明了时才使用运算符。如果用户对运算符有几种不同的理解,则使用这样的运算符将产生二义性。
使用重载的运算符本质上是一次函数调用,所以这些关于运算对象求值顺序的规则无法应用到重载的运算符上。

重载运算符时是定义为成员函数还是普通的非成员函数。


  1. 赋值(=)、下标([])、调用(())和成员访问箭头(->)运算符必须是成员。
  2. 复合赋值运算符一般来说应该是成员,但并非必须,这一点与赋值运算符略有不同。
  3. 改变对象状态的运算符或给定类型密切相关的运算符,如递增、递减、和解引用运算符,通常应该是成员。
  4. 具有对称性的运算符可能转换任意一端的运算对象,例如算术、相等性、关系和位运算符,因此它们通常应该是普通的非成员函数。
  5. 当提供含有类对象的混合类型表达式,则运算符必须定义成非成员函数。

当重载运算符函数是非成员函数时,重载运算符与内置运算符一致;当重载运算符是成员函数时,重载运算符与内置运算符有所区别,此时它的左侧运算对象绑定到隐式的this指针上,成员运算符函数的(显式)参数数量比运算符的运算对象总数少一个。

vector<string>svec1,svec2; "cobble" == "stone"; //使用了C++语言内置版本的==,比较两个指针。 svec1[0] == svec2[0]; //使用string类型的重载比较运算符 svec1 == svec2; //使用vector类型的重载比较运算符 svec1[0] == "stone"; //使用string类型的重载比较运算符

% 非成员 %= 成员 会改变对象的状态 ++ 成员 会改变对象的状态 -> 成员 否则编译器会报错 << 非成员 && 右值引用 非成员 == 非成员 具有对称性 () 调用 成员

 


14.2.1 重载输出运算符<<


  1. 输出运算符的第一个形参是一个非常量ostream对象的引用。
    ostream是非常量是因为写入内容会改变其状态;而该形参是引用是因为我们无法直接复制一个ostream对象。
  2. 第二个形参一般是常量的引用,该常量是我们想打印的类类型。第二个形参是引用的原因是我们希望避免复制实参。而之所以是常量是因为打印对象不会改变对象的内容。
ostream &operator<<(ostream &os,const Sales_data &item){
    os << item.isbn()<< " "<< item.units_sold<<" "<<item.revenue<< “ ”<<item.avg_price();
    return os;
} //完成输出后,运算符返回刚刚使用的ostream的引用

自定义IO运算符,则必须将其定义为非成员函数。 IO运算符通常需要读写类的非公有数据成员,所以IO运算符一般被声明为友元。


 

14.2.2 重载输入运算符>>


 

输入运算符的第一个形参是运算符将要读取的流的引用,第二个形参是将要读入到的(非常量)对象的引用。该运算符通常会返回某个给定流的引用。第二个形参之所以是个非常量是因为输入运算符本身的目的就是将数据读入到这个对象中。

istream &operator>>(istream &is,Sales_data& item){
    double price;//不需要初始化 因为我们将先读入数据到price,之后才使用它
    is>>item.bookNo>>item.units_sold>>price;
    if(is) //检查输入是否成功
        item.revenue = item.units_sold *price;
    else
        item = Salse_data();    //输入失败:对象被赋予默认的状态
    return is;
}

输入运算符必须处理输入可能失败的情况,而输出运算符不需要。string - unsigned double 成立 unsigned double string failbit置位

第一种情况还是成立的,第二种返回的流是错误的


 14.3 算术和关系运算符


将算术和关系运算符定义为非成员函数以允许对左侧或右侧的运算对象进行转换,因为这些运算符不需要改变运算对象的状态,所以形参都是常量的引用。

  • 算术+
Sales_data operator+(const Sales_data &lhs,const Sales_data &rhs){
    Sales_data sum = lhs;
    sum += rhs;
    return sum;
}
  • 算术-
Sales_data operator-(const Sales_data &lhs,const Sales_data &rhs){
   Sales_data sum = lhs;
   sum -= rhs;
   return sum;
}
  • 复合赋值-=
 Sales_data operator-=(const Sales_data &rhs){
   units_sold -= rhs.units_sold;
   revenue -= rhs.revenue;
   return *this;
}

如果类同时定义了算术运算符和相关的复合赋值运算符,则通常情况下应该使用复合赋值运算符来实现算术运算符。

  • 为什么使用operator+=来定义operator+比其他方法有效: ····可能只是因为可读性好吧!

 


 

 

 14.3.1 相等运算符


  • 相等
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);
}

14.4 复合赋值运算符


复合赋值运算符不非得是类的成员,但一般定义在类的内部。 类中的复合赋值运算符也要返回其左侧运算对象的引用。 二元运算符:左侧运算对象绑定到隐式的this指针

Sales_data & Sales_data::operator+=(const Sales_data &rhs){
    units_sold += rhs.units_sold;
    revenue += rhs.revenue;
    return *this;
}

赋值运算符必须定义为类的成员,复合赋值运算符通常定义在类的内部。这两类运算都应当返回左侧运算对象的引用。


 

14.5 下标运算符


表示容器的类通常可以通过元素在容器中的位置访问元素,这些类一般会定义下标运算符operator[]。

下标运算符必须是成员函数,通常以所访问元素的引用作为返回值,这样做的好处是下标可以出现在赋值运算符的任意一端。

最好定义下标运算的常量版本和非常量版本,当作用于一个常量对象时,下标运算符返回常量引用以确保我们不会给返回的对象赋值。

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];}
private:
    std::string *elements;  //指向数组首元素的指针
};

14.6 递增和递减运算符


C++语言不要求递增和递减运算符必须是类的成员,但是因为他们改变的正好是所操作对象的状态,所以建议将其设定为成员函数。 定义递增或递减运算符的类应该同时定义前置版本和后置版本。

与内置类型保持一致,前置递增/递减运算符返回对象的引用,后置递增/递减运算符返回对象的原值(递增或递减之前的值),返回的形式是一个值而非引用。

class StrBlobPtr {
public:
    StrBLobPtr& operator++();
}
  • 前置递增运算
StrBlobPtr& StrBlobPtr::operatro++(){
    check(curr,"当前值超过StrBlobPtr结尾的增量");
    ++curr;
    return *this;
}

我们把curr的当前值传递给check函数,如果这个值小于vector的大小,则check正常返回。否则若curr已经达到了vector的末尾,则抛出异常。

  • 前置递减运算
StrBlobPtr& StrBlobPtr::operator--(){
    --curr; //如果curr是零,则继续递减它将产生一个无效的下标
    check(curr,"当前递减值已经超过StrBlobPtr的结尾");
    return *this;
}

后置递增递减运算

 

为了区分后置与前置运算符,后置版本提供一个额外的(用不到)的int形参。 对于后置版本来说,递增对象之前需要首先记录对象的状态。

  • 后置递增运算符
StrBLobPtr StrBlobPtr::operator++(int){
    //在此处无需检查有效性,调用前置递增运算时才需检查
    StrBlotPtr ret = *this; //记录当前值
    ++*this;        //  向前移动一个元素,前置++需要检查递增的有效性
    return ret;     //返回之前记录的状态
}
  • 后置递减运算符
StrBLobPtr StrBlobPtr::operator--(int){
    //在此处无需检查有效性,调用前置递增运算时才需检查
    StrBlotPtr ret = *this; //记录当前值
    --*this;        //  向后移动一个元素,前置--需要检查递增的有效性
    return ret;     //返回之前记录的状态
}

后置运算符通过调用各自的前置版本来完成实际工作。 因为我们不会用到int形参,所以无需为其命名。

example

StrBlobPtr p(a1); //p指向a1中的vector p.operator++(0); //调用后置 p.operator++(); //调用前置

对于递增或递减运算,无论是前置还是后置,都会改变对象的值,所以不能定义成const的。


 

14. 7 成员访问运算符


  • 解引用运算符(*)
class StrBLobPtr{
public:
    std::string& operator*()    const{
        auto p = check(curr,"dereference past end ");
        return (*p)[curr];  //(*p)是对象所指的vector
    } 
    std::string * operator->() const {
        //将实际工作委托给解引用运算符
        return & this->operator*();
    }
    //其他成员与之前版本一致
}

解引用运算符首先检查curr是否仍在作用范围内,如果是则返回curr所指元素的一个引用。箭头运算符不执行任何自己的操作,而是调用解引用运算符并返回引用结果的地址。

箭头运算符和解引用运算符通常都是类的成员 将运算符定义成了const成员,获取一个元素不会改变StrBlobPtr对象的状态,他们的返回值分别是非常量string的引用或指针,因为一个StrBlobPtr只能绑定到非常量的StrBlobPtr对象。

example


StrBlobPtr a1 ={"ccpang","xxpang"}; //一个类
StrBlobPtr p(a1);   //p指向a1中的vector
*p = "okay";        //给a1的首元素赋值
cout << p->size()<<endl; //打印4,这是a1首元素的大小
cout << (*p).size() <<endl; //

重载的箭头运算符必须返回类的指针或定义了箭头运算符的某个类的对象。

 

14.8 函数调用运算符(函数对象)


任何定义了函数调用操作符的对象都是函数对象。C++ 支持创建、操作新的函数对象,同时也提供了许多内置的函数对象。
如果类定义了调用运算符,则该类的对象称为函数对象(function object)。

若类重载了函数调用运算符,“operator() ”  。 则我们可以像使用函数一样使用该类的对象。

//重载函数调用运算符
struct absInt{
    int operator()(int val) const {
        return val < 0 ? -val : val;
    }
}
//使用调用运算符是令一个absInt对象作用于一个实参列表,这一过程看起来很像函数调用的过程。

int i = -42;
absInt absobj;      //含有函数调用运算符的对象
int ui = absobj(i); //将i传递给absobj.operator()

//absobj只是一个对象而非函数,我们也能“调用”该对象,调用该对象其实是在运行重载的调用运算符。

参考文献:https://harttle.land/2015/07/03/stl-function-objects-and-pointers.html

14.8.2  标准库定义的函数对象函数

标准库定义了一组表示算术运算符、关系运算符和逻辑运算符的类,每个类分别定义了一个执行命名操作的调用运算符。

plus类定义了一个函数调用运算符用于执行+的操作

modulus类用于执%操作

plus<int>intAdd;   //可执行int加法的函数对
negate<int>intNegate;  //可对int值取反的函数对象
//使用intAdd::operator(int,int)求10和20的和
int sum = intAdd(10,20); //sum = 30;

 

 14.8.3  可调用对象与function

C++语言中的可调用对象:函数、函数指针、lambda表达式、bind创建的对象、重载调用运算符的类。

我们可能希望使用这些可调用对象创建一种简单地桌面计算器,为了实现这一个目的,我们需要定义一个函数表(function table)用于存放指向这些可调用对象的指针。

当程序需要执行某个特定的操作时,从表中查找该调用的函数。

在C++语言中,函数表很容易通过map来实现。

我们使用一个表示运算符符号的String对象作为关键字i:使用实现运算符的函数作为value,当我们需要求给定运算符的值时,先通过运算符索引map,然后调用找到的

那个元素。

//构建从运算符到函数指针的映射关系,其中函数接受两个int,返回一个int
map<string,int(*)(int,int)> binops; 
我们可以按照下面的形式将add添加binops
binops.insert({"+",add}); //{"+,add"}是一个pair

但是我们无法将mod或者divide存入binops,因为mod是个lambda表达式,而每个lambda都有其自己的类类型。该

类型与存储在binops中的值不匹配。

通过下面这个标准库function类型可以解决上面那个例子:

 

 则我们的map将会变成如下形式:

 如果有重载的函数呢?如何解决这个问题呢?

为什么实用std::function,  https://blog.csdn.net/smstong/article/details/44958833

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


函数对象类除了operator()之外也可包含其他成员,函数对象类通常含有一些数据成员,这些数据成员被用于定制调用运算符中的操作。

 

类型转换运算符函数


转换构造函数可以将实参类型的对象转换成类类型。 同样,类型转换运算符可以将一个类类型的值转换成其他类型。

operator type() const; //type表示某种类型

类型转换运算符可以面向任意类型进行定义,只要该类型能作为函数的返回类型。因此我们不允许转换成数组或函数类型,但是允许准换成指针(包括数组指针和函数指针)或引用类型。

类型转换符既没有显式的返回类型,也没有形参,而且必须定义成类的成员函数,类型转换运算符通常不应该改变转换对象的内容,因此,类型转换运算符一般被定义为const成员。

class SmallInt{
public:
    SmallInt(int i = 0): val(i){ //构造函数将算术类型的值转换为SmallInt对象
        if (i < 0 || i > 255)
            throw std::out_of_range("BadSmallInt values");
    }   
    operator int() const {return val;}
        //类类型转换运算符将SmallInt对象转换为int
private:
    std::size_t val;
}

显式的类型转换运算符


class SmallInt{
    public
        //定义为显式类型转换,编译器不会自动执行这一类型转换
        explicit operator int() const {return val;}
}
SmallInt si = 3;
si + 3 ;    //此处需要隐式类型转换,但是类的运算符是显式的
static_cast<int>(si+3);  //显式的请求类型转换

如果表达式被用于条件,则编译器会自动将显式类型转换应用于它。

转换为bool


while (std::cin >> value)
//while语句的条件执行输入运算符,它负责将数据读入value并返回cin,为了对条件求值,cin被istream operator bool类型转换函数隐式的执行了转换。如果cin的条件状态时good,则函数返回为真。

由于向bool类型转换部分通常用在条件部分,因此operator bool 一般定义为explicit.

陈小洁的三只猫
原文地址:https://www.cnblogs.com/ccpang/p/11403878.html