您的位置:首页 > 编程语言 > C语言/C++

实习笔记(六)C++运算符重载

2018-01-23 18:07 393 查看
类型转换运算符(conversion operator)

是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型,

格式:operator type() const;

其中type表示某种类型,类型转换运算符面向任意类型(void除外)进行定义,只要该类型能作为函数返回值类型。不允许转换成函数和数组类型,但允许转换成指针类型或引用类型。

类型转换运算符既没有显示的返回值类型,也没有形参,必须定义为类的成员函数。类型转换运算符通常不应该改变转换对象的内容,因此,类型转换运算符一般被定义为const成员。



运算符重载的规则:

(1)为了防止用户对标准类型进行运算符重载,C++规定重载后的运算符的操作对象必须至少有一个是用户定义的类型

这是什么意思呢?

比如说现在有两个数:int number1,int number2,

那么number1+number2 求的是两个数的和,

但是如果你重载以后让着两个数相加为他们的乘积,这肯定是不合乎逻辑的。

可能重载以后会有二义性,导致程序不知道该执行哪一个(是自带的的还是重载后的函数)

(2)使用运算符不能违法运算符原来的句法规则。如不能将% 重载为一个操作数,

例如:

int index;

%index;这种是不被允许的。

(3)不能修改运算符原先的优先级。

(4)不能创建一个新的运算符,例如不能定义operator** (···)来表示求幂

(5)不能进行重载的运算符:成员运算符,作用域运算符,条件运算符,sizeof运算符,typeid(一个RTTI运算符),const_cast、dynamic_cast、reinterpret_cast、static_cast强制类型转换运算符

(6)大多数运算符可以通过成员函数和非成员函数进行重载但是下面这四种运算符只能通过成员函数进行重载:

= 赋值运算符,()函数调用运算符,[ ]下标运算符,->通过指针访问类成员的运算符。

(7)除了上述的规则,其实我们还应该注意在重载运算符的时候遵守一些明智的规则:例如:不要将+运算符重载为交换两个对象的值。

重载运算符的两种形式:

重载运算符有两种方式,即:

重载为类的成员函数||重载为类的非成员函数。

重载为类的非成员函数的时候:

通常我们都将其声明为友元函数,因为大多数时候重载运算符要访问类的私有数据,(当然也可以设置为非友元非类的成员函数。但是非友元又不是类的成员函数是没有办法直接访问类的私有数据的),如果不声明为类的友元函数,而是通过在此函数中调用类的公有函数来访问私有数据会降低性能。所以一般都会设置为类的友元函数,这样我们就可以在此非成员函数中访问类中的数据了。

那么最重要的问题来了,我们什么时候声明为成员函数,什么时候声明为非成员函数呢?

首先,我们要明白这句话:对于成员函数来说,一个操作数通过this指针隐式的传递,(即本身),另一个操作数作为函数的参数显示的传递;对于友元函数(非成员函数)两个操作数都是通过参数来传递的。

(1)一般来说,单目运算符重载为类的成员函数,双目运算符重载为类的友元函数(咳咳,一般情况下)

(2)双目运算符不能为 = 。 ()【】。-> 重载为类的友元函数。

(3)如果运算符的第一次操作数要求为隐式转换则必须为友元函数。

(4)当最左边的要求为类对象,而右边的是一个内置类型,则要为友元函数。

1) 成员函数运算符

 运算符重载为类的成员函数的一般格式为:

<函数类型> operator <运算符>(<参数表>)

{

<函数体>

}

 当运算符重载为类的成员函数时,函数的参数个数比原来的操作数要少一个(后置单目运算符除外),这是因为成员函数用this指针隐式地访问了类的一个对象,它充当了运算符函数最左边的操作数。因此:

(1) 双目运算符重载为类的成员函数时,函数只显式说明一个参数,该形参是运算符的右操作数。

(2) 前置单目运算符重载为类的成员函数时,不需要显式说明参数,即函数没有形参。

(3) 后置单目运算符重载为类的成员函数时,函数要带有一个整型形参。

调用成员函数运算符的格式如下:

<对象名>.operator <运算符>(<参数>)

它等价于

<对象名><运算符><参数>

例如:a+b等价于a.operator
+(b)。一般情况下,我们采用运算符的习惯表达方式。

2) 友元函数运算符

 运算符重载为类的友元函数的一般格式为:

friend <函数类型> operator <运算符>(<参数表>)

{

<函数体>

}

当运算符重载为类的友元函数时,由于没有隐含的this指针,因此操作数的个数没有变化,所有的操作数都必须通过函数的形参进行传递,函数的参数与操作数自左至右一一对应。

 调用友元函数运算符的格式如下:

operator <运算符>(<参数1>,<参数2>)

它等价于

<参数1><运算符><参数2>

例如:a+b等价于operator
+(a,b)。

运算符的重载是c++语言特有的,java什么的是没有运算符重载的,所以运算符重载在笔试面试中有可能成为c++的高频考点。运算符重载就是重新定义运算符的意义,如常用的+,-,×,÷都是可以重载的。运算符重载使用的是关键字operator,表现形式是:

返回值 operator重载的运算符(函数参数列表)

举一个简单的例子,有一个分数类,定义如下:



[cpp] view
plain copy

class Fraction{

public:

int x;//分子

int y;//分母

Fraction(int x = 0,int y = 1):x(x),y(y){

}

void show(){

cout << x <<"/" << y << endl;

}

}

比如定义了两个类Fraction fa,fb,你做一个fa+fb肯定是错误的,因为编译器会提示你+它不认识,但是这种分数相加在现实中又是非常合理的事情。所以我们必须要重新定义+的意思,以满足+在这种情况下的使用。在这里就该被这样定义;

Fraction operator+(const Fraction& fa,const Fraction& fb)

那么现在我们要明白的是,当编译器遇到fa+fb这种情况,编译器是如何去解析的?解析的规则如下:

碰到fa + fb的两种解析规则,首先去类的成员函数中找一个函数operator+(constFraction&
fb),找不到则去全局区找一个全局函数operator+(constFraction& fa, const Fraction& fb),这里的意思就是说,我们可以把重载函数写成成员函数的形式,也可以写成全局函数的形式,博主建议,能写成成员函数的就不要写成全局函数,而且最好只选取一种方案。上面的红色字体部分就是全局形式的+重载。


知道了上面的原理之后,我们首先来采用全局形式的重载。



[cpp] view
plain copy

Fraction addFraction(const Fraction& fa,const Fraction& fb){

Fraction fc;

fc.x = fa.x * fb.y + fa.y * fb.x;

fc.y = fa.y * fb.y;

return fc;

// return Fraction(fa.x * fb.y + fa.y * fb.x,fa.y * fb.y);//简写

}

[cpp] view
plain copy

/*下面的代码将提供对分数类Fraction的+,-,*,/的重载*/

[cpp] view
plain copy

/*运算符重载,分数类的运算符重载*/

#include <iostream>

using namespace std;

class Fraction{

public:

int x;

int y;

Fraction(int x = 0,int y = 1):x(x),y(y){

}

/*只要能写成员函数就不写全局函数,编译器遇到fa+fb的时候,首先是fa,然后

碰到+号所以说是按照顺序来的,即是fa调用了重载的成员函数,所以里面的this就是

fa对象*/

Fraction operator+(const Fraction& fb){

Fraction c ;

cout << this->x << this->y << fb.x << fb.y << endl;

c.x = this->x * fb.y + this->y * fb.x;

c.y = this->y * fb.y;

return c;

//return Fraction(this->x * fb.y + this->y * fb.x,this->y * fb.y);

}

/*设计成员函数完成两个分数的相减*/

Fraction operator-(const Fraction& fb){

return Fraction(this->x * fb.y - this->y * fb.x,this->y * fb.y);

}

/*设计成员函数实现两个分数相乘*/

Fraction operator*(const Fraction& fb){//可以设计成任意返回值类型

return Fraction(this->x * fb.x,this->y * fb.y);

}

/*完成两个分数*=*/

void operator*=(const Fraction& fb){

this->x *= fb.x;

this->y *= fb.y;

}

void show(){

cout << x <<"/" << y << endl;

}

};

/*设计一个分数相加的函数,全局形式的重载*/

//Fraction addFraction(const Fraction& fa,const Fraction& fb){

// /*Fraction fc;

// fc.x = fa.x * fb.y + fa.y * fb.x;

// fc.y = fa.y * fb.y;

// return fc;*/

// return Fraction(fa.x * fb.y + fa.y * fb.x,fa.y * fb.y);//简写

//}

/*设计一个函数返回double让一个分数和一个整数相加*/

double operator+(const Fraction& fa,int x){

double c;

c = x+1.0*( fa.x / fa.y);

cout << c << endl;

return c;

}

void main(){

Fraction fa(1,3);

fa.show();

Fraction fb(1,2);

fb.show();

//addFraction(fa,fb).show();

Fraction fc = fb + fa;

fc.show();

//cout << &addFraction(fa,fb);//这句话说明了什么

//Fraction fd = fa - fb;

//fd.show();

//Fraction fe = fa * fb;

//fe.show();

//fa *= fb;

//fa.show();

///////////////////

//double res = fb + 100;

//cout << res;

}

二、输入、输出流运算符的重载<<,>>

以一个整数包装类Integer类为例

class Integer{

int data;

public:

Integer(int data = 0):data(data){}



}

如果对象是一个自定义类,则cin >> fa,cout << fa同样会让编译器报错。那么则需要重载这两个运算符。在上一节中我们说到,重载运算符有两种解析规则,首先在类的成员函数中找,然后才去找全局的重载函数。比如重载输出流运算符>>,先去ostream类型中,找一个成员函数叫做:operator<<(const
Integer& i),那么我们想一想,这个能找得到吗?ostream是系统提供的类,你的到ostream类中去添加这样一个成员函数,显然是非常困难的,所以输出,输出运算符最好的形式就是重载成全局形式。


[cpp] view
plain copy

/*采用全局函数重载<<运算符*/

ostream& operator<<(ostream& os,const Integer& i){//如果以void作为输出不支持连续输出

//return os << i.data;

os << i.data;

return os;

}

/*写出自己的输入流函数>>*/

istream& operator>>(istream& is,Integer& i){//这里不能写const,输入就是的改这个对象

is >> i.data;

return is;

}

三、关于对运算符重载的几点说明

包括二元运算符的重载,一元运算符的重载,明确重载的规则,明确哪些运算符可以重载,哪些不可以,哪些只能重载成成员形式,哪些又只能重载成全局形式。

一元运算符的重载的解析规则:首先去a对象找一个成员函数operator#(),如果找不到,就去全局找一个全局函数叫做operator#(a),#代表所需要重载的一元运算符 ,在重载++,--时需要注意,前++和后++代表的意思是不同的,产生的效果也是不同的,所以是分别重载的,还有是否能连续的++,例如++(++a)这种形式。

[cpp] view
plain copy

/*几个一元运算符的重载*/

#include <iostream>

using namespace std;

class Integer{

int data;

public:

Integer(int data = 0):data(data){}

/*!运算符的重载*/

Integer operator!(){

return Integer(!data);

//return !data; 当一个类型中出现了单参构造函数时,这里有默认的类型转换

}

/*-,~,++,--运算符的重载*/

Integer operator-(){

return Integer(-data);

}

/*++运算符的重载,默认是前++*/

Integer& operator++(){ //注意,如果这里返回的不是引用,main函数中有什么效果

data++;

return *this;

}

/*设计后++的重载*/

const Integer operator++(int) //哑元

{

return Integer(data++);

}

friend Integer& operator--(Integer& i);

friend const Integer operator--(Integer& i,int);

friend ostream& operator<<(ostream& os,const Integer& i){

return os << i.data;

}

};

/*现在考虑重载全局形式的前--和后--*/

Integer& operator--(Integer& i){

i.data--;

return i;

}

const Integer operator--(Integer& i,int){

return Integer(i.data--);

}

int main(){

Integer ia(100);

cout << !ia << endl;

cout << !!ia<< endl;

cout << -ia << endl;

cout << ++(++ia)<< endl; //返回void则不支持连续++

cout << ia << endl;

//cout << ia++ << endl;在没有重载后++,这句话在Ubuntu下是通不过的,不支持后++,vs可以通过,但是结果不对

//重载了后++之后

cout << ia++<< endl;//加上const后可以防止连续的后++

cout << ia<< endl;

/*那么连续的后++有没有意义,连续的后++在c语言中是编译不过的,支持连续的前++*/

/*全局形式的前--后--的重载*/

cout << --ia<<endl;

cout << ia << endl;

cout << ia--<<endl;

cout << ia<< endl;

}

四、运算符重载的限制

不能重载的运算符包括

1、 ::(作用域)

2、.(成员运算符)

3、.*成员指针解引用

4、Sizeof(类型大小)

5、? :三元运算符

6、Typeid 获取类型的信息(返回值类型typeinfo)



只能对已有的运算符进行重载,不能发明新的运算符;


不能对基本类型进行运算符重载(运算符重载中至少有一个类型是非基本类型);

不能改变运算符的运算特性;不能把一元的改成二元的;

五、只能是重载成成员形式的运算符:=,[],()(最好是成员+=,-=,/= ,*,->等。

赋值运算符=的重载有些麻烦,因为这里必须涉及到内存的操作,以及深浅拷贝的问题。看下面的代码



[cpp] view
plain copy

#include <iostream>

using namespace std;

class Array{

int size; //用于记录最后有数据的空间的实际大小

int len; //用于标记分配的空间大小

int *datas;

public:

explicit Array(int len = 5):len(len),size(0){//explicit防止隐式转换

//分配内存

datas = new int[len];

}

~Array(){

delete[] datas;

datas = NULL;

}

//拷贝构造函数

Array(const Array& arr){

//处理内存独立性

size = arr.size;

len = arr.len;

//申请新内存

datas = new int[len];

for (int i = 0; i < size; i++){

datas[i] = arr.datas[i];

}

}

void push_data(int d){

if (size >= len){

//扩容

expend();//expend为什么没有前置声明就可调用,记住在类内进行操作的函数不需要进行前置声明

}

datas[size++] = d;//如果size大于了预分配空间,则要进行扩容操作

}

void expend(){

int *temp = datas; //保留机制

len = 2 * len + 1;

datas = new int[len]; //datas被重新分配空间

for (int i = 0; i < size; i++){

datas[i] = temp[i];

}

delete[] temp; //释放掉中间值,这个过程注意

}

void show(){

if (0 == size)

{

cout << "[]" << endl;

return;

}

for (int i = 0; i < size - 1; i++){

cout << datas[i] << ',';

}

cout << datas[size - 1] << endl;

}

//重载=运算符

Array& operator=(const Array& arr){

if (this != &arr){//防止把自己赋值给自己

size = arr.size;

len = arr.len;

int *temp = datas;

//重新申请内存

datas = new int[len];

//赋值数据

for (int i = 0; i < size; i++){

datas[i] = arr.datas[i];

}

//释放原来自己的内存

delete[] temp;

}

return *this;

}

//重载[]运算符,根据下标取数据

int operator[](int ind)const{//并不对这个对象进行修改,可以加上const

return datas[ind];

}

};

void main(){

//Array arra = 20;//防止了隐式转换,编译不过,如果不加explicit就编译过了

Array arra;

arra.push_data(9);

arra.push_data(5);

arra.push_data(2);

arra.push_data(7);

cout << arra[0] << endl;

cout << arra[10] << endl;//越界

//Array arrb = arra;这种解析方式会去找拷贝构造函数

//arrb.show();

Array arrb;

arrb = arra;//会用运算符重载的方式解析,如果没有提供=重载,会用系统提供的逐字节拷贝方式

arrb.show();//程序崩了,被重载了之后不会报错,也不会崩溃

}

在上面的代码中,如果遇到arrb= arra且我们没有提供赋值运算符重载的时候,那么编译器会采用默认的处理方式,就是逐字节拷贝,那么这种拷贝方式在处理datas堆内存时会遇到什么问题呢?len,size,datas等成员变量依次被覆盖掉,那么此时arrb的成员datas所指向的堆内存就和arra的datas指向的堆内存一样了,那么原来arrb的datas所指向的堆内存就泄露了。这就是这里比较严重的问题。



六、圆括号运算符()的重载

我们在写程序的时候经常喜欢这样用int x = (int)y,这里的y不是int类型,这是强制的类型转换,但是如果我们定义了一个产品类Product,它拥有成员int count代表产品的数量,double price代表产品的价格,如果在开发中我们想把这个Product转换成int类型,那么显而易见,对于开发者来说我们想要取得的是产品的数量。如果我们想把Product转成double型,同样我们肯定是希望获得其价格。



所以圆括号的重载在开发中用途广泛,它的作用就是把一个单参类型转换成当前对象类型。



语法格式:圆括号既然只能是成员形式,那么我们肯定这样想:int operator()(){},但是这是不行的,编译器不认可,圆括号的重载有固定的格式:operator 类型(){

return 对象类型

}

如果要重载圆括号返回int的话,就得这样写:

operator int(){

return count;

}



[cpp] view
plain copy

/*圆括号运算符的重载,把一个单参类型转换成当前对象类型*/

/*圆括号运算符的重载*/

/*operator 类型(){

return 类型的对象

}*/

#include <iostream>

using namespace std;

class Product{

int count;

double price;

public:

Product(int count = 0, double price = 0.0) :count(count), price(price){}

/*重载()运算符*/

//我们一般会这样写,这种写法编译器不会认可的

//int operator()(){

//return count;

//}

//但是得这样写,固定形式,没有为什么

operator int(){

return count;

}

/*转换成double的圆括号重载*/

operator double(){

return price;

}

};

int main(){

Product product(100, 1.15);

/*现在如果我们想把这个product变成整数,我们想要的肯定是数量*/

int count = (int)product;

cout << count << endl;

double price = (double)product;

cout << price << endl;

}

七、new delete运算符的重载

首先的明白一个问题就是new和delete比malloc和free多做了那些事

如果类的成员变量是类类型,则自动创建这个成员,自动调用构造函数。


delete 会去调用析构函数,free不会.

[cpp] view
plain copy

#include <iostream>

using namespace std;

/*这种情况下如果c的编译器求sizeof(A)其大小是0

而用c++的编译器求siziof(A)则是1.这里表现出了编译器

对待内存分配的差异,我觉得这些细小的知识点展现出了

一名优秀c++程序设计的人员的素质*/

class A{

public:

A(){ cout << "A()" << endl; }

~A(){ cout << "~A()" << endl; }

};

class B{

A a;

public:

B(){ cout << "B()" << endl; }

~B(){ cout << "~B()" << endl; }

};

int main(){

B* pb = static_cast<B*>(malloc(sizeof(B)));

B * pb2 = new B();

free(pb);

delete pb2;

}



重载new delete:


固定写法:void * operator new(size_t size)

void operator delete(void * ptr)

[cpp] view
plain copy

#include <iostream>

#include <cstdlib>

using namespace std;

class Date{

int year;

int month;

int day;

public:

Date(int year = 0, int month = 0, int day = 0) :year(year), month(month), day(day){

cout << "Date()" << endl;

}

~Date(){ cout << "~Date()" << endl; }

};

/*放到成员和全局是一样的*/

void* operator new(size_t size){//具有自动识别大小的功能

cout << "my operator new" << size << endl;

return malloc(size);

}

void operator delete(void* ptr){

cout << "operator delete" << endl;

free(ptr);

}

int main(){

Date * date = new Date();

delete date;

}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: