运算符重载
C++允许将运算符重载扩展到用户定义的类型,C++将会根据操作数的数目和类型来决定使用哪种操作,运算符重载的格式如下:
operatorop(argumet-list)
即由关键字operator,运算符op,()里的参数列表三部分组成,op必须是有效的C++运算符
例如:operator+(),这里代表着要重载+运算符
我们可以通过一个例子来看看运算符重载的便利
我们不使用运算符重载进行两个对象的计算
cpp#include<iostream> using namespace std; class Time { private: int hours; int minutes; public: Time() { hours = minutes = 0; } Time(int h, int m = 0) { hours = h; minutes = m; } Time Sum(const Time &t) const //Time operator+(const Time &t) const { Time sum; sum.minutes = minutes + t.minutes; sum.hours = hours + t.hours + sum.minutes / 60; sum.minutes %= 60; return sum; } void show() { cout<<"hours: "<<hours<<"minutes: "<<minutes<<endl; } }; int main() { Time a(10,55); Time b(1,20); a.show(); b.show(); Time c = a.Sum(b); c.show(); return 0; }
这样就只能通过常规的函数方法来进行求和,即必须通过访问成员函数的方式,而我们使用运算符重载时
我们就能够直接使用+运算符进行计算
我们也可以将operate+()当做一个函数来理解
两者的运算结果都是相等的
总之
operator+()函数的名称使得使用函数表示法或者运算符表示法来调用它,编译器会根据操作数的类型来决定如果进行调用
注意:
在上述的代码中,Sum函数的返回值不能是一个引用,因为我们将引用作为参数是为了提高效率,如果按值传递Time对象,两种的功能相同,但是传递引用,速度更快,使用的内存更少,但是返回值却不能是引用,因为函数会创建一个新的对象来表示两个对象的和,返回对象将会创建对象的副本,则函数调用可以使用它,如果传递的是对象的引用,因为我们定义的对象sum是局部变量,这个变量会在函数结束时被删除,因此引用会指向一个不存在的对象
这样的结果是会导致编译器发生警告
运算符重载限制
C++对用户自定义的运算符重载是有着一定的限制
1.重载后的运算符必须至少有一个操作数是用户自定义的类型,这将会防止用户为标准类型重载运算符,例如将减法运算符重载为计算两个整型的和
2.使用运算符不能违反原来的句法规则,例如不能将%运算符重载为只使用一个操作数,也不能修改运算符的优先级,就像我们上面的例子,将+运算符重载为两个类的相加,得到的新运算符与原来的+具有同样的优先级
3.不能创造新的运算符
4.部分的运算符不允许重载(sizeof运算符,.成员运算符,.*成员指针运算符,::作用域解析运算符,?:条件运算符等)
5.大部分的运算符可以通过成员函数或者非成员函数进行重载,例如
注意:
我们使用非成员函数时是访问不到私有的n的,只能将其改为公有的部分或者通过成员函数才能进行访问,这里是将其改为公有部分去访问(这样做非常不好,尽量不要这么去使用)
但是( =赋值运算符,()函数调用运算符,[]下标运算符,->通过指针访问类成员运算符)只能通过成员函数进行重载
我们来看一串代码,就可以知道为什么=赋值运算符,()函数调用运算符,[]下标运算符,->通过指针访问类成员运算符只能通过成员函数进行重载
这个代码的输出结果为
我们对这个代码进行分析一下:
在main函数中,我们先使用Num创建了一个对象a,此时我们并没有对其进行初始化,因此会调用默认构造函数,进而打印出No,但是,第二条语句a=10好像是把我们赋值运算符的功能进行了重载,否则的话,这条语句就是尝试将一个整型的常量去赋值给一个Num类的对象,这会导致类型不匹配,但是我们编译却没有出错,因此,我们可以得知,当我们的类中没有定义赋值运算符重载的功能时,但是在代码中出现了赋值运算,程序就会调用与赋值运算符右侧值类型匹配的构造函数,因此输出的结果为YES,此时我们可以将a=10就理解为一个a(10)的操作
但是,当我们的类里有运算符重载的操作时,程序就只会去调用我们定义的重载函数
此时我们就编写了一个赋值运算符重载的函数,当出来赋值操作时,编译器就会去调用我们编写的函数,而不是再去调用参数类型匹配的构造函数
注意:
如果我们不在类里面去定义一个赋值运算符重载的函数,而是定义了一个非成员函数的赋值运算符重载的函数,这样会导致编译器报错,因为有构造函数也可以满足我们的赋值操作,编译器不知道要去调用哪一个函数,而加号运算符不会出现这样的操作,因为它们不会去调用构造函数,这就是为什么=赋值运算符,()函数调用运算符,[]下标运算符,->通过指针访问类成员运算符)只能通过成员函数进行重载的原因
友元
友元的介绍
C++控制对于类对象私有部分的访问,通常,公有类方法提供是唯一的访问途径,但是有时候这种限制太过严格,以至不适用于某些编程问题,在这种特定的情况下,C++提供了另外一种形式的访问权限------友元,友元有三种
友元函数
友元类
友元成员函数
通过让函数成为类的友元,可以赋予该函数与类成员函数相同的访问权限,即可以访问到类的私有部分的成员
为什么需要友元
在为类重载二元运算符时,通常需要友元,例如下面这个代码
我们上面进行了+运算符的重载,在这个基础上再加上*运算符的重载
cpp#include<iostream> using namespace std; class Time { private: int hours; int minutes; public: Time() { hours = minutes = 0; } Time(int h, int m = 0) { hours = h; minutes = m; } //Time Sum(const Time &t) const Time operator+(const Time &t) const { Time sum; sum.minutes = minutes + t.minutes; sum.hours = hours + t.hours + sum.minutes / 60; sum.minutes %= 60; return sum; } Time operator*(double num) { Time result; long total = hours * num *60 + minutes * num; result.minutes = total % 60; result.hours = total / 60; return result; void show() { cout<<"hours: "<<hours<<" minutes: "<<minutes<<endl; } }; int main() { Time a(1,30); Time b(1,20); Time d; Time c; a.show(); b.show(); d = a + b; c = b * 2.0; c.show(); }
在这个例子中,加法运算符的重载结合的是两个Time值,而乘法运算符将一个Time类的对象与一个double类型的值进行了结合,这就限制了该运算符的使用方式,左侧的操作数b是调用对象
c = b * 2.0;
将会被转化为
c = b.operator*(2.0);
那么如果出现了 c = 2.0 * b;这条语句会发生什么呢?
从概念上来说,b*2.0与2.0*b应该是相同的,但是第一个表达式的值不对应与成员函数,因此2.0不是Time类的对象,因此,编译器不能使用成员函数调用来替换这个表达式,如果使用这样进行编译时,编译器会进行明确的报错
解决这个问题的方法
1.告知所有人只能按照b*2.0这样的格式编写,但是这样对我们并不友好
2.再编写一个非成员函数,让2.0*b这种类型也能匹配,但是这样又会出现一个问题,即我们非成员函数无法去访问我们的私有成员,例如下图
定义了一个非成员函数,但是在非成员函数里尝试去访问Time类中的私有成员,这样编译器就会发出警告,消除警告可以像我们上面所说的将私有成员该为公有成员(非常不好)
由于定义非成员函数不能直接去访问类里面的私有成员,那么我们应该怎么让非成员函数也能够去访问类的私有成员呢?
使用友元函数,就可以去访问类的私有成员
创建友元
友元函数的创造步骤如下
1.将函数的原型放在类声明中,并在声明前加上关键字friend
2.编写函数定义,因为不是类成员函数,因此不需要加上类和作用域解析运算符,此外,在函数定义中也不需要加上关键字friend
按照我们上述的问题,编写能够匹配2.0*b的非成员函数,定义为友元函数
在类的声明中
非成员函数的定义
常用友元:重载<<运算符
一个很有用的类的特性是,可以对<<运算符进行重载,使之能和cout一起来显示对象的内容,例如我们前面使用过的成员函数show(),假如我们有一个Time类的对象tip,如果我们可以使用下面这种方式来显示会比使用show来显示更好
cout<<trip;
我们可以这样做是因为<<是可以被重载的运算符,最初<<运算符是C和C++的位运算符符,ostream类对这个运算符进行了重载,将其转化为了一个输出工具,而cout是一个ostream类的对象,它是智能的,能够识别所有的C++基本类型,因为对于每种基本类型来说,ostream类声明中都包含了相对应的重载的operator<<()定义,因此,要使cout能够识别Time对象,一种方法就是讲一个新的函数运算符定义添加到ostream类声明中,但是这样非常不好,这样会在标准接口上浪费时间,相反,通过Time类声明来让Time类知道如何使用cout,即在Time类里面进行重载,方法如下所示:
1.<<的第一种重载
在Time类中声明
定义函数
这样就可以使用cout<<trip
2.<<的第二种重载
我们上面所写的第一种重载无法进行连续的输出,例如
这样编译器会发出警告
因为C++从左往右读取输出语句,例如int x = 1, y = 2;
cout<<x<<y;等同于(cout<<x)<<y;在这里(cout<<x)的返回值类型还是cout类型,因此还能继续输出结果,而上面定义的输出的结果是void类型,因此无法满足连续的输出,为了保证我们能够连续的输出,我们就要对第一种重载的返回值进行更改.
声明在Time类中
定义operator<<()
调用函数
在我们定义函数时,我们可以不像之前的代码一样,先定义一个临时的类的对象,对其进行操作之后再返回,而是直接使用构造函数去返回,这样写比较简洁,即如下
这样相当于创建了一个无名的新对象,并返回了该对象的副本,这就确保了新的Vector对象是根据构造函数制定的标准规则创建的
提示:
如果方法通过计算得到一个新的类对象,则应该考虑是否可以使用构造函数来完成这个工作,这样可以避免麻烦,并且确保新的对象是按照正确的方式创建的
类的自动类型转换和强制转换
自动类型转换
我们通过下面这个例子来看看C++类的自动转换
在这个程序中,我们定义了一个Sto类,接下来我们使用这个类来创造一个对象a和b,但是在运用时却出现了b = 136.2,在这条语句中,程序将自动使用Sto(double n2)这个构造函数来创建一个临时的对象,随后采用逐成员赋值的方式将该临时对象的内容复制到b中,这一个过程是隐式进行的,因此不需要进行强制类型转换
C++新增的关键字explicit可以关闭类的自动类型转换
只需要在构造函数前加上explicit关键字,这样关闭隐式的转换之后,再出现这样的赋值语句就会出错
转换函数
我们在类的自动转换中将一个数字赋值给我们类的对象,我们在这里还可以进行相反的操作,即将一个类的对象赋值给我们的变量,这需要使用到转换函数
转换函数是用户自定义的强制类型转换,可以像使用强制类型转换那样去使用它们
转换函数的格式如下:
operator 要转换的类型();
注意:
1.转换函数必须是类方法
2.转换函数不能指定返回类型,因为要转换的类型指出了我们要将类转换为什么类型,因此不需要指定返回
3.转换函数不能有参数,因为转换函数是类方法,它需要通过类对象来调用,从而告知了函数要转换的值,因此不需要参数
我们按照上面的例子定义转换函数并调用
当出现强制类型转换语句时,程序就会去找相匹配的转换函数
在C++11中,可以对转换函数使用explicit关键字,这样就只能进行显式的类型转换,隐式的类型转换就不能发生
总结
C++为下面的类提供了类型转换
1.只有一个参数的类构造函数将会用于将类型与参数相同的值转换为类类型
2.转换函数是特殊的类成员运算符函数,用于将类对象转换为其他对象,转换函数是类的成员,没有参数,没有返回类型,该函数能自动被调用