operator

运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。

在c++中,可以定义一个处理类的新运算符。这种定义很像一个普通的函数定义,只是函数的名字由关键字operator及其紧跟的运算符组成。差别仅此而已。它像任何其他函数一样也是一个函数,当编译器遇到适当的模式时,就会调用这个函数。

定义重载的运算符就像定义函数,只是该函数的名字是operator@,这里的@代表了被重载的运算符。函数的参数中参数个数取决于两个因素。

  1. 运算符是一元(一个参数)的还是二元(两个参数);

  2. 运算符被定义为全局函数(对于一元是一个参数,对于二元是两个参数)还是成员函数(对于一元没有参数,对于二元是一个参数-此时该类的对象用作左耳参数)

可以重载的运算符

运算符类型 运算符
算术运算符 加法运算符(+) 减法运算符(-) 乘法运算符(*) 除法运算符(/) 取模运算符(%)
关系运算符 相等运算符(==) 不等运算符(!=) 大于运算符(>) 小于运算符(<) 大于等于运算符(>=) 小于等于运算符(<=)
逻辑运算符 逻辑与运算符(&&) 逻辑或运算符(||) 逻辑非运算符(!)
位运算符 按位与运算符(&) 按位或运算符(|) 按位异或运算符(^) 按位取反运算符(~) 左移运算符(<<) 右移运算符(>>)
赋值运算符 简单赋值运算符(=) 复合赋值运算符(+=、-=、*=、/=、%= 等)
自增自减 自增运算符(++) 自减运算符(--)
成员访问 成员访问运算符(.) 指针成员访问运算符(->)
数组访问 下标运算符([])
函数调用 函数调用运算符(())
类型转换 类型转换运算符
并非所有的运算符都可以重载,例如:点运算符(.)、域解析运算符(::)、作用域运算符(::)等不能被重载。

image-20240223095047902

示例

通过一个对于加法的重载来计算时间的相加。

#ifndef TIME1_H_
#define TIME1_H_

class Time {
private:
	int hours;
	int minutes;
public:
	Time();
	Time(int h, int m = 0);
	void AddMin(int m);
	void AddHours(int h);
	void Reset(int h = 0, int m = 0);
	Time operator+(const Time& t) const;/*在函数的末尾加上 const 表示这是一个常成员函数,即在函数内部不允许修改对象的成员变量。,参数添加const表示函数内部不会更改t对象*/
	void Show() const;
};

#endif 

参数是引用,但是返回类型不是引用,目的是提高效率,速度更快,使用的内存更少。

返回值不可以是引用,因为函数会创建一个新的Time对象来表示其他两个Time对象的和,返回对象会创建对象的副本,而调用函数可以使用它。如果返回类型是Time&,则引用的是重载符是局部变量,在函数结束时会被删除,因此引用会指向一个不存在的对象。

#include<iostream>
using namespace std;
#include "time1.h"

Time::Time() {
	hours = minutes = 0;
}
Time::Time(int h, int m) {
	hours = h;
	this->minutes = m;
}

void Time::AddMin(int m) {
	minutes += m;
	hours += minutes / 60;
	minutes %= 60;
}

void Time::AddHours(int h) {
	hours += h;
}
void Time::Reset(int m, int h) {
	hours = h;
	minutes = m;
}
Time Time::operator+(const Time& t) const {
	Time sum;
	sum.minutes = minutes + t.minutes;
	sum.hours = hours + t.hours +sum.hours/60;
	sum.minutes %= 60;
	return sum;
}
void Time::Show() const {
	cout << hours << "hours, " << minutes << "mins" << endl;
}
#include<iostream>
using namespace std;
#include "time1.h"

int main() {
	Time regina;
	Time ivanlee(1, 27);
	Time baby(7, 17);
	cout << "regina time: ";
	regina.Show();
	cout << endl;
	cout << "ivanlee time: ";
	ivanlee.Show();
	cout << endl;
	cout << "baby time: ";
	baby.Show();
	cout << endl;

	regina = ivanlee + baby; // +重载,可以直接调用相加函数
	cout << "regina time:";
	regina.Show();
	cout << endl;

	ivanlee = regina.operator+(ivanlee);//通过调用函数的方式使用重载
	cout << "regina.operator+(regina):";
	ivanlee.Show();
	cout << endl;

	return 0;
}

程序首先定义三个对象,然后通过两种不同是的使用方式。operator+()使得可以使用函数表示法或者运算符表示法调用,编译器将根据操作数的类型来确定如何做。

image-20240223110759771

	regina = ivanlee + baby + baby2;
	regina = ivanlee.operator+(baby + baby2);
	regina = ivanlee.operator+(baby.operator+(baby2));

那上述这三种方法是否都合法呢?

image-20240223111211940

自增自减(++/--)运算符重载

重载的++和--运算符有点让人不知所措,因为我们总是希望能根据它们出现在所作用对象的前面还是后面来调用不同的函数。解决办法很简单,例如当编译器看到++a(前置++),它就调用operator++(a),当编译器看到a++(后置++),它就会去调用operator++(a,int).

//Time1.h
Time operator--(int);
Time operator++();

operator-- 是后置递减运算符的重载版本。这里的 int 参数只是一个标记,用于区分前置和后置递减运算符。

//time1.cpp
Time Time::operator--(int) {
	int newmin = this->minutes--;
	Time tmp(this->hours, newmin);
	return tmp;/*一个临时对象 tmp 的副本*/
	
}
Time Time::operator++() {
	++this->minutes;
	return *this;
}

返回tmp这是因为 operator--(int) 是后置递减运算符的重载版本,它返回的是之前的值,而不是递减后的值。在递减操作完成后,我们需要返回递减前的值作为结果,同时更新对象的值。因此,在后置递减运算符中,我们需要先创建一个临时对象保存当前值,然后再执行递减操作,最后返回保存的临时对象。这样做是为了保持后置递减操作的语义,即返回旧值,然后再进行递减操作。所以,return tmp; 返回的是保存旧值的临时对象,而不是对象本身。如果你想要返回对象本身,应该使用引用类型的返回值,如 Time& 类型。

但是下面返回的是*this在递增/递减运算符的实现中,我们通常需要返回调用递增/递减运算符的对象的引用。因此,我们需要使用 * 解引用指针,以便返回对象本身的引用。

Time regina(1, 27);
regina--;
cout << "regina--:";
regina.Show();
cout << endl;
++regina;
cout << "++regina:";
regina.Show();
cout << endl;

image-20240223114554276

重载指针运算符->

重载指针的箭头操作符(->),通常用于访问类中的成员函数或成员变量。箭头操作符被用来访问类的成员的简便方法,特别适用于封装了指针的类。要使用 operator->,首先需要创建一个封装了指针的类,并在该类中定义 operator-> 运算符的重载函数。

在最初的.h文件里我们多声明的一些成员

#pragma once
#ifndef TIME1_H_
#define TIME1_H_

class Myclass {
public:
	void display() {
		cout << "operator->()" << endl;
	}
};

class Time {
private:
	int hours;
	int minutes;
	Myclass* ptr;
public:
	Time();
	Time(int h, int m = 0);
	Time(Myclass* p);
	Myclass* operator->();
};

#endif 

然后将新的构造函数和重载运算符进行定义

Time::Time(Myclass* p) {
	ptr = p;
}
Myclass* Time::operator->() {
	return ptr;
}

Time::operator->() 是指定了箭头运算符重载函数属于 Time 类
重载箭头运算符的目的是为了可以像访问结构体或类的成员变量一样,直接通过指针访问对象的成员变量或成员函数。在这个例子中,如果返回的是 MyClass
类型,则可以通过箭头运算符来访问 MyClass 对象的成员变量或成员函数。
MyClass* 表示返回类型,Time 表示所属的类名。*

这样我们再定义新的Time的对象时,引入了Myclass成员的指针,就可以直接调用Myclass类里的功能了。

	Myclass regina;
	Myclass* p = &regina;
	Time ivan(p);
	ivan->display();
	Myclass* regina2 = new Myclass();
	Time ivan2(regina2);
	ivan2->display();
	return 0;

image-20240225231005924

重载运算符*

在 C++ 中,星号运算符 * 可以用于两种不同的场合:

  1. 作为指针类型声明时,表示该变量是一个指针变量;
  2. 作为取值运算符或解引用运算符时,用于访问指针指向的对象。

如果你想要使用 * 运算符来访问 Time 类内部保存的 MyClass 对象的成员变量和成员函数,那么可以通过重载解引用运算符 operator* 来实现。

*-> 运算符在 C++ 中都用于访问类对象的成员,但它们的作用有所不同:

  • * 运算符
    • 用于解引用指针,获得指针指向的对象。
    • 当你有一个指针,想要获取指针指向的对象时,可以使用 * 运算符。
  • -> 运算符
    • 用于通过指针访问对象的成员。
    • 当你有一个指向对象的指针,并且想要访问该对象的成员变量或成员函数时,可以使用 -> 运算符。

举个例子,假设有一个指向类对象的指针 ptr

CodeMyClass* ptr = new MyClass();

如果要调用 MyClass 对象的成员函数 display(),可以按照以下方式使用 *-> 运算符:

// 使用 * 运算符和 . 运算符
(*ptr).display();

// 使用 -> 运算符
ptr->display();

上述两种调用方式是等价的,但使用 -> 运算符更加简洁直观,尤其是在处理指针数组或链表时,能够更清晰地表达代码逻辑。

operator*() 函数的定义中使用 & 是为了返回指向 MyClass 对象的引用。这样做是为了允许对返回的对象进行修改,并且避免了不必要的对象复制。

当你使用 * 运算符解引用指针时,你通常希望能够通过该指针修改指向的对象。为了实现这一点,operator*() 函数应该返回一个引用类型。

Time 类中重载的 operator*() 函数中,MyClass& 表示返回一个指向 MyClass 对象的引用。通过返回引用,你可以对 MyClass 对象进行修改,而不是创建一个新的对象副本。

如果在 operator*() 函数中不使用 &,而是返回 MyClass 类型的对象,那么返回的将是对象的副本,而不是原始对象本身。这意味着对返回的对象的修改将不会影响到原始对象。

因此,在这种情况下,使用 & 是为了返回一个指向原始对象的引用,以便允许对原始对象进行修改。

//Myclass& operator*();

Myclass& Time::operator*() {
	return *pptr;
}
	Myclass* regina2 = new Myclass();
	Time ivan2(regina2);
	(*ivan).display();

重载运算符=

赋值符常常初学者的混淆。这是毫无疑问的,因为’=’在编程中是最基本的运算符,可以进行赋值操作,也能引起拷贝构造函数的调用。并且对于一个类里有没有指针所引起的拷贝构造是不一样的,首先介绍没有指针的情况。

赋值运算符重载的主要目的是实现自定义类型对象之间的赋值操作,使得在将一个对象赋值给另一个对象时能够按照程序员定义的方式进行成员变量的赋值操作。

通过重载赋值运算符,可以实现对象之间的赋值操作符(=)的行为。在 C++ 中,默认情况下,对象之间的赋值操作会简单地将一个对象的每个成员变量的值复制到另一个对象中,这被称为浅拷贝。但对于包含动态内存分配等资源的类对象来说,简单的浅拷贝可能会导致资源管理问题,因此需要通过重载赋值运算符来实现深拷贝,确保资源的正确释放和复制。

通过在类中重载赋值运算符,程序员可以控制对象之间的赋值行为,从而实现自定义的赋值逻辑,比如深度复制指针指向的动态内存、更新对象状态等。这样可以确保在对象赋值时正确处理类内部资源的管理,避免潜在的内存泄漏或资源错误使用问题。

如果你的类没有重载赋值运算符,那么在执行 regina = ivanlee; 这行代码时,编译器会自动为你生成一个默认的赋值运算符。这个默认的赋值运算符会针对每一个成员变量执行浅拷贝,将右侧对象的对应成员变量值复制到左侧对象中。

如果你的类中只包含基本数据类型的成员变量,那么默认的赋值运算符可以正常工作,regina = ivanlee; 会将 ivanlee 对象的成员变量值复制到 regina 对象中。

但是,如果你的类中包含指针类型的成员变量,并且这些指针指向动态分配的内存,那么默认的赋值运算符就不能满足你的需求了。默认的赋值运算符会简单地将指针的值复制到左侧对象中,从而导致两个对象指向同一块内存。当其中一个对象被销毁时,这块内存就会被释放,从而导致另一个对象的指针变成了悬空指针,引发未定义的行为

Time& Time::operator=(const Time &obj) {
	if (this != &obj) {
		
		this->hours = obj.hours;
		this->minutes = obj.minutes;
		/*深拷贝是指在进行对象拷贝时,不仅要复制对象本身的数据,
		还要复制对象内部指向的动态分配内存的数据。
		在你的赋值运算符重载函数中,并没有处理对象内部可能存在的动态分配内存的情况。

		如果 Time 类的成员变量 hours 和 minutes 是基本数据类型(如 int),
		那么你目前的实现可以满足基本的浅拷贝需求。但如果 Time 类包含指针类型的成员变量,
		你需要确保在赋值操作时,对这些指针指向的内存进行深度复制,以避免浅拷贝导致的问题。*/
		// 执行深拷贝
		/*if (obj.ptr) {
			delete ptr;
			ptr = new Myclass(*obj.ptr);
		}

		if (obj.pptr) {
			delete pptr;
			pptr = new Myclass(*obj.pptr);
		}*/
	}

	return *this;
}



Time regina(1, 27);
Time ivanlee(7, 17);
regina = ivanlee;
ivanlee.AddHours(1);
regina.Show();
ivanlee.Show();

这样的话就完成了一次深拷贝,并且regina和ivanlee有各自的存储空间,修改任何一方不会影响另一方。

image-20240227173238364

不能使用&&和||

不能重载operator&& 和 operator|| 的原因是,无法在这两种情况下实现内置操作符的完整语义。说得更具体一些,内置版本版本特殊之处在于:内置版本的&&和||首先计算左边的表达式,如果这完全能够决定结果,就无需计算右边的表达式了--而且能够保证不需要。我们都已经习惯这种方便的特性了。

我们说操作符重载其实是另一种形式的函数调用而已,对于函数调用总是在函数执行之前对所有参数进行求值。

重载遇上友元函数

重载和友元函数结合的意义在于通过友元函数的方式,实现对类的私有成员的访问,并且可以根据需要对不同版本的友元函数进行重载。这样可以灵活地扩展类的功能,并且保持良好的封装性。

具体来说,结合重载和友元函数可以实现以下几点意义:

  1. 访问私有成员:通过友元函数,可以在函数体外部访问类的私有成员,而不破坏类的封装性。这在某些情况下是非常有用的,例如需要在函数外部对私有数据进行操作或显示。
  2. 灵活性:重载友元函数可以根据不同的参数类型或数量实现不同的功能。这种灵活性可以让你根据具体需求选择合适的友元函数版本,从而更好地满足不同的使用场景。
  3. 代码复用:通过重载友元函数,可以避免在不同情况下写多个类似的友元函数,提高代码的重用性和可维护性。
  4. 简化接口:将一些与类相关的操作定义为友元函数,可以简化类的接口,使得类的使用更加直观和方便。

总的来说,重载和友元函数结合使用可以带来更大的灵活性、可读性和可维护性,同时也能够更好地平衡封装性和功能扩展性。因此,在设计类的时候,可以考虑使用友元函数来扩展类的功能,并根据需要进行重载以实现更丰富的功能。

class Time {
private:
	int hours;
	int minutes;
	/*Myclass* ptr;
	Myclass* pptr;*/
public:
	friend void display(const Time& t);
};
void display(const Time& t);
------------
    # mytime.cpp
    void display(const Time& t) {
	
		cout << "Time: " << t.hours << ":" << t.minutes  << endl;
	
}
#    Time regina(1, 27);
	Time ivanlee(7, 17);
	regina = ivanlee;
	display(regina + ivanlee);

image-20240227174919500