1.重载的运算符
(1).重载运算符函数的参数数量与该运算符作用的运算对象数量一样多。
(2).除了重载的函数调用运算符operator()之外,其他重载运算符不能含有默认实参。
(3).对一个重载的运算符,其优先级和结合律与对应的内置运算符保持一致。
(4).当一个重载的运算符是成员函数时,this绑定到左侧运算对象。故,成员运算符函数的参数数量比运算对象数量少一个。
(5).对一个运算符函数,或者是类的成员,或者至少含一个类类型的参数。
2.运算符函数的调用形式:
// 假设有非成员函数 operator+
显式:operator+(data1, data2);
隐式:data1+data2
// 假设有成员函数 operator+
显式:data1.operator+(data2)
隐式:data1+data2
3.关于将运算符函数定义为成员还是非成员:
赋值(=
),下标([]
),调用(()
),成员访问箭头(->
)必须为成员。
4.运算符重载
4.1.重载<<
(1).通常,输出运算符的第一个形参是一个非常量ostream对象的引用。
a.为引用,是因为ostream对象不能被拷贝。
b.非常量是因为,向流写入内容会改变其状态。
(2).第二个形参一般是一个常量的引用,该常量是我们想打印的类类型。
a.是引用,是因为可以避免值拷贝。
b.常量是因为,打印对象不会改变对象的内容。
(3).为与其他输出运算符一致,operator<<一般返回它的ostream形参。
(4).一个实例
#include <iostream>
class A
{
public:
int m_a;
int m_b;
};
std::ostream &operator<<(std::ostream &os, const A&item)
{
os << item.m_a << " " << item.m_b;
return os;
}
4.2.重载>>
(1).通常,输入运算符的第一个形参是运算符将要读取的流的引用,
(2).第二个形参是将要读入到的非常量对象的引用。
(3).该运算符通常会返回某个给定流的引用。
(4).一个实例
#include <iostream>
class A
{
public:
int m_a;
int m_b;
};
std::istream& operator>>(std::istream &is, A& item)
{
char a;
is >> item.m_a >> a >> item.m_b;
return is;
}
执行输入时,可能发生下列错误:
(1).流含有错误类型的数据时,读取操作可能失败。
(2).读取操作到达文件末尾或遇到输入流的其它错误时也会失败。
4.3.算术运算符
4.3.1.最佳实践
(1).类定义了一个算术运算符时,一般也要定义一个复合赋值运算符。
(2).一般借用复合赋值运算符来定义算术运算符。
(3).通常,算术和关系运算符定义为非成员函数以允许左侧或右侧的运算对象进行转换。
(4).这些运算符一般不需改变运算对象的状态,故形参可以是常量的引用。
(5).算术运算的结果一般位于局部变量内。操作完成一般返回局部变量的副本作为结果。
(6).复合赋值运算符更适合作为类成员来定义。
(7).一个实例
#include <iostream>
class A
{
friend bool operator==(const A &a1, const A &a2);
public:
A():m_a(0), m_b(0){}
A(int i):m_a(i), m_b(i){}
void print()
{
printf("a_%d,b_%d\n", m_a, m_b);
}
A& operator+=(const A &a2)
{
m_a = m_a + a2.m_a;
m_b = m_b + a2.m_b;
return *this;
}
private:
int m_a;
int m_b;
};
A operator+(const A &a1, const A &a2)
{
A sum = a1;
sum += a2;
return sum;
}
bool operator==(const A &a1, const A &a2)
{
return a1.m_a == a2.m_a && a1.m_b == a2.m_b;
}
bool operator!=(const A& lhs, const A& rhs)
{
return !(lhs == rhs);
}
int main()
{
A a1(10);
A a2(10);
A a3 = a1 + a2;
a3.print();
return 0;
}
上述是一个为类型A定义运算符+,+=,==,!=
的实例。我们采用类成员定义复合赋值。
下述是一个复合赋值采用非成员的版本。
#include <iostream>
class A
{
friend bool operator==(const A &a1, const A &a2);
friend A operator+=(A &a1, const A &a2);
public:
A():m_a(0), m_b(0){}
A(int i):m_a(i), m_b(i){}
void print()
{
printf("a_%d,b_%d\n", m_a, m_b);
}
private:
int m_a;
int m_b;
};
// 返回值是复合赋值作为表达式的结果
// 参数1是复合赋值左边操作数,此操作数会被复合赋值所影响。
A operator+=(A &a1, const A &a2)
{
a1.m_a += a2.m_a;
a1.m_b += a2.m_b;
return a1;
}
A operator+(const A &a1, const A &a2)
{
A sum = a1;
sum += a2;
return sum;
}
bool operator==(const A &a1, const A &a2)
{
return a1.m_a == a2.m_a && a1.m_b == a2.m_b;
}
bool operator!=(const A& lhs, const A& rhs)
{
return !(lhs == rhs);
}
int main()
{
A a1(10);
A a2(10);
A a3 = a1 + a2;
a3.print();
return 0;
}
4.4.关系运算符
4.4.1.最佳实践
(1).通常,关系运算符定义为非成员函数以允许左侧或右侧的运算对象进行转换。
(2).这些运算符一般不需改变运算对象的状态,故形参可以是常量的引用。
以下是一个关系运算符重载的例子
#include <iostream>
class A
{
friend bool operator==(const A& a1, const A& a2);
public:
A():m_a(0),m_b(0){}
A(int i):m_a(i),m_b(i){}
private:
int m_a;
int m_b;
};
bool operator==(const A& a1, const A& a2)
{
if(a1.m_a == a2.m_a && a1.m_b == a2.m_b)
{
return true;
}
else
{
return false;
}
}
bool operator!=(const A& a1, const A& a2)
{
return !(a1 == a2);
}
int main()
{
A a1(1), a2(2);
printf("a1==a2?%d\n", a1 == a2);
return 0;
}
4.5.赋值运算符
(1).赋值运算符限制性因素有:
赋值作为表达式存在一个结果数值。
赋值会修改左边操作数状态。
(2).最佳实践
a,赋值一般实现为成员函数
b.为类型自定义赋值运算符时,运算符内部必须处理:
b.1.基类部分赋值
b.2.类型成员赋值
c.为类型自定义赋值运算符时,需要考虑自赋值场景。
d.赋值与拷贝针对包含资源的类涉及浅拷贝,深拷贝的概念。又涉及到左值,右值,移动语义,完美转发的概念。这些后续统一放在专门一篇讲解。
(3).一个实例
#include <iostream>
class Norm1
{
public:
Norm1(char i):m_i(i){}
void print()
{
printf("norm1:%c\n", m_i);
}
private:
char m_i;
};
class Norm2
{
public:
Norm2(char i):m_i(i){}
void print()
{
printf("norm2:%c\n", m_i);
}
private:
char m_i;
};
class Base
{
public:
Base(char c = 'b'):m_n1(c),m_n2(c){}
void print()
{
printf("base:\n");
m_n1.print();
m_n2.print();
}
private:
Norm1 m_n1;
Norm2 m_n2;
};
class A : public Base
{
public:
A(char c = 'a'):Base(c), m_n1(c+1),m_n2(c+1){}
A& operator=(const A& a)
{
if(this == &a)
{
return *this;
}
Base::operator=(a);
m_n1 = a.m_n1;
m_n2 = a.m_n2;
return *this;
}
void print()
{
Base::print();
m_n1.print();
m_n2.print();
}
private:
Norm1 m_n1;
Norm2 m_n2;
};
int main()
{
A a1('x');
A a2('a');
a1.print();
a2.print();
a1 = a2;
printf("after:\n");
a1.print();
a2.print();
return 0;
}
4.6.下标运算符
对下标运算符最好定义常量和非常量版本,对常量版本返回常量引用。
4.6.1.强制性约束
从强制性约束角度要求形参列表只能有一个形参
#include <iostream>
class Norm
{
public:
void print() const
{
printf("Norm_%d\n", m_i);
}
private:
int m_i;
};
class A
{
public:
int operator[](Norm n)
{
printf("test[]\n");
return 1;
}
private:
Norm m_arrA[10];
};
int main()
{
A a;
Norm n;
int i = a[n];
return 0;
}
上述是一个合法的重载的[]
运算符。
4.6.2.最佳实践
(1).形参最好是std::size_t。
(2).重载运算符最好提供常量版本,非常量版本。
这样对类型的常量实例,非常量实例均可执行[]
运算。
(3).返回值应该设计成引用类型。
标准库类型[]
运算符也均返回引用类型。
#include <iostream>
class Norm
{
public:
void print() const
{
printf("Norm_%d\n", m_i);
}
private:
int m_i;
};
class A
{
public:
Norm &operator[](std::size_t i)
{
printf("[]\n");
return m_arrA[i];
}
const Norm &operator[](std::size_t i) const
{
printf("[]const\n");
return m_arrA[i];
}
private:
Norm m_arrA[10];
};
int main()
{
A a;
a[1].print();
const A a1 = A();
a1[1].print();
return 0;
}
上述是一个[]
运算符重载的最佳实例
4.7.递增和递减运算符
4.7.1.强制性约束
(1).对后置版本的++,--
,形参列表必须提供单个类型为int的形参。
(2).对前置版本的++,--
,形参列表必须为空。
#include <iostream>
class A
{
public:
A() : m_i(0) {}
int operator++()
{
printf("++()\n");
m_i++;
return 1;
}
int operator++(int n)
{
printf("++(int)\n");
A temp = *this;
m_i++;
return 1;
}
private:
int m_i;
};
int main()
{
A a;
a++;
++a;
return 0;
}
上述是一个合法的前置++,后置++
运算符的重载实现。
4.7.2.最佳实践
(1).定义++,--
运算符时候,应该同时定义前置版本,后置版本。
(2).前置版本应该返回类型自身的引用。后置版本应该返回类型自身。
与标准库类型保持一致。
(3).不应该提供常量版本。
#include <iostream>
class Norm
{
private:
int m_i;
};
class A
{
public:
A() : m_i(0) {}
A &operator++()
{
printf("++()\n");
m_i++;
return *this;
}
A operator++(int)
{
printf("++(int)\n");
A temp = *this;
m_i++;
return temp;
}
private:
int m_i;
};
int main()
{
A a;
a++;
++a;
return 0;
}
上述是一个前置++,后置++的最佳实践。
4.8.成员访问运算符:*,->
4.8.1.强制性约束
(1).对*
运算符,必须提供0个形参或1个形参。
(2).对->
运算符,必须提供0个形参。
#include <iostream>
class Norm
{
public:
Norm(int i) : m_i(i) {}
void print()
{
printf("i_%d\n", m_i);
}
private:
int m_i;
};
class A
{
public:
int operator*(Norm)
{
printf("*\n");
return 1;
}
int operator->()
{
printf("->\n");
return 2;
}
private:
int m_i;
};
int main()
{
A a;
A *p = &a;
Norm n(1);
p->operator->();
p->operator*(n);
a.operator*(n);
a.operator->();
// a->print();
//*a;
return 0;
}
上述我们为类型定义了一个合法的*
运算符,一个合法的->
运算符。
4.8.2.最佳实践
(1).*
运算符的形参应该是0个。
因为只有这样,才能使用A a; *a;
这样的方式触发*
运算符访问。
(2).->
运算符的返回类型,应该要么是重载了->运算符的类型,要么是指针类型。
因为如下述,我们执行A a; a->print();这样的操作时。操作处理细节为:
1.若a重载了->
运算符。则执行运算符,获得返回值。否则,报错。
2.若返回值是指针类型,进入到对指针类型执行->
情形。终止。
3.若返回值不是指针类型且重载了->
运算符,则执行运算符,获得返回值。否则,报错。
4.进入2。
我们下述的a->print()
对应的输出为:
#include <iostream>
class Norm
{
public:
Norm(int i) : m_i(i) {}
Norm *operator->()
{
printf("norm ->\n");
return this;
}
void print()
{
printf("i_%d\n", m_i);
}
private:
int m_i;
};
class A
{
public:
Norm operator*()
{
Norm n(1);
printf("*\n");
return n;
}
Norm operator->()
{
Norm n(11);
printf("->\n");
return n;
}
private:
int m_i;
};
int main()
{
A a;
A *p = &a;
p->operator->();
p->operator*();
a.operator*();
a.operator->();
a->print();
*a;
return 0;
}
4.8.3.注意点
以4.8.2中实例为说明对象。整理c++类型中成员访问细节。
int main()
{
A a;
A *p = &a;
p->operator->();
p->operator*();
a.operator*();
a.operator->();
a->print();
*a;
return 0;
}
(1).针对指针类型,基于指针执行->
可以访问到指针指向对象的成员。此行为不允许重载。
(2).针对非指针类型,称其为对象类型,基于对象类型执行.
可以访问到对象的成员。此行为不允许重载。
(3).调用类型*,->
运算符的形式有显式和隐式。
a.显式调用诸如:
int main()
{
A a;
A *p = &a;
p->operator->();
p->operator*();
a.operator*();
a.operator->();
return 0;
}
b.隐式调用诸如:
int main()
{
A a;
a->print();
*a;
return 0;
}
4.9.函数调用运算符
4.9.1.强制性约束
对形参列表无强制性约束。
#include <iostream>
class A
{
public:
A() {}
int operator()(int)
{
printf("(int)\n");
return 0;
}
int operator()(int, int)
{
printf("(int,int)\n");
return 1;
}
private:
int m_i;
};
int main()
{
A a;
a(1);
a(1, 2);
return 0;
}
上述我们定义了两个函数调用运算符,分别接收一个参数,两个参数。
定义类函数调用运算符的类,类的对象称作函数对象。
4.10.转换构造函数
4.10.1.强制性约束
我们称只传递一个实参即可调用的构造函数为转换构造函数
#include <iostream>
class A
{
public:
A(int i, int j = 0) : m_i(i)
{
printf("A(int)_%d\n", i);
}
private:
int m_i;
};
void fun(A a)
{
}
int main()
{
fun(1);
return 0;
}
上述我们定义了一个转换构造函数。因为有了此转换构造函数,上述fun(1)
执行中先基于1构造得到类型A的实例,再执行函数调用。
若要阻止这类默认转换,只需将转换构造用explicit
修饰。此时,只能通过显式构造基于int得到类型A实例。
#include <iostream>
class A
{
public:
explicit A(int i, int j = 0) : m_i(i)
{
printf("A(int)_%d\n", i);
}
private:
int m_i;
};
void fun(A a)
{
}
int main()
{
fun(A(1));
return 0;
}
4.11.类型转换运算符
4.11.1.强制性约束
类型转换运算符的形参列表必须为空
#include <iostream>
class Norm
{
public:
private:
char m_c;
};
class A
{
public:
explicit A(int i, int j = 0) : m_i(i)
{
printf("A(int)_%d\n", i);
}
operator int()
{
printf("int()\n");
return m_i;
}
operator Norm()
{
printf("Norm()\n");
Norm n;
return n;
}
private:
int m_i;
};
void fun(A a)
{
}
int main()
{
fun(A(1));
A a(1);
int n = 1 + a;
printf("n_%d\n", n);
Norm nn = a;
return 0;
}
上述我们定义了两个类型转换运算符,分别转换到int,Norm
类型。