【C/C++】运算符重载

本文为 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){}

尝试失败后,我们发现成员函数无法实现左移运算符的重载。因此我们使用全局函数实现左移运算符重载。

这时我们只需要将参数 coutp1 传入即可,我们希望实现的效果为 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,不能写 floatdouble

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 个函数,分别为:

  1. 默认构造函数(无参,函数体为空)
  2. 默认析构函数(无参,函数体为空)
  3. 默认拷贝构造函数,对属性进行值拷贝
  4. 赋值运算符 operator=,对属性进行值拷贝

如果类中有属性指向堆区,做赋值操作时也会出现浅拷贝问题重复释放内存的问题,我们需要用深拷贝解决。

我们创建两个 Person 类的对象,并对其进行赋值操作。注意类中存在指向堆区的属性:

#include <iostream>
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。这样在释放内存时,p1p2 有独立的内存,就不会发生重复释放的问题了。

在这里插入图片描述

当作等号赋值时,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;
}

此时便可以正常运行了。

关系运算符重载

更新中……

函数调用运算符重载

更新中……

有帮助的话点个赞加关注吧 😃

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值