《C++ Primer》学习笔记(第十四章)——重载运算与类型转换

重载运算与类型转换

一、运算符重载
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);
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值