本节课要点:
- 运算符重载
考虑我们有一个meter类:
- min —— 最小刻度
- max —— 最大刻度
- value —— 当前测量值
- span —— 测量范围
#pragma once
#include <iostream>
#include "int_type.h"
class meter {
public:
using int_type = ::int_type;
using reference = int_type&;
using different = int_type;
private:
int_type min, max;
int_type value;
different span;
public:
explicit meter(int_type a = 0, int_type b = 100) : min(a), max(b), value(a), span(b - a + 1) {}
~meter() {}
};
如果我们做出以下操作:
meter m;
m = m + 10;
我们人类可以理解是 value + 10,但是编译器不知道该如何进行运算,因为 m 是一个类对象,而 10 是一个整数。因此,我们必须显式地告诉编译器 +运算符 的含义。
运算符重载
在C++中,很多的运算符被视为函数,称为运算符函数,其函数名字由关键字 operator 加上一个运算符构成。设运算符为@,则运算符函数的原型可以表示为:
返回值类型 operator @(参数列表);
1. 重载运算符函数的考虑因素
- 运算符的原始语义
- 重载形式的选择
- 运算符是否可以级联
2. 重载+运算符
(1)首先分析加法的原始语义
左右操作数保持不变,产生一个新的值结果,这个值结果是一个右值。
(2)其次进行重载形式的选择
- 若函数的结果是个新值,则函数最好以友元的形式重载。
- 若函数的结果保存到操作数中,则函数最好以成员的形式重载。
经过加法运算,我们将得到一个新值,因此以友元的形式进行重载。
(3)最后加法是否可以级联。
显然可以。
// meter.h
friend meter operator+(const meter & a, int_type b);
// meter.cpp
meter operator+(const meter & a, int_type b) {
meter m{a}; // m是一个临时对象,{}代表统一初始化,调用m的复制构造函数
m.value += b;
return m; // m是一个右值,等于返回了一个右值引用
}
3. 重载=运算符
如果我们做出以下操作:
meter m;
m = 5;
这将发生一次隐式类型提升。m是类类型,5是整型,5会被隐式地提升成类类型。在该过程中,会默认调用一个匿名对象的构造函数,完成对5的类型提升,最后再把匿名对象赋值给m。
为了避免得到我们不想要的结果,于是把构造函数声明成显示的,这样才不会被用于隐式类型提升:
explicit meter(int_type a = 0, int_type b = 100) : min(a), max(b), value(a), span(b - a + 1) {}
同时,我们需要重载赋值运算符:
meter & operator=(int_type v) { // 参数v是右操作数,this对象是左操作数
this->value = v;
return *this;
}
如果函数返回的是一个值类型,那么返回的是一个右值。
4. 重载++运算符
(1)前缀自加
++ ++a; // 未报错。判断出可以级联
++a = 5; // 未报错。判断出返回的是一个左值
前缀自加运算符概要如下:
- 作为成员重载
- 函数没有参数
- 函数要返回操作数的左值引用
meter & operator++() {
++value;
return *this;
}
(2)后缀自加
a++ ++; // 报错。判断出返回的不是一个左值
后缀自加运算符概要如下:
- 作为成员重载
- 函数没有参数
- 函数要返回一个临时右值
meter operator++(int) {
auto m{*this};
++value;
return m; // m是一个将亡对象,因此返回值类型为右值
}
前缀自加和后缀自加都没有参数,但为了区分两者,C++语法规定,重载的后缀自加函数必须有一个整型参数。因此,这个 int 是一个只有类型没有名字的占位参数。
5. 重载<<运算符
运算符<<和>>的原始语义是数左移和右移,是两种位操作。在C++中,这两个运算符如果与流(stream)对象结合,其含义就改变了:<<是输出流运算符,>>是输入流运算符。可以看到,这两个运算符实际上已经被C++标准库重载了。
流运算符概要如下:
- 重载为类的友元。
- 函数有两个参数,第一个参数是流对象的引用,第二个参数是有操作数。
- 函数返回参数流对象的引用,以便于级联。
// meter.h
friend std::ostream & operator<<(std::ostream & os, const meter & m);
// meter.cpp
std::ostream & operator<<(std::ostream & os, const meter & m) {
return os << '(' << m.min << ',' << m.max << ',' << m.value << ')';
}
6. 重载类型转换运算符
- 装箱(boxing):把一个标量类型转换为类类型。
- 拆箱(unboxing):把一个类类型转换为标量类型。
现有一个process类,我们需要能够完成meter和process之间的互相转换。
process.h
#pragma once
#include <iostream>
class process {
public:
using percent_t = unsigned short;
private:
percent_t percent;
public:
process(percent_t per = 0) : percent(per) {}
percent_t& value() {
return percent;
}
friend std::ostream& operator<<(std::ostream& os, const process& p);
};
重载的运算符函数名实际上就是一种类型的名字。
meter -> process
// meter.h
operator process();
// meter.cpp
meter::operator process() { // 加上作用域,防止成为全局函数
return process{(value - min) * 100 / span}; // 创建一个临时对象并返回
}
process -> meter
// process.h
operator meter();
// process.cpp
process::operator meter() {
meter t{0, 100};
return t = static_cast<meter::int_type>(percent * 100); // 强制类型转换,静态转换;如果不能转换,编译器不会放过
}
static_cast<meter::int_type>(percent * 100); // 强制类型转换,静态转换。
如果不能转换,编译器不会放过;如果不写,编译器将对这个类型转换过程袖手旁观
注意: 为了防止互相包含,形成死套,我们需要超前声明。
- 在meter.h超前声明process是一个类
- 在meter.cpp中进行实现
对于process同理。
// meter.h
class process; // forwarding
7. 重载()运算符
reference operator()() {
return value;
}
使用:
meter m;
int value = m();
()作用在类对象上时,形式非常类似于函数调用,但“函数名”不是真正代表了函数,而是一个对象的名字。这种对象因此得名函数对象。
可调用对象:
- 函数。
- lambda表达式。
- 函数对象。
meter.h
#pragma once
#include <iostream>
#include "int_type.h"
//forwarding
class process;
class meter {
public:
using int_type = ::int_type; //using int_type = int;
using reference = int_type&;
using different = int_type;
private:
int_type min, max;
int_type value; //因为value是不确定的,所以不需要给参数
different span; //最小值和最大值的距离
public:
//1.使用默认参数
//2.把构造函数声明成显式的,这样才不会被用于隐式类型提升
explicit meter(int_type a = 0, int_type b = 100) : min(a), max(b), value(a), span(b - a + 1) {}
//复制控制 —— 不需要。因为都是数值类型,所以编译器帮我们对拷了
~meter() {}
//meter:如果函数返回一个值类型,那返回的是一个右值
meter & operator=(int_type v) { //参数v是右操作数,this对象是左操作数
this->value = v;
return *this;
}
meter & operator++() {
++value;
return *this;
}
meter operator++(int) { //只有类型没有名字,称为占位参数
auto m{*this};
++value;
return m; //m是一个将亡对象,因此返回值类型为右值
}
//类型转换运算符operator int_type,没有参数也没有返回值
//关键字——operator,类型名——int_type
operator int_type() {
return value;
}
operator process();
reference operator()() {
return value;
}
//meter & a:隐藏修改a的可能,不符合加法语义;b本来就是一个简单类型,加不加限定无所谓
//因为产生新值,所以返回一个右值
friend meter operator+(const meter & a, int_type b); //函数名字是operator+,之间可以有空格
friend std::ostream & operator<<(std::ostream & os, const meter & m);
};
meter.cpp
#include <iostream>
#include "meter.h"
#include "process.h"
meter operator+(const meter & a, int_type b) {
meter m{a}; //m是一个临时对象,{}代表统一初始化,调用m的复制构造函数
m.value += b;
return m; //m是一个右值,等于返回了一个右值引用
} //可以级联(又不是在=左边,啊喂!)
//std::ostream & os 不能加const,因为<<要修改os
std::ostream & operator<<(std::ostream & os, const meter & m) {
return os << '(' << m.min << ',' << m.max << ',' << m.value << ')';
}
//加上作用域,防止成为全局函数
meter::operator process() {
//(value - min) * 100 / span 就是算成一个百分比
return process{(value - min) * 100 / span}; //创建一个临时对象并返回
}