本文为 C/C++ 学习总结,讲解运算符重载。欢迎在评论区与我交流
运算符重载概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
对于内置数据类型,编译器知道如何使用加号进行运算:
int a = 10;
int b = 10;
int c = a + b;
但是对于我们下面定义的 Person
,编译器并不知道如何进行加法运算。
class Person {
public:
Person() {};
Person(int a, int b) {
this->m_A = a;
this->m_B = b;
}
public:
int m_A;
int m_B;
};
// 直接相加会报错
Person p3 = p1 + p2;
此时我们可以通过对加号运算符重载来解决这个问题。编译器给运算符重载起了一个通用的名字 operator
。
重载加号运算符有两种方式。我们首先通过重载成员函数重载 +
号。
Person operator+ (Person& p) {
Person tmp;
tmp.m_A = this->m_A + p.m_A;
tmp.m_B = this->m_B + p.m_B;
return tmp;
}
成员函数的调用:
Person p3 = p1.operator+(p2);
因为我们使用了编译器提供的重载运算符函数名,可以将上面的调用简化为:
Person p3 = p1 + p2;
再通过全局函数重载 +
号:
Person operator+(Person& p1, Person& p2) {
Person tmp;
tmp.m_A = p1.m_A + p2.m_A;
tmp.m_B = p1.m_B + p2.m_B;
return tmp;
}
此时调用方式为:
Person p3 = operator+(p1, p2);
同样可以简化为下面的形式:
Person p3 = p1 + p2;
我们再介绍一下运算符重载进行函数重载。此时我们将类对象与 int
型数据相加程序会报错:
Person p4 = p1 + 100;
此时需要进行函数重载:
Person operator+(Person& p1, int num) {
Person tmp;
tmp.m_A = p1.m_A + num;
tmp.m_B = p1.m_B + num;
return tmp;
}
左移运算符重载用于输出自定义的数据类型。例如定义一个 Person
类,如果直接输出类对象程序会报错:
class Person {
public:
Person(int A, int B) :m_A(A), m_B(B) {}
int m_A;
int m_B;
};
// 输出
Person p(10, 20);
cout << p;
我们首先考虑使用成员函数重载左移运算符。如果我们像下面这样写,相当于给对象 p1
又传入了一个 Person
对象。但是我们只有一个 Person
对象。
void operator<<(Person &p1){}
此时考虑传入 cout
对象,但是这种写法的本质效果为 p1 << cout
,这不是我们想要的结果。
void operator<<(cout){}
尝试失败后,我们发现成员函数无法实现左移运算符的重载。因此我们使用全局函数实现左移运算符重载。
这时我们只需要将参数 cout
和 p1
传入即可,我们希望实现的效果为 operator << (cout, p1)
(简化为 cout << p1
)。我们按 Ctrl 并点击 cout
转到其定义,发现 cout
属于 ostream
类型(输出流)的数据,称为标准输出流对象:
且全局只能有一个,我们使用引用方式创建,不能有新的标准输出流对象:
void operator<<(ostream& cout, Person& p) {
cout << "m_A = " << p.m_A << "m_B = " << p.m_B;
}
此时成功实现类对象的输出。但是如果我们想在后面加 endl
换行,程序会报错。可以无限追加输入是由于链式编程思想,如果 cout << p1
调用后返回 void
,则无法追加内容。我们需要返回 cout
:
ostream& operator<<(ostream& cout, Person& p) {
cout << "m_A = " << p.m_A << "m_B = " << p.m_B;
return cout;
}
这样就是实现了左移运算符的重载,可以直接输出类对象了。
在实际应用中,我们通常会将类属性定义为 private
,这样全局函数就无法访问类属性了,如果写 getA
这样的类方法接口又显得十分繁琐,这时可以使用友元函数 friend
。将下面的代码加到类的最开始,就可以访问私有属性了:
class Person {
friend ostream& operator<<(ostream& cout, Person& p);
public:
Person(int A, int B) :m_A(A), m_B(B) {}
/*void operator<<()*/
private:
int m_A;
int m_B;
};
同理,右移运算符也可以实现自定义数据类型的输入。这里我们直接给出结果:
istream& operator<<(istream& cin, Person& p) {
cin >> p.m_A >> p.m_B;
return cin;
}
// 输入
cin >> p1;
我们通过重载递增运算符实现自己的整型数据。首先定义类,并实现左移运算符重载:
class MyInteger {
friend ostream& operator<<(ostream& out, MyInteger myint);
public:
MyInteger() { m_Num = 0; }
private:
int m_Num;
};
ostream& operator<<(ostream& out, MyInteger myint) {
out << myint.m_Num;
return out;
}
void test01() {
MyInteger myInt;
cout << ++myInt << endl;
cout << myInt << endl;
}
此时输出整型类型会报错,这时需要对递增运算符进行重载。重载的类型有两种,一种是前置递增运算符,另一种是后置递增运算符。
我们首先来重载前置 ++ 运算符,如果返回值是 void
,则此时 ++myInt
返回的是 void
类型,无法进行输出。因此我们将返回值写为 MyInteger
并返回自身:
MyInteger operator++() {
//先++
m_Num++;
//再返回
return *this;
}
我们使用下面的代码进行测试:
void test01() {
MyInteger myInt;
cout << ++(++myInt) << endl;
cout << myInt << endl;
}
我们期望的结果是第一行输出 2,第二行输出 2,但第二行输出的结果是 1。这是因为我们返回的是值,相当于对新的数据进行 ++ 操作,我们应该返回引用,一直对同一个数据进行递增操作:
MyInteger& operator++() {
//先++
m_Num++;
//再返回
return *this;
}
下面我们重载后置 ++ 运算符。但当我们写下面的代码进行重载时,编译器会报错,提示“无法重载仅按返回值类型区分的函数”:
void operator++() {
}
这是因为两个同名函数发生了重定义的现象,并没有发生重载,返回值不能区分函数重载。因此我们需要使用 int
占位参数重载函数,用来区分前置和后置递增,并且只能写 int
,不能写 float
或 double
:
void operator++(int) {
// 先返回结果
// 后递增
}
如果我们先写返回结果,函数会直接结束,因此我们需要先记录当时结果,后递增,最后将记录结果返回:
MyInteger operator++(int) {
//先返回
MyInteger temp = *this; //记录当前本身的值
m_Num++;
return temp;
}
需要注意的是,后置递增需要返回值,而不是引用。如果返回引用,相当于返回了局部对象 temp
的引用,在函数结束后内存被释放。
使用下面的代码进行测试:
void test02() {
MyInteger myInt;
cout << myInt++ << endl;
cout << myInt << endl;
}
第一行输出 0,第二行输出 1,得到正确结果。这里不对 cout << (myInt++)++ << endl;
进行测试是因为 C++ 内置类型也无法实现 cout << (a++)++ << endl;
:
C++ 编译器给一个类添加 4 个函数,分别为:
如果类中有属性指向堆区,做赋值操作时也会出现浅拷贝问题重复释放内存的问题,我们需要用深拷贝解决。
我们创建两个 Person
类的对象,并对其进行赋值操作。注意类中存在指向堆区的属性:
#include
using namespace std;
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(18);
Person p2(20);
p2 = p1; // 赋值运算
cout << "p1的年龄为:" << *p1.m_Age << endl;
cout << "p2的年龄为:" << *p2.m_Age << endl;
}
int main() {
test01();
return 0;
}
执行程序后正常打印,但是程序会崩溃:
我们画图分析这个问题。我们首先创建 p1
,其中的 m_Age
指向堆区。当创建 p2
后,使用赋值运算符将 p1
赋值给 p2
后,p2
所指的空间也为 0x0011。所以当析构函数释放内存时,这个堆区内存被释放了两次,程序崩溃。
我们需要使用深拷贝解决这个问题,即重载赋值运算符,使得等号赋值时给 p2
也创建一个堆区,将 p1
堆区中的数值赋值给 p2
。这样在释放内存时,p1
和 p2
有独立的内存,就不会发生重复释放的问题了。
当作等号赋值时,p2
上已经指向了一个堆区的内存,应该先判断是否有属性在堆区,如果有先释放,然后再深拷贝。
void operator=(Person& p) {
// 编译器提供浅拷贝
//m_Age = p.m_Age;
// 先判断是否有属性在堆区
if (m_Age != NULL) {
delete m_Age;
m_Age = NULL;
}
// 深拷贝
m_Age = new int(*p.m_Age);
}
此时可以正常打印结果。但是内置的 C++ 类型可以进行连等操作,例如:
int a = 10;
int b = 20;
int c = 30;
c = b = a;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
但如果我们也使用重载的运算符进行连等操作,则会报错,如:
Person p1(18);
Person p2(20);
Person p3(30);
p3 = p2 = p1; //赋值操作
cout << "p1的年龄为:" << *p1.m_Age << endl;
cout << "p2的年龄为:" << *p2.m_Age << endl;
cout << "p3的年龄为:" << *p3.m_Age << endl;
这是因为 p2 = p1
返回的是 void
类型,无法将其赋值给 p3
,因此在重载运算符时还需要返回 Person
类型:
Person& operator=(Person& p) {
// 编译器提供浅拷贝
//m_Age = p.m_Age;
// 先判断是否有属性在堆区
if (m_Age != NULL) {
delete m_Age;
m_Age = NULL;
}
// 深拷贝
m_Age = new int(*p.m_Age);
// 返回自身。this指向自身,*this就是自身
return *this;
}
此时便可以正常运行了。
更新中……
更新中……
有帮助的话点个赞加关注吧