【C++】类和对象-深度剖析默认成员函数-下

> 🍃 本系列为初阶C++的内容,如果感兴趣,欢迎订阅🚩

> 🎊个人主页:[小编的个人主页])小编的个人主页

> 🎀 🎉欢迎大家点赞👍收藏⭐文章

> ✌️ 🤞 🤟 🤘 🤙 👈 👉 👆 🖕 👇 ☝️ 👍


目录

🐼前言

🐼赋值运算符重载

🐼运算符重载

前置++和后置++:

流插入<<流提取>>

[🐼 赋值运算符重载](#🐼 赋值运算符重载)

🐼取地址运算符重载

🐼const成员函数

🐼取地址运算符重载


🐼前言

在上一节,我们分享了C++默认成员函数的前3个,构造函数,析构函数,和拷贝构造函数

如果还有疑惑的可以浏览我的上一篇文章默认成员函数-上

这一节我们将继续探讨C++剩下的默认成员函数:

🐼赋值运算符重载

🐼运算符重载

在我们之前对内置类型做运算时,如**+ - * /** 不需要我们考虑,计算机底层就实现了😄,内置类型的运算是计算机💻是通过一系列硬件和软件解决方案来处理的。但是当运算符被用于类类型的对象时,对于这样一个复杂对象的运算,C++语言允许我们通过运算符重载的形式指定新的含义

运算符重载是具有特殊名字的函数,他的名字是由operator和后面要定义的运算符共同构成 。和其他函数⼀样,它也具有其返回类型和参数列表以及函数体。

👇我们来看下面这个例子:

cpp 复制代码
 重载为全局的⾯临对象访问私有成员变量的问题 
 有⼏种⽅法可以解决: 
 1、成员放公有  
 2、Date提供getxxx函数 
 3、友元函数 
 4、重载为成员函数 
#include<iostream>
using namespace std;
class Date
{
	//将运算符重载函数设置为友元
	friend bool operator==(const Date& x1, const Date& x2);

public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
    
    //重载为成员函数
	/*bool operator==(const Date& d)
	{
		return _year == d._year
			&& _month == d._month
			&& _day == d._day;
	}*/
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	//提供成员变量的接口
	//int GetYear()
	//{
	//	return _year;
	//}
//private:
	int _year;
	int _month;
	int _day;
};
bool operator==(const Date& x1, const Date& x2)
{
	return x1._year == x2._year
		&& x1._month == x2._month
		&& x1._day == x2._day;
}

//全局,但是成员变量变成共有
int operator-(const Date& x1, const Date& x2)
{

	return 0;
}

int main()
{
	Date d1(2024, 11, 16);
	Date d2(2024, 11, 15);
	//运算符重载的两种写法
	cout << (d1 == d2) << endl;
	cout << (operator==(d1,d2))<<endl;//

}

🌲在上述例子中,我们创建了两个日期类的对象d1,d2 ,我们想判断对象d1是否等于d2,这里重载了运算符(==) ,来帮助我们判断两个对象是否相等。我们重载成了全局的运算符重载函数,但是面临无法访问类私有成员的问题

这里有四种解决办法:

  1. 提供成员变量的函数接口,如在类中实现成员函数GetYear,GetMonth等,通过这种方式可以访问到成员变量,但是这就说明外界可以访问到内部成员变量,破坏了封装
  2. 成员变量设置为共有,在上述我们就是这样做的,但这并不能保护私有成员。
  3. 运算符重载函数设置为友元,这样也可以访问到私有成员变量。友元我们在后面的文章中会分享到。
  4. 运算符重载函数写到类内,成为成员函数 ,这种方式是我们最常用的,没有破坏封装,提供接口给外部用户使用,通过运算符重载函数实现类的对象之间的运算。

👇下面,我们来看运算符重载函数作为成员函数的例子:

cpp 复制代码
#include<iostream>
using namespace std;
class Date
{
	//将运算符重载函数设置为友元
	friend bool operator==(const Date& x1, const Date& x2);

public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//重载为成员函数
	bool operator==(const Date& d)
	{
		return _year == d._year
			&& _month == d._month
			&& _day == d._day;
	}
	//d1-d2
	int operator-(const Date& d)
	{
        //内部逻辑省略
		return 0;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	
private:
	int _year;
	int _month;
	int _day;
};



int main()
{
	Date d1(2024, 11, 16);
	Date d2(2024, 11, 15);
	cout << (d1 == d2) << endl;
	cout << d1 - d2 << endl;
}

🍁在这个类中我们定义了两个运算符重载函数,重载==来判断两个对象是否相等,重载**-** 来计算两个日期对象相隔几天。但是我们发现,和全局的运算符重载不同,形参只有一个,这是为啥呢😮?因为在运算符重载作为成员函数是有this指针的😏。

运算符重载特性:

  • 重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。⼀元运算符有⼀个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第⼀个参数(d1),右侧运算对象传给第二个参数(d2)。
  • 如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针 ,因此运算符重载作为成员函数时,参数比运算对象少⼀个

🍁因此**-** 运算符的显示参数是int operator-(Date* const this,const Date& d),只不过成员函数this指针就隐藏了。

  • 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持⼀致。也就是重载运算符是给类对象重载的,内置类型的运算符不需要我们定义。硬件就实现了
  • 不能通过连接语法中没有的符号来创建新的操作符:比如operator@。
  • . * :: sizeof ?: . 注意以上5个运算符不能重载。重载操作符至少有⼀个类类型参数,不能通过运算符重载改变内置类型对象的含义,如: int operator+(int x, int y),这就是不对的运算符重载

前置++和后置++

👇我们下面重载一下对象的前置++和后置++:

cpp 复制代码
#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//d1++
	Date operator++(int)//规定,后置++,增加一个int形参
	{
		Date tmp = *this;
		(*this)._day += + 1;
		return tmp;
	}
	//++d
	Date& operator++()//不加int,好区分
	{
		(*this)._day += +1;
		return *this;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};


int main()
{
	Date d1(2024, 11, 16);
	Date d2(2024, 11, 15);
	//运算符重载的两种写法
	d2 = d1++;
	d1.Print();
	d2.Print();
	Date d3 = ++d1;
	d3.Print();
	d1.Print();
}

🍃我们实现了对象的前置++,后置++两个运算符重载,但是他们的名字一样(名字是由operator和后面要定义的运算符共同构成),这怎么解决😮?

C++为了避免这种问题,规定:重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,无法很好的区分。 C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,方便区分

这里还有一个小细节,两个运算符重载的对象,一个引用返回(引用返回可以提高效率),一个非引用返回,具体看返回后对象还是否存在


流插入<<流提取>>

👇我们再来实现对**流插入<<流提取>>**的运算符重载,完成对对象的输入输出:

cpp 复制代码
#include<iostream>
using namespace std;
class Date
{
	//声明为友元
	friend ostream& operator<<(ostream& out, const Date& d);
	friend istream& operator>>(istream& in, Date& d);

public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};
ostream& operator<<(ostream& out,const Date& d)
{
	cout << d._year << "年" << d._month << "月" <<d. _day << "日" << endl;
	return out;
}
istream& operator>>(istream& in, Date& d)
{
	in >> d._year >>d._month >> d._day;
	return in;
}

int main()
{
	Date d1,d2;
	cin >> d1>>d2;
	cout << d1 << d2;
}

🍃在上述例子中,我们通过重载运算符流插入<<,流提取>> ,分别对对象d1,d2,进行输入和输出。由于输出流ostream这个类的对象是cout,以便函数内部对内置类型输出,但要注意,这里out必须是第一个参数,我们刚刚提到:二元运算符有两个参数,二元运算符的左侧运算对象传给第⼀个参数(out),右侧运算对象传给第二个参数(d),如果交换,那么格式应该是d<<cout,这显然不符合我们的习惯。

🍃如果把流插入和流提取作为成员函数,那么第一个参数一定是this指针 ,所以,我们必须要声明在类外部,为了能突破类域访问到类中的成员变量 ,我们将这两个运算符重载函数设置为友元函数

🌻总结:重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第⼀个形参位 置,第⼀个形参位置是左侧运算对象

🐼 赋值运算符重载

⭐️概念:

赋值运算符重载 是⼀个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值 ,这里要注意跟拷贝构造区分😃,拷贝构造用于⼀个对象拷贝初始化给另⼀个要创建的对象

特点:

  • 赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数,这点很重要。赋值运算重载的参数建议写成const当前类类型引用,否则会传值传参会有拷贝。
  • 有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景。拷贝构造函数无返回值。

👇举个例子:

cpp 复制代码
#include<iostream>
using namespace std;
class Date
{
	friend ostream& operator<<(ostream& out, const Date& d);

public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& d)
	{
		cout << " Date(const Date& d)" << endl;
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	// 传引⽤返回减少拷⻉ 
	// d1 = d2;
	Date& operator=(const Date& d)
	{
		// 不要检查⾃⼰给⾃⼰赋值的情况 
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		// d1 = d2表达式的返回对象应该为d1,也就是*this 
		return *this;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
ostream& operator<<(ostream& out, const Date& d)
{
	cout << d._year << "年" << d._month << "月" << d._day << "日" << endl;
	return out;
}

int main()
{
	Date d1(2024, 7, 5);
	Date d2(d1);
	Date d3(2024, 7, 6);
	d1 = d3;
	// 需要注意这⾥是拷贝构造,不是赋值重载 
	// 请牢牢记住赋值重载完成两个已经存在的对象直接的拷⻉赋值 
	// ⽽拷贝构造⽤于⼀个对象拷贝初始化给另⼀个要创建的对象 
	Date d4 = d1;
	d1 = d2 = d3;
	cout << d1 << d2 << d3 << d4;

	return 0;
}

🍃我们实现了了赋值运算符重载函数,用引用接受实参const Date& d是为了提高效率 ,减少不必要的拷贝,返回值为Date& 在减少不必要拷贝的同时,将返回值作为第一个对象,支持连续赋值。最后,我们调用刚刚重载的运算符<<来打印类对象。

在赋值操作时,对于自已给自已赋值的操作单独处理,this!=&d,确保不必要的麻烦,如在堆上开辟空间,可能会造成冲突。

运行结果:

  • 没有显式实现时,编译器会自动生成一个默认赋值运算符重载,默认赋值运算符重载行为跟默认构造函数类似,对内置类型成员变量会完成**值拷贝/浅拷贝(**一个字节⼀个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。

🐬如果我们不显示写赋值运算符重载,编译器对内置类型进行浅拷贝,运行结果与上图一致.

🐋总结:像Date这样的类成员变量全是内置类型且没有指向什么资源 ,编译器自动生成的赋值运算符重载就可以完成需要的拷贝,所以不需要我们显示实现赋值运算符重载对于有资源的类,如栈等 ,编译器自动生成的赋值运算符重载完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自已实现深拷贝(对指向的资源也进行拷贝)

🐟像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的赋值运算符重载会调用Stack的赋值运算符重载, 也不需要我们显示实现MyQueue的赋值运算符重载。这里还有一个小技巧,如果⼀个类显示实现了析构并释放资源,那么他就需要显示写赋值运算符重载,否则就不需要。这点和拷贝构造函数很相似

🔍我们发现:赋值运算符重载很像拷贝构造函数和运算符重载的叠加,但必须为成员函数 ,运算符重载没有规定必须为成员函数,赋值运算符重载和拷贝构造函数的最大区别是:**对已经创建的对象进行赋值拷贝。**而拷贝的规则和拷贝构造函数很相似.

🐼取地址运算符重载

🐼const成员函数

⭐️概念:

将const修饰的成员函数称之为const成员函数,const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改🚫。

❄️对于上述日期类的Printf成员函数,const修饰Date类的Print成员函数,Print隐含的this指针由 Date* const this 变为 const Date* const this

👇我们这里区分一下:

Date* const this 这里const修饰的是this指针,表示this指针指向的地址不能改变。

const Date* this 这里修饰的是this指针指向的内容,表示对类中成员变量函数等不能修改。

由于C++中this指针时隐示的,我们不可能修改,所以如果不想修改this指针指向的值,将const修饰成员函数放到成员函数参数列表的后面

举例:

cpp 复制代码
#include<iostream>
using namespace std;
class Date
{
public:
	//完整写法
	// void Print(const Date* const this) const
	//在函数列表后加上const,表示成员函数中的任何成员类不可能修改,才加,安全性更高
	void Print() const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

用const修饰成员函数这样的做法安全性更好,也体现了封装的特性。

🔍我们这个也总结一下const对象和const成员函数的调用关系:

普通对象可以调用const成员函数,是一种权限的缩小。

const对象可以调用const成员函数,是一种权限的平移。

const对象不可以调用普通成员函数,是一种权限的放大。

普通对象可以调用普通的成员函数,是一种权限的平移。

const成员函数内部可以调用普通的成员函数,是一种权限的缩小。

普通成员函数内部不可以调用const成员函数,是一种权限的放大。

🐼取地址运算符重载

⭐️概念:

取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,⼀般这两个函数编译器自动生成的就可以够我们用了,不需要去显示实现。

除非我们不想让对方获得类对象的地址,就可以自已实现⼀份,胡乱返回⼀个地址。

举例:

cpp 复制代码
#include<iostream>
using namespace std;
class Date {
public: 
	Date()
		:_year(2024)
		,_month(11)
		,_day(20)
	{

	}
	Date* operator&()
	{
	//return this;
	return nullptr; 
	}
	const Date* operator&()const
	{
		return this;
		// return nullptr;
	}
private:
	int _year=1; // 年 
	int _month=1; // ⽉ 
	int _day=1; // ⽇ 
};

int main()
{
	Date d1;
	const Date d2;
	cout << &d1<<endl;
	cout<<&d2<<endl;
}

我们这里分别创建了一个普通对象和const修饰的对象来调用取地址运算符重载。发现对象的地址我们是可以操控的。没有特殊场景,编译器会自动调用,不用我们显示实现

感谢你看到这里,如果觉得本篇文章对你有帮助,点赞👍收藏 ⭐️吧,你的支持就是我更新的最大动力。⛅️🌈 ☀️

相关推荐
C++忠实粉丝39 分钟前
计算机网络socket编程(2)_UDP网络编程实现网络字典
linux·网络·c++·网络协议·计算机网络·udp
Mongxin_Chan1 小时前
【Cpp】指针与引用
c++·算法
SSL_lwz1 小时前
P11290 【MX-S6-T2】「KDOI-11」飞船
c++·学习·算法·动态规划
熬夜学编程的小王2 小时前
【C++篇】从基础到进阶:全面掌握C++ List容器的使用
开发语言·c++·list·双向链表·迭代器失效
悄悄敲敲敲2 小时前
C++:智能指针
开发语言·c++
zhangpz_2 小时前
c ++零基础可视化——vector
c++·算法
萨达大2 小时前
23种设计模式-模板方法(Template Method)设计模式
java·c++·设计模式·软考·模板方法模式·软件设计师·行为型设计模式
刀鋒偏冷2 小时前
ninja: error: ‘/opt/homebrew/Cellar/opensslxxx/xx/lib/libssl.dylib
c++
理论最高的吻2 小时前
98. 验证二叉搜索树【 力扣(LeetCode) 】
数据结构·c++·算法·leetcode·职场和发展·二叉树·c
沈小农学编程3 小时前
【LeetCode面试150】——202快乐数
c++·python·算法·leetcode·面试·职场和发展