目录
什么是运算符重载
运算符重载(Operator overload)是对已有的运算符赋予多重含义,使同一个运算符作 用于不同类型的数据时做出不同的行为。
例如:学生类包括姓名,年龄,班级,成绩等,定义了两个学生变量,想比较它们的大 小,如何比较大小呢?
为“学生”类重载“>”符号,然后在这个方法里实现比较的意义:按年龄、按成绩或 按姓名首字母比较等。
运算符重载的意义
运算符重载的本质是函数重载,是实现多态的重要手段,为用户提供了一个直观的接口。 调用运算符操作自定义数据类型其实就是调用运算符函数。运算符重载使用同样的运算 符,可以操作不同的数据,增强了C++的可扩充性,使代码更加直观、易读,便于对对 象进行各种运算操作。
运算符重载的语法格式
重载的运算符是具有特殊名字的函数:它们的名字由关键字operator,后跟要重载的运 算符。
返回类型 operator 运算符(参数列表)
{
函数体;
}
说明:运算符是要重载的运算符名(如+、-、*、/等),但必须是C++允许重载的运算 符
简单例子
“+”运算符的重载
#include <iostream>
using namespace std;
class A
{
private:
int x, y;
public:
A(int x1 = 0, int y1 = 0)
{
x = x1;
y = y1;
}
A operator+(const A& a)const//重载-运算符的实现
{
return A(x + a.x, y + a.y);//调用A类构造函数创建一个临时匿名对象作为函数返 回
}
void show()
{
cout << "x=" << x << "," << "y=" << y << endl;
}
};
int main()
{
A a1(1, 2);
A a2(3, 4);
A a;
a = a1 + a2;
a.show();
}
输出结果
x=4,y=6
const一般用在代码前面表示常量,只可读不可改,这里用在函数中表示常成员函数: 只读函数,只可读不可改数据成员的值。也就是时候const定义的函数里面的数据成员 的值不可改变
代码分析
在A类中重载了运算符+,该重载只对A类对象有效。
执行a=a1+a2;语句时,编译器检测到+号左边是一个A类对象(+号具有左结合性,所以 先检测左边),就会调用成员函数operator+(),也就是转换为下面的形式:
a=a1.operator+(a2); //a1是要调用函数的对象,a2是函数的实参
可见:
重载运算符并没有改变其原来的功能,只是增加了对自定义数据类型的运算功能。
运算符重载的两种方式
1,重载为类的成员函数
(1),双目运算符
如果是双目运算符重载为类的成员函数,则它有两个操作数:左操作数是对象本身 的数据,由this指针指出,右操作数则通过运算符重载函数的参数表来传递。
调用格式为:
左操作数.运算符重载函数(右操作数)
#include <iostream>
using namespace std;
class A
{
private:
int x, y;
public:
A(int x1 = 0, int y1 = 0)
{
x = x1;
y = y1;
}
A operator+(const A& a)const
{
A t;
t.x = this->x + a.x;
t.y = this->y + a.y;
return t;
}
void show()
{
cout << "x=" << x << "," << "y=" << y << endl;
}
};
int main()
{
A a1(1, 2);
A a2(3, 4);
A a;
a = a1 + a2;
a.show();
}
输出结果
x=4,y=6
(2),单目运算符
如果是单目运算符重载为类的成员函数,则要分为前置(++i)和后置(i++)运算符。
如果是前置运算符,则它的操作数是函数调用者,函数没有参数。
调用格式为:
操作数.运算符重载函数()
#include <iostream>
using namespace std;
class A
{
private:
int x, y;
public:
A(int x1 = 0, int y1 = 0)
{
x = x1;
y = y1;
}
A &operator++()//++i 前置++实现
{
++x; //先自增
++y;
return *this; //后引用
}
A operator++(int) //i++ 后置++实现
{
//int参数没有任何意义,只是为了区分是前置还是后置形式
A a = *this; //保存对象引用
++(*this); //自增,调用前面实现的前置++
return a; //返回先前保存的对象
}
void show()
{
cout << "x=" << x << "," << "y=" << y << endl;
}
};
int main()
{
A a1(1, 2), a2(3, 4);
(a1++).show();
(++a2).show();
return 0;
}
输出结果
x=1,y=2
x=4,y=5
实现前置“++”时,数据成员进行自增运算,然后返回当前对象(即this指针所指 向的对象)。实现后置“++”时,创建了一个临时对象来保存当前对象的值,然后再将当前对象自增,最后返回的是保存了初始值的临时对象。
注意:前置单目运算符和后置单目运算符的最主要区别是函数的形参,后置单目运 算符带一个int型形参,但它只起区分作用。
2,重载为类的友元函数
运算符重载为类的友元函数,只是在函数前加一个friend关键字。
(1)重载格式
friend 返回类型 operator 运算符(参数列表)
{
函数体;
}
说明:运算符重载为类的友元函数时,由于没有隐含的this指针,因此,操作数的个数没有变化,所有的操作数都必须通过函数形参进行传递,函数的参数与操作数 自左自右保持一一对应。
(2)调用格式
operator 运算符(参数1,参数2);
例如:调用a1+a2相当于函数调用operator+(a1, a2)
下面看一个例子:
将”+”运算符重载为类的友元函数
#include <iostream>
using namespace std;
class A
{
private:
int x, y;
public:
A(int x1 = 0, int y1 = 0)
{
x = x1;
y = y1;
}
friend A operator+(const A& a, const A& b);
void show()
{
cout << "x=" << x << "," << "y=" << y << endl;
}
};
A operator+(const A& a, const A& b)
{
return A(a.x + b.x, a.y + b.y);
}
int main()
{
A a1(1, 2), a2(3, 4);
A c;
c = a1 + a2;
c.show();
return 0;
}
输出结果
x=4,y=6
两种重载方式的选择
多数情况下,将运算符重载为类的成员函数和类的友元函数都是可以的,但两者各具特点:
1,一般,单目运算符最好重载为类的成员函数,双目运算符最好重载为类的友元函数。
2,若一个运算符的操作需要修改对象的状态,选择重载为成员函数较好。
3,若运算符的操作数(尤其是第一个操作数)可能有隐式类型转换,则只能选用友元函 数。
4,具有对称性的运算符可能转换任意一端的运算对象,如:算术(a+b和b+a)、关系运算 符(a>b和b<a)等,通常重载为友元函数。
5,有4个运算符必须重载为类的成员函数:赋值=、下标[ ]、调用( )、成员访问->。
运算符重载的规则
C++中不是任意运算符都可以重载,运算符重载规则:
1,C++中不允许用户定义新的运算符,只能对已有的运算符进行重载,
2,重载后的运算符的优先级、结合性也应该保持不变,也不能改变其操作个数和语 法结构。
3,重载后的含义,与操作基本数据类型的运算含义应类似,如加法运算符“+”,重 载后也应完成数据的相加操作。
4,有5个运算符不可重载:类关系运算符“:”、成员指针运算符“*”、作用域运算 符“::”、sizeof运算符、三目运算符“?:”
5,运算符重载函数不能有默认参数,否则就改变了运算符操作数的个数,是错误的。
6,运算符重载函数既可以作为类的成员函数,也可以作为类的友元函数(全局函数)。
作为全局函数时,一般都需要在类中将该函数声明为友元函数。因为该函数大部分情况下都需要使用类的private成员。作为类的成员函数时,二元运算符的参数只有一个, 一元运算符不需要参数(参数没有任何意义,只是为了区分是前置还是后置形式:带 一个整型参数为后置形式)。因为有个参数(左操作数)是隐含的(隐式访问this指针 所指向的当前对象)。作为全局函数时,二元操作符需要两个参数,一元操作符需要一 个参数,而且其中必须有一个参数是对象,好让编译器区分这是程序员自定义的运算符, 防止程序员修改用于内置类型的运算符的性质。
常用运算符的重载
1,输入输出运算符的重载
C++标准库对左移运算符<<和右移运算符>>分别进行了重载,使其能够用于不同数 据的输入输出。对于基本类型数据(如bool、int、double等)和标准库所包含的 类(如string、complex、ofstream、ifstream等)可以直接使用输入运算符“>>”、 输出运算符“<<”进行读写操作。对于自己定义的新数据类型(如:类对象)则需要重载这两个运算符。
cout<<obj1 <<endl; cin>>obj1; //cout是ostream类的对象,cin是istream类的 对象
由于输入输出操作的第一个操作数为ostream/istream对象,也就是说左侧的运算 对象必须是ostream/istream对象,如果重载成类的成员函数,则左侧的操作对 象将是我们定义的一个类对象(需要修改标准库中的类,显然不是我们所期望 的)。因此输入输出运算符不可重载为类的成员函数,只能重载为非成员函数 (全局函数,友元函数)。
重载“<<”和“>>”运算符的一般格式为:
istream& operator>>(istream&, 类对象引用); //输入运算符>>重载
ostream& operator<<(ostream&, const 类对象引用); //输出运算符<<重载
说明
(1)对于输入运算符来说,第一个参数是istream对象的引用,第二个参数是要 向其中存入数据的对象,不能为常量。返回istream类对象的引用(可作为下次调 用时的第一个参数),是为了能够连续读取:cin>>c1>>c2;,让代码书写更加漂亮。 如果不返回引用,就只能一个一个地读取:cin>>c1; cin>>c2;
cin>> c; 可以理解为:operator >> (cin , c);
(2)对于输出运算符“<<”来说,第一个参数是ostream对象引用,因为向流中 写入数据会改变流的状态 ,所以不能用const修饰ostream对象。由于采用了引用 的方式进行参数传递,并且也返回了对象的引用,所以重载后的运算符可以实现连 续输出。
cout<< c; 可以理解为:operator<< (cout , c);
案例
#include <iostream>
using namespace std;
class A
{
private:
int x, y;
public:
A(int x1 = 0, int y1 = 0)
{
x = x1;
y = y1;
}
friend istream& operator>>(istream& is, A& a);
friend ostream& operator<<(ostream& os, const A& a);
};
istream& operator>>(istream& is, A& a)
{
is >> a.x >> a.y;
return is;
}
ostream& operator<<(ostream& os, const A& a)
{
os << "x=" << a.x << "," << "y="<<a.y ;
return os;
}
int main()
{
A a1(1, 2);
cin >> a1;
cout << a1;
return 0;
}
输出结果
21 88
x=21,y=88
重载输入输出运算符后,便可像基本数据类型一样直接对类对象进行输入输出,不 用再编写用于输入输出对象数据成员的show()等成员函数,而且使用起来更为简洁。
注意:
通常情况下,输出运算符主要负责输出对象的内容,而非控制格式,此时不应该把 换行符也重载进去。如果把换行符也重载进去,用户有时想在同一行输出一些描述 性的文本,就无法完成了。因此,在重载输出运算符时,应尽量少一些格式化操作, 可以使用户控制更多的输出细节。
2,关系运算符的重载
关系运算符(如“==”或“<”)也是较常用的一类运算符,同样,若类对象间需要 完成比较操作,也应完成对关系运算符的重载。关系运算符重载的过程非常直观。 重载关系运算符一般都返回true或false值。
关系运算符共有六个,并且相互之间有对应关系,如小于运算符“<”与大于运算 符“>”对应,因此对于关系运算符的重载应成对完成。
可重载为类的成员函数也可重载为类的友元函数(具有对称性的运算符通常重载为 类的友元函数)
案例
#include <iostream>
using namespace std;
class A
{
private:
int x, y;
public:
A(int x1 = 0, int y1 = 0)
{
x = x1;
y = y1;
}
friend bool operator>(const A& a1,const A& a2);
friend bool operator<(const A& a1,const A& a2);
friend bool operator==(const A& a1, const A& a2);
};
bool operator>(const A& a1, const A& a2)
{
return a1.x > a2.x;
}
bool operator<(const A& a1, const A& a2)
{
return a1.x < a2.x;
}
bool operator==(const A& a1, const A& a2)
{
return a1.y == a2.y;
}
int main()
{
A a1(1, 2);
A a2(3, 2);
if (a1 > a2)
{
cout << "a1的x大于a2的x"<<endl;
}
else if (a1 < a2)
{
cout << "a1的x小于a2的x" << endl;
}
if (a1 == a2)
{
cout << "a1的y都等于a2的y" << endl;
}
return 0;
}
输出结果
a1的x小于a2的x
a1的y都等于a2的y
重载了”>”,”<”,”==”的关系运算符,其他的也是一样,重载关系运算符之后可以直 接比较对象的大小,实际上”>”,”<”只是比较了对象中的x值,”==”只是比较了对象 中的y值,
重载关系运算符的准则
(1)关系运算符都要成对的重载。如:重载了“<”运算符,就要重载“>” 运算符,反之亦然。
(2)“==”运算符应该具有传递性。如:a==b,b==c,则a==c成立。
(3)当成对出现运算符重载时,可以把一个运算符的工作委托给另一个运算 符。如:重载“!=”运算符是在“==”运算符的基础上,重载“<”运算 符 由重载过的“>”运算符来实现。
3,赋值运算符的重载
若类中没有显式定义赋值运算符的重载函数,编译器将自动提供一个默认的重载函 数,完成数据成员的“浅拷贝”(跟默认拷贝构造函数一样,就是把一个对象的数 据成员的值复制给另一个对象对应的数据成员)。若类中的数据成员上需要附加额 外的信息,比如类中存在指针成员(编译器默认的赋值运算符不能满足要求,会出 现内存泄露),指针成员指向新开辟的空间,此时需要重载赋值运算符,以实现“深 拷贝”。
注意:赋值运算符只能被重载为类的成员函数。
赋值运算符的重载案例这里就不演示了
此外还有下标运算符的重载,new和delete运算符的重载等,这里就不赘述了
类型转换函数
重载类型转换运算符的原因
对于基本数据类型的数据可以通过强制类型转换操作符来将数据转换成需要的类 型。
static_cast <new_type> (expression)
C++中的static_cast执行非多态的转换,用于代替C中通常的转换操作。因此,被 做为显式类型转换使用。
例如:static_cast <int> (2.1),这个表达式将实型数据2.1转换成了int类型数据。
对于自定义的类来说,在很多情况下也需要支持此操作来实现自定义类与基本数据 类型之间的转换,对此C++提供了类型转换函数。类型转换函数也称为类型转换运 算符重载函数。
重载类型转换运算符的格式
operator 类型名(); //类对象->基本类型
说明:
(1)通过类型转换函数可以将自定义类型对象转换为基本类型数据。
(2)类型转换函数被重载的是类型名。在函数名前不能指定函数类型,函数也没 有参数。返回值类型是由函数名中指定的类型名来确定的(如果类型名为double, 则将类型数据转换为double类型返回)。
(3)因为函数转换的主体是本类的对象,类型转换运算符重载函数只能作为类的 成员函数。
案例:定义一个A类,将A类型的对象转换为char类型
#include <iostream>
using namespace std;
class A
{
private:
int x, y;
public:
A(int x1 = 0, int y1 = 0)
{
x = x1;
y = y1;
}
operator char()
{
return static_cast<char>(x);
}
void show()
{
cout << "x=" << x << "," << "y=" << y << endl;
}
};
int main()
{
A a1(65, 2);
a1.show();
char ch = a1;
cout << ch << endl;
return 0;
}
输出结果
x=65,y=2
A
从运行结果可见,A类型对象a1成功转换为char类型的字符’A’
说明:
类型转换函数有一个功能:当需要的时候,编译系统会自动调用重载函数,建立一 个无名的临时对象(或临时变量)。
例如:在上面例子中,将a1对象赋值给ch时,如果没有重载“=”运算符,编译 器首先会检查是否有类型转换函数,如果有,则调用operator char()函数,将a1 对象转换为一个临时的char类型变量,再将这个临时变量赋值给ch。
转换构造函数
什么是转换构造函数
自定义数据类型与基本数据类型之间的转换,除了类型转换运算符重载,还可以利 用转换构造函数。所谓转换构造函数就是当一个构造函数只有一个参数,而且该参 数又不是本类的const引用时,这种构造函数称为转换构造函数。
转换构造函数的作用
用转换构造函数不仅可以将一个标准类型数据转换为类对象,也可以将另一个类的 对象转换为转换构造函数所在的类对象。
//基本类型或另一个类的对象->本类对象
转换构造函数的用法
使用转换构造函数将一个指定数据转换为类对象的方法如下 :
先声明一个类
在这个类中定义一个转换构造函数,在函数体中指定转换方法。
在该类的作用域内可以用以下形式进行类型转换:
类名(指定类型的数据);
案例
#include <iostream>
using namespace std;
class A
{
private:
int x, y;
public:
A(int x1 = 0, int y1 = 0)
{
x = x1;
y = y1;
}
A(double z)//转换构造函数:一个参数(非const引用),函数体中指定转换方 法
{
cout << "A类构造函数调用!" << endl;
x = static_cast<int>(z);
y = 0;
}
void show()
{
cout << "x=" << x << "," << "y=" << y << endl;
}
};
int main()
{
A a1(65, 2);//调用普通构造函数创建对象
A a2(32.4);//调用转换构造函数
cout << "a1类" << endl;
a1.show();
cout << "a2类" << endl;
a2.show();
return 0;
}
输出结果
A类构造函数调用!
a1类
x=65,y=2
a2类
x=32,y=0
定义了一个转换构造函数,将double类型的数据转换为A类型,在创建对象a2 时,将32.4转换成了A类型,与类型转换函数一样,在将double类型数据32.4 转换成A类型时,先调用转换构造函数将32.4转换成一个临时的A类对象,然 后将这个临时对象赋值给对象a2
用转换构造函数可以将一个指定类型的数据转换为类的对象,但不能反过来将一个 类的对象转换为一个其他类型的数据(例如:将一个A类对象转换成double 类 型数据),如果要这么做,则需要类型转换函数。