前言
在C++中,运算符重载是指为自定义类型(类或结构体)定义或修改运算符的行为,使得这些运算符可以用于自定义类型的对象。运算符重载使得用户定义的类型可以像内置类型一样使用运算符,从而提高了代码的可读性和可维护性。本文将会详细介绍各类运算符重载的相关语法及常见错误。
一、什么是运算符重载?
“什么是运算符重载?”比如,我们可以在C++中实现两个整数的相加,如:
int a=10;
int b=20;
int c=a+b;
但是,我们知道与C面向过程编程不同,类和对象是C++的核心特性之一。但是如何实现两个对象(比如A对象与B对象)之间的相加呢?在默认情况下,C++编译器是识别不到两个对象之间的相加的。系统提示:没有与这些操作数匹配的“+”运算符。如:
这样,在C++面向对象编程的过程中,造成了很大的不利。然而C++允许你对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。这就是运算符重载。但需要注意的是,对于内置数据类型表达式的运算符是不可更改的。
下面,我将带大家详细介绍下,有关运算符重载的常见类型及注意事项。
二、运算符重载的相关语法及注意事项
1.加号运算符重载
如下,我定义了一个Person类,我想要在test01函数中实现两个对象p1与p2的相加,默认情况下,系统会提示我们“没有与这些操作数匹配的“+”运算符”。这是因为C++语法并没有定义两个对象的相加。
class Person
{
public:
Person(int a)
{
m_a = a;
}
int m_a;
};
void test01()
{
Person p1(10);
Person p2(10);
Person p3(0);
p3 = p1 + p2;
cout << p3.m_a << endl;
}
int main()
{
test01();
return 0;
}
为了解决这个问题,我们可以自己定义一个“+”运算符,来实现两个对象的相加,那么我们应该怎么实现加法运算符重载呢?C++给了我们两种方式实现运算符重载(全局函数实现重载与成员函数实现运算符重载),但两种方式不能同时出现。
(Ⅰ)、成员函数实现加号运算符重载
我们在成员函数中加入Person operator+(Person& p)成员函数,如下。operator+是固定写法,使用“operator运算符”,我们就可以实现对该运算符的重载。
class Person
{
public:
Person(int a)
{
m_a = a;
}
Person operator+(Person& p)
{
Person ptemp(0);
ptemp.m_a = this->m_a + p.m_a;
return ptemp;
}
int m_a;
};
需要注意的是函数运算符重载中,函数的返回类型为Person,这里是不能写成Person&引用类型的。因为我们这里是创建了一个临时对象ptemp,一旦该函数调用完,对象ptemp就被释放了。否则就会造成内存的非法访问。
(Ⅱ)、全局函数实现加号运算符重载
我们也可以使用全局函数实现加号运算符重载。如下:
Person operator+(Person& p_1, Person& p_2)
{
Person ptemp(0);
ptemp.m_a = p_1.m_a + p_2.m_a;
return ptemp;
}
与成员函数实现加号运算符重载一样,这里依然返回值为Person类型,而不是Person&。
(Ⅲ)、对加号运算符重载的总结
其实,在调用p3 = p1 + p2时,C++会自动调用相应的全局函数或成员函数。对于成员函数,C++会翻译为p3=p1.operator+(p2),所以我们可以发现,对于成员函数实现运算符重载时,第一个操作数p1是调用成员函数所属的对象,而p2才是被传入的参数。这点的理解尤为重要。
对于全局函数实现加号运算符重载,系统会理解为p3=operator+(p1,p2)。有人可能会有疑问,为什么系统会理解为p3=operator+(p1,p2),而不是p3=operator+(p2,p1)。很好,这里看似是一个小问题,“不都是两个数相加吗?谁前谁后不都一样吗。”但是,我们想个问题,这里实现的是两个数相加,但是如果是两个数相减呢?如果传入参数的顺序不一样,那么结果就会大不一样。我们这里不妨用代码试验一下。
这里还是对于上面同样一个类,我们实现减法运算符的重载。这里,p1传参为20,p2传参为10。我们计算p3=p1-p2的结果。如果系统传参顺序为p3=operator-(p1,p2),那么计算结果就为10;如果系统传参顺序为p3=operator-(p2,p1),那么计算结果就为-10;
Person operator-(Person& p_1, Person& p_2)
{
Person ptemp(0);
ptemp.m_a = p_1.m_a - p_2.m_a;
return ptemp;
}
void test01()
{
Person p1(20);
Person p2(10);
Person p3(0);
p3 = p1 - p2;
cout << p3.m_a << endl;
}
int main()
{
test01();
return 0;
}
可以得到,系统计算结果为10,而不是-10。说明系统会理解为p3=operator-(p1,p2)。即对于全局函数实现操作符重载,左操作数为函数的第一个参数,右操作数为第二个参数。
另外还需注意的是:对于同一个运算符同一种类型的重载,不能全局函数与成员函数实现都存在,否则系统就会报错。
除了用运算符重载实现同一种类两个对象的相加,我们也可以实现,不同类型之间的相加,如:Person类与整数的相加。
Person operator+(Person& p1, int a)//可以实现类与整型的相加
{
Person temp;
temp.m_a = p1.m_a + a;
return temp;
}
或牛类与马类的相加,返回值为人类。
class Person//定义人类
{
public:
Person(int a)
{
m_a = a;
}
int m_a;
};
class Horse//定义马类
{
public:
Horse(int a)
{
m_a = a;
}
int m_a;
};
class Oxen//定义牛类
{
public:
Oxen(int a)
{
m_a = a;
}
Person operator+(Horse p)//运算符重载,在Oxen类中实现
{
Person ptemp(0);
ptemp.m_a = this->m_a + p.m_a;
return ptemp;
}
int m_a;
};
void test01()
{
Oxen p1(20);
Horse p2(10);
Person p3(0);
p3 = p1 + p2;
cout << p3.m_a << endl;
}
int main()
{
test01();
return 0;
}
2.左移运算符重载
我们可以发现,在实现加号运算符重载的过程中,我们要打印输出对象中属性的值通常需要用到下面这个代码:
cout << p3.m_a << endl;
但是我们有没有更好的方法去实现对对象中属性的打印输出呢? 答案是有的。比如我们可以通过对左移运算符<<重载来实现对对象属性的打印输出,代码就可以简化为:
cout<<p3<<endl;
那么现在的问题就是该如何来实现对左移运算符的重载。我们知道实现运算符重载有两种方式,全局函数或成员函数。如果用成员函数来实现运算符重载,C++会隐式地将左侧的对象作为调用者,因此在成员函数中可以直接访问该对象的成员变量和其他成员函数。例如,对于 a + b
,如果 a
是一个类的对象,C++会自动调用 a.operator+(b)
。那我们在思考下,对于左移运算符,cout是它的左操作数,那我们如果要实现对左移运算符的重载的话,我们是不是就要在cout这个类中定义运算符重载的成员函数。显然这是不可能的。所以对于左移运算符的重载,我们是不能通过成员函数来实现的。
既然不能通过成员函数来实现,那我们就只能通过全局函数来实现左移运算符重载了。对于一个全局函数除了函数体外,我们还需要知道什么?
①首先是函数名,这个我们十分清楚,对于运算符重载,函数名不久是operator加上相应的操作数吗?那么对于左移运算符,函数名就是operator<<。②其次,我们还需要知道函数的传参类型。我们知道,对于全局函数实现运算符重载,运算符的左操作数就是全局函数要传入的第一个参数,右操作数就是要传入的第二个参数。那么我们就知道了,对于左移运算符的重载,第一个参数一定是cout,第二个参数是类对象。对于cout类型为ostream,对于对象p1,类型为Person。那是否可以写成(ostream cout,Person p1)呢?不能,因为当我们写成ostream cout时,本质上是通过拷贝构造函数生成了一个对cout对象的临时拷贝。而ostream类中的拷贝构造函数是Protected权限类型的,系统无法拷贝。所以编译器就会提示错误。这里可以采取引用即ostream&。③最后是返回类型,对于左移运算符我们希望在cout<<p3后还可以打印一个换行endl,即链式编程。所以我们就需要,打印完p1的内容后,能够返回一个cout。这样才能实现在p1打印输出后,仍然可以打印其他内容。如下图。仍然要注意的是,返回类型依然为引用,原因与上面相同。当函数返回类型为ostream时,本质还是进行了一次对cout对象的拷贝。
所以现在我们知道了函数的返回类型,函数名以及传参类型,剩下的就是函数体。我们要实现对一个对象属性的打印, 只需在函数体内将传参对象的属性进行打印就好。完整版代码如下:
class Person
{
public:
Person(int a, int b)
{
m_a = a;
m_b = b;
}
//不能在类内创建成员函数来实现操作符重载,因为无论怎么创建,在调用时一定是p1<<
//void operator<<()
//{
//}
//private:
int m_a;
int m_b;
};
//全局函数实现操作符重载
ostream& operator<<(ostream& cout, Person& p)//ostream里面的拷贝构造函数是protected的,无法拷贝。所以要采取引用,这样在调用的时候并不需要创建一个临时变量
{
cout << p.m_a<<" "<<p.m_b;
return cout;
}
void test()
{
Person p1(10,20);
cout << p1 << endl;
}
int main()
{
test();
return 0;
}
3.赋值运算符重载
下面我们要讲的是赋值运算符重载。在默认情况下,C++编译器会给一个类添加4个函数。分别为:
(1)、无参构造函数
(2)、析构函数
(3)、拷贝构造函数(对属相进行值拷贝)
(4)、赋值运算符operator=,对属性进项值拷贝
首先,我创建了一个Person类 。同时定义了一个函数test01(),在test01()函数中创建三个对象p1、p2、p3,想要把p1的值赋给p2和p3。在默认情况下,即使你不定义赋值运算符,C++编译器也会自己生成一个赋值运算符重载函数,对属性进行至拷贝。所以下面的代码并不会报错。
class Person
{
public:
Person(int age)
{
m_age=age;
}
int m_age;
};
void test01()
{
Person p1(10);
Person p2(20);
Person p3(30);
p3 = p2 = p1;
cout << p1.m_age << endl;
cout << p2.m_age << endl;
cout << p3.m_age << endl;
}
int main()
{
test01();
return 0;
}
下面我将介绍三点关于赋值运算符重载的注意事项。
①、首先是我们要学会区分赋值运算符重载与构造函数的区别。如下代码的输出结果会是什么?Person p2=10、Person p3=p1、p4=p1这三个语句都有赋值号“=”,那么他们都分别会调用什么函数?
class Person
{
public:
Person(int a)
{
cout << "有参构造函数的调用" << endl;
m_a = a;
}
Person(Person& p)
{
cout << "拷贝构造函数的调用" << endl;
m_a = p.m_a;
}
void operator=(Person& p)
{
cout << "赋值运算符重载函数的调用" << endl;
m_a = p.m_a;
}
int m_a;
};
int main()
{
Person p1(10);
Person p2 = 10;
Person p3 = p1;
Person p4(20);
p4 = p1;
return 0;
}
我们看下打印结果,可以看到Person p2=10与Person p3=p1都调用的是构造函数,而不是赋值运算符重载函数的调用。而p4=p1调用的是赋值运算符重载函数。这里要求我们区分初始化与赋值之间的关系。Person p2=10与Person p3=p1系统会理解为Person p2=Person(10)与Person p3=Person(p1)。这里本质上是调用相应的构造函数对对象进行初始化。
②、其次是赋值运算符重载必须是成员函数,这是因为即使我们自己不在类内定义赋值运算符重载的成员函数,系统也会自己给我们生成一个相应的重载成员函数。如果我们再定义全局函数,那么在使用p1=p2时,就会产生二异性。 ③、要注意系统默认的赋值运算符重载时进行的是值拷贝。当我们在堆区开辟内存时,非常有可能出现错误。比如下面这个代码在运行过程中就会出错。
class Person
{
public:
Person(int age)
{
m_age=new int(age);
}
~Person()
{
if (m_age != NULL)
{
delete m_age;
m_age = NULL;
}
}
int* m_age;
};
void test01()
{
Person p1(10);
Person p2(20);
Person p3(30);
p3 = p2 = p1;
cout << *p1.m_age << endl;
cout << *p2.m_age << endl;
}
int main()
{
test01();
return 0;
}
报错的原因是因为系统默认的赋值构造函数是进行的值拷贝,即p2拷贝的是p1指向堆区内存空间。当函数test01调用完后,p2中m_age指向的堆区内存就被释放了,而p1中的m_age仍指向堆区那片区域,但是这片区域已经被销毁了,于是就会出现内存的非法访问。
所以,我们类中,如果有在堆区开辟的内存空间,我们一定要自己构建拷贝构造函数与赋值运算符重载函数,同时,在进行赋值时,要先释放之前堆区的空间,再赋值,以免产生内存碎片。正确的代码如下:
class Person
{
public:
Person(int age)
{
m_age=new int(age);
}
Person(Person& p1)
{
this->m_age = new int(*p1.m_age);
}
~Person()
{
if (m_age != NULL)
{
delete m_age;
m_age = NULL;
}
}
Person& operator=(Person& p)//返回值为引用是为了可以进行链式赋值操作
{
if (m_age != NULL)//这里是为了,防止内存碎片的产生
{
delete m_age;
m_age = NULL;
}
m_age = new int(*p.m_age);
return *this;
}
int* m_age;
};
void test01()
{
Person p1(10);
Person p2(20);
Person p3(30);
p3 = p2 = p1;
cout << *p1.m_age << endl;
cout << *p2.m_age << endl;
}
int main()
{
test01();
return 0;
}
4.递减/递增运算符的重载
这里以递增运算符为例,递增运算符又可分为前置递增与后置递增。我们分别来介绍这两种递增运算符的重载。
(Ⅰ)前置递增运算符的重载
代码如下,为了实现链式编程,我们需要返回值为引用类型。
class Person
{
public:
Person(int a)
{
m_a = a;
}
Person& operator++()
{
m_a++;
return *this;
}
int m_a;
};
int main()
{
Person p1(10);
Person p2(p1);
++(++p1);
cout << p1.m_a << endl;
cout << p2.m_a << endl;
return 0;
}
(Ⅱ)后置递增运算符的重载
代码如下:
class Person
{
public:
Person(int a)
{
m_a = a;
}
Person& operator++()//前置加加
{
m_a++;
return *this;
}
Person operator++(int)//后置加加
{
Person temp(this->m_a);
m_a++;
return temp;
}
int m_a;
};
int main()
{
Person p1(10);
Person p2(20);
++(++p1);
cout << p1.m_a << endl;
cout<<(p2++).m_a<<endl;
cout << p2.m_a << endl;
return 0;
}
(Ⅲ)区别及注意事项
可以看出,前置加加与后置加加有很大的区别。主要表现在以下几点:
①、前置加加返回值为Person&引用类型,而后置加加返回值为Person。为什么会有这个区别呢?因为前置加加有个特性是先加加后使用,而后置加加是先使用,后加加。对于前置加加,我们可以加加后返回原对象(*this),但是对于后置加加,是先返回当前对象的值,然后再将对象的值增加1。为了实现这一点,必须在返回之前保存当前对象的状态。所以我们创建了一个临时对象temp,但是对于temp,我们返回时不能返回引用,因为函数调用完这部分内存就被释放了。返回引用或指针都可能造成内存的非法访问。
②、前置加加没有占位参数,而后置加加有占位参数。这是不是意味着我们在要用后置加加时要传参呢?答案是否。后置加加必须有参数,C++编译器要求,需要在后置运算符重载函数中加参数“int”(这里只能用int类型来做占位参数),这个类型在此除了以示区别之外并不代表任何实际含义;如果不加,编译器无法区分是前置++,还是后置++,导致报错。
③、如果使用后置递增,因为返回值不是引用类型,所以就不能进行链式编程了或链式编程会出现错误。按理来说,对p2进行了两次后置加加操作,应该输出也为12,但是因为返回值不是引用类型。所以产生了错误。
其实,对于C++来说,无论任何变量类型的后置加加操作,系统都不允许进行链式编程(具体为什么我也没搞懂)。比如下面这个代码,有知道的小伙伴可以评论区留言。
④、前置加加与后置加加都可以用全局函数来实现。如下:
Person& operator++(Person& p)//全局函数实现加加也是可以的
{
p.m_a++;
return p;
}
Person operator++(Person& p,int)//全局函数实现后置加加也是可以的
{
Person temp(p.m_a);
p.m_a++;
return temp;
}
5.关系运算符重载
这个相对简单,只不过注意返回类型为bool类型就可以。
class Person
{
public:
Person(int age)
{
m_age = age;
}
bool operator==(Person p)
{
if (this->m_age == p.m_age)
{
return true;
}
return false;
}
int m_age;
};
//bool operator==(Person p1, Person p2)//也可通过全局函数实现重载
//{
// if (p1.m_age == p2.m_age)
// {
// return true;
// }
// return false;
//}
void test()
{
Person p1(10);
Person p2(10);
if (p1 == p2)
{
cout << "p1==p2" << endl;
}
else
{
cout << "p1!=p2" << endl;
}
}
int main()
{
test();
return 0;
}
6.函数调用运算符的重载
函数调用运算符“()”,的使用与函数的调用相似,所以又称为仿函数。这里要注意的是,我们必须通过创建一个对象(匿名对象也可)来调用函数调用运算符。
class m_Add
{
public:
void operator()(int b, int c)
{
cout << b + c << endl;
}
};
void test01()
{
m_Add Add;
Add(10, 20);。
m_Add()(10, 20);//匿名对象也可以,只不过匿名对象创建好后,在该行执行完就结束了
Add(10, 20);
}
int main()
{
test01();
return 0;
}
同时,函数调用运算符的重载必须是成员函数。全局函数实现时会报错。
总结
本文对C++有关运算符重载的知识进行了详细归纳。
可总结为几点:
①、除了左右移运算符、赋值运算符、函数调用运算符以外,其余函数都可以用全局函数或成员函数实现对运算符的重载。
②、左右移运算符重载只能使用全局函数。
③、赋值运算符与函数调用运算符重载只能使用成员函数。
等等..............(这里就不再详细列举了,大家可以回顾下文中的红色字体)。