// 重载运算符Op
// T1: 返回类型,最好和内置的Op返回类型一样。比如&&运算返回bool,=、+=运算返回引用
// t1 t2 ...: 形参列表,1元运算符1个参数,2元运算符2个参数,3元对应3个参数。
T1 operatorOp(T1 t1, T2 t2, ...)
{
}
运算符也是函数,形参就是运算对象,所以运算符也可以重载。和一般函数重载的区别是函数名字比较特殊,operator后面紧接运算符组成函数名。用法和普通重载一样。
但是不能为内置类型重载运算符,这些都是已经内部定义了。
除了重载的函数调用运算符operator()之外,其他重载运算符不能含有默认实参。
如果一个运算符函数是成员函数,则它的第一个( 左侧)运算对象绑定到隐式的this指针上。
重载运算符应该继承内置版本的含义,如string的+运算是连接两个string,非常容易被理解。
只能重载已有运算符,不能发明运算符 。
对于运算符重载的参数,不能全部是内置类型参数,至少有一个类成员或者类类型参数。
a + b; // 可以看成是operator+(a, b)
// 重载非成员运算符
data1 + data2; // 等价
operator+(data1, data2); // 等价
// 重载成员运算符
data1 += data2 ; // 等价,实参列表是(data1,data2)
data1.operator+=(data2); // 等价
// 这里也隐含了一条信息:重载成员运算符,左侧运算对象必须是类对象,例子如下:
strings = "world";
string t = s + "!"; // 正确
string u ="hi" + s; // 如果+是string的成员,则产生错误
T1 operator&&(T2, T3); // 会丢失短路求值属性
T1 operator||(T2, T3); // 会丢失短路求值属性
// 重载=运算符:应该和内置版本一样返回左侧对象的引用。
T1& operator=(T1, T2);
// 重载+=复合赋值运算,应该和内置版本一样,先+,再=
T1& operator+=(T1, T2);
int operator+(int, int) // 错误,不能重载int类型的+号运算符。两个int至少有一个是类类型或者类成员
{
}
可重载运算符
成员还是非成员?
成员还是非成员?运算符是作为普通函数,还是成员函数好,可以根据下面的准则:
- 成员
- =、[]、()、->,必须是成员。
- +=、&=一般是成员。
- —、++、*解引用等改变实参状态的,一般是成员。
- 非成员
// os: 是非const的引用,因为os会改变状态,且ostream不可复制。 // t: 是const的引用,因为一般不会改变t的内容。 // return: 是ostream的引用,流不可复制。 ostream& operator<<(ostream &os, const T &t); // 正确。 ostream operator<<(ostream &os, const T &t); // 错误:流不可复制,应该返回引用。 ostream operator<<(ostream &os, const T t); // 不建议:没必要拷贝t
ostream &operator<<(ostream &os, const T &t) {
os << t.a << “ “ << t.b << “ “;
// 内部不需要有\n、endl之类的格式化操作。完全可以由用户自行完成。
// 重载输出,我们只关注应该输出什么,而不是输出成什么样。
os << t.a << "\n" << t.b << "\n" << endl;
return os;
}
<a name="6Ddsx"></a>
# 输入运算符>>
```cpp
// os: 是非const的引用,因为os会改变状态,且ostream不可复制。
// t: 非const引用,因为要对t写入数据嘛。
// return: 是ostream的引用,流不可复制。
ostream& operator>>(ostream &os, T & t){
......
// 要考虑流操作失败的情况。
// 1、比如读取的类型和t.b不符合,则t.b和t.c的输入都会失败
// 2、os到达文件结尾,后续操作会全部失败
os >> t.a >> t.b >> t.c;
if(os){ // 前面的流操作全部成功
// 处理数据
// 也可能发现数据错误,我们应该手动设置流的条件状态。
// 一般应该使用标准库的标志信息。
// eofbit表示到达文件尾、badbit表示流被破坏,一般就用failbit
os.setstate(os.rdstate() & os.failbit);
}
else{ //前面的流操作出现失败,需要处理好t内部数据,可能部分有数据。
......
}
return os;
}
算术运算符
对称运算符,一般定义成非成员函数。
// 形参一般都是常量引用,因为不会去修改
// 返回是值传递,拷贝一份副本。
// 算术运算符一般要成双成对出现,比如重载+,就要重载+=
T1 operator+(const T2 &l, const T3 &r){
T2 temp = l;
temp += r; // 建议使用对应的复合运算符来完成。
return temp;
}
T1 operator+=(const T2 &l, const T3 &r){
}
T1 operator&&(const T2 &l, const T3 &r){
}
关系运算符
一般定义成非成员函数。
// 形参一般都是常量引用,因为不会去修改
// 返回类型:一般都是bool,继承内置的。
bool operator<(const T2 &l, const T3 &r){
bool ret = false;
...
return ret;
}
一般的关系运算符应该完成的工作
- 1、定义顺序关系
- 令其与关联容器中对关键字的要求一致
2、如果类同时也含有==运算符的话,则定义一种关系令其与==保持一致。特别是,如果两个对象是!=的,那么一个对象应该<另外一个 。
==相等运算符
判断两个对象数据是否相等。重载==的设计准则:
对象有比较操作,那就赶紧重载==。
- ==应该能判断数据(成员)是否相同。
- ==应该有传递性。
- ==和!=应该成双成对出现。用其中一个去实现另外一个,只有一个是实现具体比较工作。
- <运算符是被经常使用到的,比如标准库容器的默认使用的关系运算符基本都是<,所以重载operater<非常有必要。 ```cpp
// 1、重载了==一半就要重载!= // 2、其中一个运算符的实现可以用另外一个运算符完成,这样只需要实现其中一个即可。 bool operator==(const T1 &l, const T2& r){ return !(l != r); }
bool operator!=(const T1 &l, const T2& r){
}
<a name="2TdFD"></a>
# =赋值运算符
必须定义为成员函数。重载=运算符的逻辑应该和类的拷贝赋值、移动赋值一样。
```cpp
StrVec& StrVec::operator=(initialized_list<string> il){
// 第一步,分配新空间并拷贝元素
auto data = alloc_n_copy(il.begin(), il.end());
// 第二步,销毁旧数据
free();
// 第三步,赋值新数据
elements = data.first;
first_free = cap = data.second;
return *this;
}
T t = {"a", "b", "c"};
// ********************************************************
// 复合赋值运算符
// 作为成员的二元运算符:左侧运算对象绑定到隐式的 this 指针
// 假定两个对象表示的是同一本书
T& T::operator+=(const T &rhs){
......
return *this;
}
下标运算符
下标运算符,必须是成员函数,内置的索引类型是std::size_t,为了与内置的统一,最好返回值类型也是引用。
// 一般要定义两个版本:
// 1、非常量版本
int& T::operator[](std::size_t n){
}
// 2、常量版本
const T& T::operator[](std::size_t n) const {
}
T t;
const T ct;
auto i = t[0]; // 非常量版本
auto ci = ct[0]; // 常量版本
++递增,—递减运算符
应该定义成类的成员。
有前置、后置版本,所以我们也应该重载两个版本。这里有个技巧,一般用前置的版本来实现后置。
从下面的重载实现,我们加深对i++和++i的理解。
class T {
public:
//前置版本
T& operator++();
T& operator--();
//后置版本:int仅用于区分。
T operator++(int);
T operator--(int);
//T p(a1);
//p.operator++(0); // 调用后置版本,这个0是为了让编译器知道。
//p.operator++(); // 调用前置版本
public:
int curr;
}
T& T::operator++(){
++curr; //将curr在当前状态下向前移动一个元素
return *this;
}
T& T::operator--(){
--curr;
return *this;
}
T T::operator++(int){
T ret = *this; //记录当前的值
++*this;
return ret; //返回之前记录的状态
}
T T::operator--(int){
//此处无须检查有效性,调用前置递减运算时才需要检查
T ret = *this; //记录当前的值
--*this;
return ret; //返回之前记录的状态
}
成员访问运算符
->必须是成员,*解引用一般是成员。
class T{
string& operator*() const;
string& operator->() const;
};
string& T::operator*() const{
}
string* T::operator->() const{
return & this->operator*(); //*解引用运算来实现->运算。
}
T *point;
(*point).mem; // point 是一个内置的指针类型
point.operator()->mem; // point 是类的一个对象
函数调用运算符
必须定义成成员。定义了调用运算符的类的对象,叫函数对象,常常用于泛型算法的实参。lambda是函数对象。
struct T {
int operator()(int val) const {
return val < 0 ? -val : val;
} ;
}
int i = -42 ;
T t;
int ui = T(i);
/****************Comp函数对象代替lambda******************/
struct Comp{
Comp(std::size_t sz);
bool operator()( const string &s){
return s.size() >= m_sz;
}
std::size_t m_sz;
};
std::vector<string> words;
std::size_t sz = 10;
find_if(words.begin(), words.end(), //找出第一个不小于sz长度的word
[sz] (const string &s){
return s.size() >= sz;
});
stable_sort(words.begin(), words.end(),Comp(sz)); //用Comp代替lambda。
标准库函数对象
标准库在functional头文件中定义了如下函数对象。
算术 | 关系 | 逻辑 |
---|---|---|
plus |
equal_to |
logical_and |
minus |
not_equal_to |
logical_or |
multiplies |
greater |
logical_not |
divides |
greater_equal |
|
modulus |
less |
|
negate |
less_equal |
plus<int> intAdd; // 可执行int加法的函数对象
negate<int> intNegate; // 可对int值取反的函数对象
int sum = intAdd(10, 20); // 等价于sum=30
sum = intNegate(intAdd(10,20)); // 等价于sum=-30
sum = intAdd(10, intNegate(10)); // sum=0
/*************************在算法中使用函数对象***************************/
// 传入一个临时的函数对象用于执行两个string对象的>比较运算
sort(svec.begin(), svec.end(), greater<string>());
vector<string*> nameTable; // 指针的vector
// 错误:nameTable中的指针彼此之间没有关系,所以<将产生未定义的行为
sort(nameTable.begin(), nameTable.end(),
[](string*a, string*b){
return a < b;
});
// 正确:标准库规定指针的less是定义良好的
sort(nameTable.begin(), nameTable.end(), less<string*>());
function
定义在functional头文件中。是模板,必须提供可调用对象的调用形式(call signature)。
int add(int a, int b){
return a + b;
}
//add函数的调用形式就是int(int, int),或者叫签名。
function描述的是函数对象的类型,只要调用形式(签名)相同,那么在function看来他们就是相同类型的函数对象。
支持的操作
function<T> f; // f是一个用来存储可调用对象的空function,
// 这些可调用对象的调用形式应该与函数类型T相同
function<T> f(nullptr); // 显式地构造一个空function
function<T> f(obj); // 在f中存储可调用对象obj的副本
f // 将f作为条件:当f含有一个可调用对象时为真;否则为假
f(args) // 调用f中的对象,参数是args
// 定义为function<T>的成员的类型
result_type // 可调用对象的返回类型。
argument_type // 只有一个实参时,就是第一个实参的类型,和first同义。
first_argument_type // 第一个实参的类型,如果有的话。
second_argument_type // 第二个实参的类型,如果有的话。
操作例子
// 每个可调用对象表示计算器的一种算法。加减乘除等。
int add(int a, int b){ return a + b; }; // 函数
auto minus = [](int a, int b){ return a - b; }; // lambda
struct multi{ // 函数对象
int operator()(int a, int b){ return a * b; }
};
function<int(int, int)>
function<int(int, int)> f1 = add; // 函数指针
function<int(int, int)> f3 = minus; // lambda
function<int(int, int)> f2 = multi(); // 函数对象类的对象
cout << f1(4, 2) << endl;
cout << f2(4, 2) << endl;
cout << f3(4, 2) << endl;
// map存储这些不同类型的可调用对象
map<string, function<int(int, int)>> binops;
binops.insert({"+", add}); // 函数指针
binops.insert({"-", minus}); // lambda
binops.insert({"x", multi()}); // 函数对象
binops.insert({"%", std::modulus<int>}); // 标准库的函数对象
binops.insert({"/", [](int a, int b){ return a / b;}); // 未命名的lambda
binops["+"](10, 5);
binops["-"](10, 5);
binops["x")(10, 5);
binops["/"](10, 5);
binops["%"](10, 5);
// 需要注意重载函数的情况。
int add(int i, int j) { return i + j; };
T add(const T&, const T&);
binops.insert({"+", add}); // 二义性错误,不知道是哪个add
int (*fp)(int, int) = add;
binops.insert({"+", fp}) ; // 正确:fp指向一个正确的add版本
类型转换运算符
转换构造函数和类型转换运算符共同定义了类类型转换 ( class-type conversions ),这样的转换有时也被称作用户定义的类型转换 ( user-defined conversions) 。
// type:任意类型,除了void,只要能作为函数的返回类型。
// 参数:必须为空
// 返回值:不能有返回值
// 引用限定:一般是const,不修改转换对象。
operator type() const;
operator int() const;
int operator int() const; // 错误:指定了返回类型
operator int(int = 0) const; // 错误:参数列表不为空
class SmallInt {
public:
// 非显式转换构造函数
SmallInt(int i = 0); // 将int转换为SamllInt
// 非显式的类型转换运算符
operator int() const { return val; } // 将SmallInt转换为int
private:
std::size_t val;
}
SmallInt::SmallInt(int i = 0)
:val(i){
if (i < 0 || i > 255){
error();
}
}
SmallInt si;
si = 4; // 首先将4隐式地转换成Smalllnt,然后调用Smallint::operator=
si + 3; // 类型转换运算是非explicit时,才正确,否则错误。
SmallInt si = 3.14; // 1、内置类型转换将double实参转换成int
// 2、调用SmallInt()构造函数
si + 3.14; // 1、SmallInt 的类型转换运算符将 si 转换成 int
// 2、内置类型转换将所得的值让继续转换成 double
显式、隐式类型转换
class SmallInt {
public:
// 非显式的类型转换运算符
operator int() const { return val; } // 将SmallInt转换为int
//显式的类型转换运算符,下面可以看到这两种显式的差别。
explicit operator int() const { return val; } // 将SmallInt转换为int
private:
std::size_t val;
}
si + 3; // 类型转换运算是非explicit时,才正确,否则错误。
static_cast<int>(si) + 3; // 类型转换运算是explicit的
int i = 42;
cin << i; // 如果iostream的类型转换是隐式的,
// 则cin会被转换成bool,bool转成int 1
// 最后变成:1 << i !!
while(std::cin >> value){ // 隐式转换成了bool
}
所以我们最好声明为显式的,也有例外,编译器会用隐式转换代替显式转换:
- if、while、do的条件判断
- for中的条件判断
- 逻辑运算符的运算对象
- 条件运算符的条件表达式
重载运算符的函数匹配
重载运算符要时刻记住,有内置版本的运算符,如果你的类型能隐式转换成内置类型(类型转换),那么类对象和内置类型之间发生运算,就不知道要用哪个版本的运算符了。看下面例子。
class Smallint {
friend Smallint operator+(const Smallint& , const Smallint&) ;
public:
Smallint(int = 0); // int转Smallint
operator int() const { return val; } // Samllint转int
private:
std::size_t val;
}
Smallint s1, s2;
Smallint s3 = s1 + s2; // 使用重载operator+
int i = s3 + 0; // 二义性错误
// 有两个版本的+运算符(重载的和内置的+(int, int))
// s3能转成int,用内置的+
// 0能转成Smallint,用重载的。
所以,我们对同一个类既提供了转换目标是算术类型的类型转换,也提供了重载的运算符, 则将会遇到重载运算符与内置运算符的二义性问题。