重载运算与类型转换
一、运算符重载
1、运算符重载函数的名字由关键字operator后跟运算符组成。重载运算符函数的参数数量应该与该运算符作用的运算对象一样多,即一元运算符有一个参数,二元运算符有两个参数,但是如果运算符函数为成员函数,那么第一个(运算符左侧)对象隐式绑定到this上,因此成员运算符函数的参数数量比运算符的运算对象数量少一个。
2、只能重载已有的运算符,对于运算符重载的参数,必须至少有一个类类型的参数,也就是我们不能对内置类型重载运算符,如:
int operator+(int ,int); //错误,参数至少有一个是类类型
3、运算符重载函数的调用:
①、对于定义为非成员运算符重载函数的调用有两种方式:
a1+a2; //普通表达式
operator+(a1,a2); //函数调用
②、对于定义为成员运算符函数的调用也有两种方式:
a1+=a2;//普通表达式
a1.operator+=(a2);//成员函数调用
4、什么时候把运算符重载函数定义为成员函数,什么时候定义为非成员函数?
①、赋值(=)、下标([])、调用(())和成员访问箭头(->)必须定义为成员函数,而输入输出运算符必须定义为非成员函数。
②、改变对象状态的运算符如:递增,递减,解引用运算符以及与类密切相关的运算符一个定义为成员函数。
③、具有对称性的运算符,也就是可以装换任意一端的运算对象,如:算术、相等性、关系和位运算符等应该定义为普通的非成员函数。
注意:当把运算符重载函数定义为成员函数时,其左侧运算对象必须是该类的一个对象。
接下来对常见的运算符举几个运算符重载的栗子:
1、输出运算符重载
输出运算符重载必须是非成员函数。
class A {
public:
A(int i=0, string s=“”) :age(i), name(s) {};
friend ostream& operator<<(ostream &os, const A& a); //将重载函数声明为类A友元,以便访问其数据成员
private:
int age;
string name;
};
ostream& operator<<(ostream &os, const A& a) //输出运算符重载函数声明为非成员函数
{
os << "姓名:" << a.name<<" "<<"年龄为:" << a.age ;
return os;
};
int main()
{
A a(20, "DD");
cout << a ; //调用输出运算符重载函数
return 0;
}
如上述代码所示:输出运算符含有两个参数,第一个为非常量的ostream对象的引用,第二个是常量的类A对象引用。之所以要是非常量的ostream对象引用,是由于要向ostream对象中写入数据,因此必须是非常量,而ostream对象不能拷贝,因此只能是引用。第二个参数之所以为常量引用,是由于我们只是输入类A对象的内容而不是改变该对象的值,因此为const,另一方面之所以是引用也是避免对对象进行拷贝,减少程序开销。另外返回类型为ostream形参。
2、输入运算符重载函数
与输出运算符一样,输入运算符也应该定义为非成员函数,我们在上述代码的基础上加上输入运算符重载函数:
istream& operator>>(istream &is, A& a)
{
is >> a.age >> a.name;
return is;
}
同样我们需要先在类A中将输入运算符重载函数声明为友元。定义与输出运算符类似,不同之处在于第二个参数是类A的非常量对象引用,这是因为我们需要向该对象中写入数据,因此不能使用const的形参。
3、赋值运算符重载
赋值运算符的重载在第十三章已经讲过,也就是拷贝赋值构造函数以及移动构造函数,这里就不在记录了,值得一提的是:赋值运算符必须定义为类的成员函数。
4、复合赋值运算符
复合赋值运算符可以定义为成员函数,也可以定义为非成员函数,不过最好还是把复合赋值运算符定义为成员函数。复合赋值运算符返回左侧运算对象的引用,对上述类A加上复合运算符重载函数,不过该函数只是作为一个例子,没有任何意义(我们只是对年龄进行了相加)。
A& A::operator+=(const A& r){
age=age+r.age;
return this;
}
//
A a1,a2;
a1+=a2; //右侧的a2作为复合赋值运算符的参数
5、下标运算符重载
下标运算符重载函数必须是成员函数,下标运算符重载函数返回值通常为一个引用,由于返回类型为引用属于左值,所以这样可以使得下标运算符出现在赋值运算符的任意一端。另外通常定义两个下标运算符版本:一个是返回普通引用的非常量成员函数,一个是返回常量引用的常量成员函数。这样可以保证在对常量对象进行下标运算时,防止通过下标改变对象 值。
A& operator[](int n);
const A& operator[](int n) const;
6、算术运算符
通常将算术运算符定义为非成员函数,这样可以使得左侧或右侧对象进行交换,另外由于这类运算符不需要改变运算对象本身的值,因此参数一般为const,另外返回类型为值类型。为上述类A定义一个+运算符:
A operator+(const A& a1, const A& a2){
A temp=a1; //声明一个局部变量
temp+=a2; //调用之前定义的复合赋值运算符
return temp;
}
7、相等运算符
我们重载一个相等运算符operator==,同时也要重载相对应不等运算符operator!=。
bool operator==(const A& a1,const A& a2){
return (a1.age==a2.age&&a1.name==a2.name);
};
bool operator!=(const A& a1,const A& a2){
return !(a1==a2); //调用已经定义的相等运算符,简化程序
};
8、递增、递减运算符
递增、递减运算符一般定义为类的成员函数。
①、定义前置的递增、递减运算符:
同样在上述A类中添加一个前置的递增、递减运算符,前置运算符返回递增、递减后对象的引用。
A& A::operator++(){ //前置递增运算符
age=age+1;
return *this; //返回递增后对象的引用
}
A& A::operator--(){ //前置递减运算符
age=age-1;
return *this: //返回递减后对象的引用
}
②、定义后置的递增、递减运算符:
由于前置和后置运算符使用同一个符号,运算对象类型和数量也相同,唯一区别在于符号一个在前一个在后,那么为了与前置版本区分开来,后置版本的函数接受一个额外的(不会被使用)的int类型形参,当我们使用后置版本的运算符时,编译器会为这个形参传入值为0的实参,这个实参的唯一作用就是区分前置还是后置版本的运算符。另外后置运算符返回递增、递减之前对象的原值,而不是引用
A A::operator++(int){ //后置版本的递增运算符
A a=*this; //首先记录原对象
++*this; //使用之前声明的前置版本的运算符,来简化代码
return a; //返回递增前原对象的值
}
A A::operator--(int){ //后置版本的递减运算符
A a=*this; //首先记录原对象
--*this; //使用之前声明的前置版本的运算符,来简化代码
return a; //返回递减前原对象的值
}
从上述代码可以看出:前置版本的运算符是返回改变后对象的引用,而后置版本的运算符是先用局部变量保存原对象,再对原对象进行递增或者递减操作,最后返回的是保存有原对象的局部变量的值。这也解释了为什么前置版本的运算符返回的是左值(引用),而后置版本的运算符返回的是右值。
9、函数调用运算符
如果一个类定义了调用运算符(()),那么该类的对象称为函数对象,我们可以像使用函数一样调用该类对象,这些对象的行为像函数,也称为仿函数。函数调用运算符必须是成员函数,一个类可以定义多个不同版本的调用运算符。
class Min {
public:
int operator()(int a, int b) { //重载了函数调用运算符,该类对象称为函数对象
return a < b ? a : b;
}
};
int main()
{
int a = 10, b = 20;
Min min;
cout<<min(a, b)<<endl; //像调用函数那样使用对象
return 0;
}
另外i,标准库也定义了几组函数对象,这些类型被定义为模板形式。详见书上第510页。我们用plus来举个栗子:
#include<functional> //包含在头文件functional中
//
plus<int> add; //声明一个plus<int>对象add,且形参类型为int型
int sum=add(50,50); //像调用函数那样使用函数对象add
10、类型转换运算符
类型转换运算符必须是类的成员函数,不能声明返回类型,形参列表也必须为空,并且转换函数不应该改变待转换对象的内容,因此一个定义为const。其声明形式为:
operator type() const; //没有返回值类型,没有形参,声明为const。
举个简单的栗子:
class ChangeInt{
public:
operator int(){ return num; }; //类型转换运算符,转换为int型。
private:
short num;
}
二、可调用对象与function
C++中可调用的对象有:函数、函数指针、lambda表达式、bind创建的对象、重载了函数调用运算符的类(函数对象)。(lambda表达式和bind的知识后面再详细记录吧)。不同的类型可能具有相同的调用形式,如:
int add(int i,int j) { return i+j; } //普通函数
auto mod=[](int i,int j) { return i%j: } //lambda表达式
class minus{ //函数对象类
public:
int operator()(int i,int j){
return i-j;
}
};
如上述代码所示:尽管三种可调用的对象类型不同,但是具有相同的调用形式:
int (int,int) //调用形式,即具有两个int型的形参,返回值类型为int型
如果我们想把以上几种可调用对象放在一个函数表map中,如何实现呢?
比如我们定义一个运算符到函数指针的关系的map:
map<string, int(*)(int,int)> func;
func.insert("+",add); //正确
func.insert("%",mod); //错误,因为mod不是函数指针
由于我们定义的func的键类型为string类型,而值类型为函数指针类型,但是mod和minus都不是函数指针类型,尽管他们调用形式相同,但是不能放在func中。那么我们如何利用他们调用形式相同这一特征呢?
解决上述的办法就是使用标准库function类型,function是一个类模板,类型为调用形式,只要调用形式一致的可调用对象,都可以转换为function对象。如:
#include<functional> //包含在头文件functional中
function<int(int,int)> f1=add;
function<int(int,int)> f2=mod;
function<int(int,int)> f3=minus();
map<string,function<int(int,int)>> func;
func.insert({"+",f1});
func.insert({"%",f2});
func.insert({"-",f3});
其实就是利用他们调用形式一致这一特征,将调用形式一致的可调用对象放在了一起。对于上述代码,如果想使用加法运算,可以这样调用:
func["+"](50,50);//使用func["+"]找到add,紧接着传入两个实参,add(50,50);