运算符重载

重载运算符

概念

基本概念

1)重载运算符是具有特殊名字的函数,由关键字operator和其要定义的运算符符号共同组成;包含返回类型,参数列表以及函数体
2)如果一个运算符函数是成员函数,那么第一个(左侧)运算对象绑定到隐式的this指针上。
3)对于一个运算符函数来熟,它或者是类成员,或者至少含有一个类类型参数
int operator+(int, int); //error
4)我们能重载大多数(不是全部)运算符。有些可以被重载,有些不能被重载
5)我们只能重载运算符,但不能发明运算符。
6)对于一个运算符,其优先级和结合律与对应的内置运算符保持一致。

某些运算符不应该被重载

1)某些运算符指定运算对象的求值顺序。而实用重载的运算符本质上是一次函数调用,所以这些关于运算对象的求值顺序的规则无法应用到重载的运算符上。
ex:逻辑与(&&)和逻辑或(||),其重载版本中,两个运算对象总是会被求值的。
因为其重载版本无法与内置版本保持一致,因此不建议重载它们。
2)一般不重载逗号和取地址运算符:C++已经定义了这两种运算符作用于类类型对象的特殊含义,因此不应该被重载。

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

1 如果类执行IO操作,则定义移位运算符使其与内置类型保持一致
2 如果需要检查是否相等,则需要定义operator==;如果有了==,通常应该有!=
3 如果包含一个内在的但需比较操作,则定义operator<; 如果有了<,通常应该定义其他的关系操作
4 重载的运算符返回类型通畅应该与其内置版本返回类型兼容;
逻辑和关系运算符返回bool;算术运算符返回值类型;赋值运算符和复合运算符返回左侧运算对象的引用;

赋值和复合赋值运算符

1 赋值运算符和复合运算符返回左侧运算对象的引用;
2 如果含有算数运算符或者位运算符,则最好也提供对应的复合赋值运算符。

选择成为成员或者非成员

1 必须是成员:赋值=, 下标[], 调用(), 成员访问箭头->
2 复合赋值一般是成员,但非必需;
3 改变运算对象的状态或与给定类型密切相关的运算符一般是成员:递增,递减,解引用
4 具有对称性的运算符可能转换任意一端的运算对象:算术,相等,关系,位,通常都是普通非成员函数

输入输出运算符

ex:给定以下类型:

class Sales_Data {
public:
    //...
private:
    string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
}

输出运算符

ostream& operator<<(ostream& os, const Sales_Data& data) {
    return os << data.isbn() << " " << data.units_sold
              << " " << data.revenue << " " << data.avg_price();
}

1)分析:
a 第一个形参类型:non-const(向流写入内容会改变其状态),引用(无法直接拷贝流类型对象)
b 第二个形残类型:const(无需改变该类型),引用(无需拷贝)
c 返回值类型:const(保持与内置类型一致,为了写代码的连续性)

2)注意:
a 输出运算符尽量减少格式化操作,尤其是不会打印换行符;这样给用户权利控制输出的细节;
b 必须是非成员类型;否则左侧运算对象就是类的对象
c 一般是友元;因为要访问该对象的私有成员

输入运算符

istream& operator>>(istream& is, Sales_Data& data) {
    double price;
    is >> data.bookNo >> data.units_sold >> price;
    if (is) {
        data.revenue = data.units_sold * price;
    } else {
        data = Sales_Data();
    }
    return is;
}

1)分析:
a 第一个形参和返回值类似于输出运算符
b 第二个形参:non-const(需要修改对象)

2)注意:
a) 输入运算符必须处理输入可能失败的情况,而输出运算符不需要。
可能发生一下错误:
i 流含有错误类型的数据
ii 达到文件末尾或者其他错误
b)如果在发生错误前,对象已经有一部分被改变,则适当的将对象置为合法状态显得异常重要;
c)必要的情况下需要做更多数据验证的工作;同时也该设置流的条件状态以标示出失败信息。

算术和关系运算符

1 通常是非成员,以允许左侧或右侧对象进行转换
2 通常是const 引用,因为不改变对象,不需要拷贝;
3 通常定义了算术运算符,会定义一个对应的复合赋值运算符;且用复合赋值运算符定义算术运算符
4 算术运算符返回值类型,而关系运算符返回bool类型

//算术运算符
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 如果定义了operator=,也该定义operator!=
3 相等和不相等通常应该有一个把工作委托给另一个。

赋值运算符

1 赋值运算符必须定义为成员函数,不论形参是什么类型。
2 复合赋值运算符不非得是类的成员,不过还是倾向于把包括复合赋值在内所有的赋值运算符定义在类的内部。

//赋合赋值运算符返回引用类型
Sales_Data& operator+=(const Sales_Data& data) {
    bookNo += data.bookNo;
    units_sold += data.units_sold;
    revenue += data.revenue;
    return *this;
}

下标运算符

1 下标运算符必须是成员函数
2 返回值为访问的元素的引用。这样做的好处是下标可以出现在赋值运算符的任意一端。
3 通常会定义两个版本:一个返回普通引用,一个返回常量引用。

递增和递减运算符

1 C++并不要求递增和递减是成员,但因为它改变了对象的状态,所以建议将其设定为成员。
2 应该同时为类定义前置版本和后置版本。
3 前置版本返回对象的引用;后置版本返回值类型
4 同时定义前置和后置版本,为了解决一个问题:即普通的重载形式无法区分这两种情况,后置版本通常接受一个额外(不被使用)的int类型的形参。该形参唯一的作用就是区分前置版本和后置版本的函数。
5 后置运算符调用各自的前置版本来完成实际的工作。

class A {
public:
    A& operator++();
    A operator++(int);
};

A a;
a.operator++();
a.operator++(0);    //显示调用后置版本时必须提供参数

成员访问运算符

1 箭头运算符必须是类的成员;解引用运算符通常也是类的成员
2 两者通常定义成const类型,因为并没有改变对象的状态。
3 通常箭头运算符把工作委托给解引用运算符

string* operator->() const {
    return &(this->operator*());
}

函数调用运算符

1 函数调用运算符必须是成员函数
2 如果定义了函数调用运算符,则该类的对象称作函数对象。
3 函数对象是lambda的实现原理。通常含有些数据成员,被用于定制调用运算符中的操作。

//简单的实现一个函数对象
class PrintString {
public:
    PrintString(ostream& os = cout, const char *string = "*"): os_(os), string_(string) {}
    void operator() (const string& s) const { os_ << s << string_; }
private:
    ostream& os_;
    const char *string_;
};

//调用方法
vector<string> v{"my", "bd", "al"};
for_each(v.begin(), v.end(), PrintString(cerr, '\n'));

ex:实现一些小例子
1 统计大于3的值有多少个

vector<int> v{1, 1, 1, 3, 5, 2, 7};
int sz = 3;
cout << count_if(v.begin(), v.end(), bind(greater<int>(), _1, sz)) << endl;
cout << count_if(v.begin(), v.end(), bind(less<int>(), sz, _1)) << endl;

2 找到第一个不等于1的值

auto iter = find_if(v.begin(), v.end(), bind(not_equal_to<int>(), 1, _1));
auto iter = find_if_not(v.begin(), v.end(), bind(equal_to<int>(), _1, 1));

3 将所有的值乘以2

transform(v.begin(), v.end(), v.begin(), bind(multiplies<int>(), _1, 2));

可调用对象与function

C++语言中,有几种可调用对象:函数,函数指针,lambda表达式,bind创建的对象,重载了函数调用运算符的类。虽然各种类型不同,但可能共享一种调用形式(call signature)。调用形式表明了返回类型以及传递给调用的实参类型。例如:int(int, int);
对于几个可调用对象共享同一种调用形式的情况,我们希望把他们看成具有相同的类型。
ex:

//普通函数
int add(int i, int j) { return i + j; }
//lambda
auto mod = [](int i, int j) { return i % j; }
//function object
struct divide {
    int operator()(int i, int j) const {
        return i / j;
    }
};

例子:实现一个简单的桌面计算器

为了实现目的,需要定义一个函数表,用于存储指向这些可调用对象的指针。当程序需要特定操作时,从表中查找该函数调用。

//构建从运算符到函数指针的映射关系
map<string, int(*)(int, int)> binops;

按照形式,把函数指针添加进map
binops.insert({"+", add}); //正确

但我们不能把mod和divide存入binops。

为了解决上述问题,标准库定义了function的模版,创建一个具体的function类型时,我们必须提供额外的信息。具体使用如下;
function<int(int, int)>
该声明表示:它可以接受两个int,返回一个int的可调用对象;

function<int(int, int)> f1 = add;
function<int(int, int)> f2 = divide();
function<int(int, int)> f3 = [](int i, int i) { return i * j; };

最后,通过function实现简单计算器

map<string, function<int(int, int)>> bioOp = {
    {"+", plus<int>()},
    {"-", minus<int>()},
    {"*", multiplies<int>()},
    {"/", divides<int>()},
    {"%", modulus<int>()}
};

string op;
int left, right;
cin >> left >> op >> right;
cout << bioOp[op](left, right);

重载的函数与function

我们不能(直接)将重载的函数名字存入function类型的对象中

string add(const string& i, const string& j) { return i + j; }
int add(int i, int j) { return i + j; }

map<string, function<int(int, int)>> bioOp;

bioOp.insert({"+", add});    //error

解决方法:

1 存储函数指针而非函数的名字

int (*pf)(int, int) = add;
bioOp.insert({"+", pf});

2 使用lambda消除二义性
bioOp.insert({"+", [}(int a, int b) { return add(a, b); });

通过使用lambda来指定我们希望使用的那个版本

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值