首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >今天你学C++了吗——C++中的类与对象(第三集)

今天你学C++了吗——C++中的类与对象(第三集)

作者头像
用户11352420
发布2024-12-25 10:28:29
发布2024-12-25 10:28:29
2170
举报
文章被收录于专栏:编程学习编程学习

运算符重载

我们前面学习到了函数可以重载,那运算符重载又是什么概念呢?

运算符重载的出现

像 + , - ,* 这些运算符我们可以直接用于内置类型,但是如果用在类类型的对象上面的时候就不可以了,因为这个行为是编译器没有定义的,就会报错~

例:

代码语言:javascript
复制
#include<iostream>
using namespace std;

class Example
{
private:
	int _a;

public:
	Example(int a)
	{
		_a = a;
	}
};

int main()
{
	//运算符我们可以直接用于内置类型
	int x = 1;
	int y = 2;
	cout << "x + y = " << x + y << endl;

	Example e1(4);
	Example e2(5);
	//err
	//类类型对象使用运算符时,这个行为是编译器没有定义的
	cout << "e1 + e2 = " << e1 + e2 << endl;
	return 0;
}

这个时候我们就给出了解决方案,使用运算符重载~

运算符重载规则

》 当 运算符被用于类类型的对象 时,C++允许我们通过 运算符重载的形式为运算符指定新的含义 。 》 C++规定 类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错。

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

》 重载运算符函数的 参数个数 和 该运算符作用的运算对象 数量相同~ 所以一般来讲重载运算符函数至少有一个参数~ 》 ⼀元运算符有⼀个参数,⼆元运算符有两个参数 》 二元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第二个参数 (严格的匹配原则)

》 如果⼀个 重载运算符函数是成员函数 ,则它的 第⼀个运算对象默认传给隐式的this指针 ~ 因此 运算符重载作为成员函数时,参数会比运算对象少一个(已经有一个this指针指向的内容了~)

例:(运算符重载函数为成员函数)

代码语言:javascript
复制
#include<iostream>
using namespace std;

class Example
{
private:
	int _x;
	int _y;
public:
	//全缺省构造函数——也可以叫默认构造函数
	Example(int x = 1, int y = 1)
	{
		_x = x;
		_y = y;
	}
	//运算符重载==
	//== 有两个操作对象,但是这里成员函数隐含一个this指针
	//严格匹配规则,左侧运算对象传给第⼀个参数(this指针)
	// 右侧运算对象传给第二个参数e

	bool operator== (const Example& e)
		//传参使用引用,减少拷贝次数
		//const 修饰,不希望被修改
	{
		return _x == e._x && _y == e._y;
	}
};

int main()
{
	Example e1(1, 1);
	Example e2(2, 2);
	//运算符==重载
	if (e1 == e2)//这种写法更加方便
	//if (e1.operator==(e2))
		//也可以像第二种这样显示写
	{
		cout << "e1 == e2" << endl;
	}
	else
	{
		cout << "e1 != e2" << endl;
	}
	return 0;
}

例:(运算符重载函数为全局函数)

当定义为全局函数就存在问题了,这是为什么呢?

很简单~因为在类里面_x和_y成员变量是私有的,所以在类外面是不可以访问的~那我们有什么方法解决呢?这里给出几种方法~

1、 成员放公有 2、 类里面提供getxxx函数 (获取成员变量的函数) 3、 友元函数 4、 重载为成员函数 (最开始的方法)

我们一个个来看:

》成员放公有

代码语言:javascript
复制
#include<iostream>
using namespace std;

class Example
{
public:
	int _x;
	int _y;

	//全缺省构造函数——也可以叫默认构造函数
	Example(int x = 1, int y = 1)
	{
		_x = x;
		_y = y;
	}
};
//运算符重载==(全局函数)
//== 有两个操作对象
//严格匹配规则,左侧运算对象传给第⼀个参数e1
//右侧运算对象传给第二个参数e2
//方法一:成员变量公有,类外就可以访问~
bool operator==(const Example& e1, const Example& e2)
{
	return e1._x == e2._x && e1._y == e2._y;
}
int main()
{
	Example e1(1, 1);
	Example e2(2, 2);
	//运算符==重载
	if (e1 == e2)
	{
		cout << "e1 == e2" << endl;
	}
	else
	{
		cout << "e1 != e2" << endl;
	}
	return 0;
}

》类里面提供getxxx函数 (获取成员变量的函数)

代码语言:javascript
复制
#include<iostream>
using namespace std;

class Example
{
private:
	int _x;
	int _y;
public:
	//全缺省构造函数——也可以叫默认构造函数
	Example(int x = 1, int y = 1)
	{
		_x = x;
		_y = y;
	}
	//const加在后面,const修饰对象不被修改(后面会讲到)
	int GetX()const
	{
		return _x;
	}
	//const加在后面,const修饰对象不被修改
    int GetY()const
	{
		return _y;
	}
};
//运算符重载==(全局函数)
//== 有两个操作对象
//严格匹配规则,左侧运算对象传给第⼀个参数e1
//右侧运算对象传给第二个参数e2

//方法二:类里面提供get函数
bool operator==(const Example& e1,const Example& e2)
{
	return (e1.GetX() == e2.GetX())&& (e1.GetY() == e2.GetY());
}
int main()
{
	Example e1(1, 1);
	Example e2(2, 2);
	
	//运算符==重载
	if (e1 == e2)
	{
		cout << "e1 == e2" << endl;
	}
	else
	{
		cout << "e1 != e2" << endl;
	}
	return 0;
}

友元函数 (这个在后面会详细讲解~这里先使用一下)

代码语言:javascript
复制
#include<iostream>
using namespace std;

class Example
{
private:
	int _x;
	int _y;
public:
	//全缺省构造函数——也可以叫默认构造函数
	Example(int x = 1, int y = 1)
	{
		_x = x;
		_y = y;
	}
	//使用友元函数,友元函数类外可以访问私有成员
	friend bool operator==(const Example& e1, const Example& e2);
};
//运算符重载==(全局函数)
//== 有两个操作对象
//严格匹配规则,左侧运算对象传给第⼀个参数e1
//右侧运算对象传给第二个参数e2

//方法三:使用友元函数
//友元函数只需要在声明处加friend
bool operator==(const Example& e1, const Example& e2)
{
	return e1._x == e2._x && e1._y == e2._y;
}
int main()
{
	Example e1(1, 1);
	Example e2(2, 2);

	//运算符==重载
	if (e1 == e2)
	{
		cout << "e1 == e2" << endl;
	}
	else
	{
		cout << "e1 != e2" << endl;
	}
	return 0;
}

还有一种方法就是把它设置为成员函数,前面实现过,这里就不多说啦~

运算符重载注意点

我们来看看其他的注意点~

》 运算符重载后, 优先级和结合性与对应的内置类型运算符保持⼀致

》 不能通过连接语法中没有的符号来创建新的操作符 ,比如operator@

》 .* :: sizeof ? : . 以上5个运算符不能重载 ~

其他运算符都好说,.* 这个运算符我们是没有见过的,那么这个运算符有什么作用呢?我们结合代码一起来看看~

例:

代码语言:javascript
复制
#include<iostream>
using namespace std;
class A
{
public:

	void func()
	{
		cout << "func( )" << endl;
	}
};

//typedef void(*)() PF;//err 错误的重命名函数指针写法
//正确写法:名称与前面嵌套在一起
//因为是成员函数前面加A::
typedef void(A::* PF)(); //成员函数指针类型

int main()
{
	// C++规定成员函数要加&才能取到函数指针
	PF pf = &A::func;
	//A::func到类里面找func函数
	//&A::func得到它的地址——成员函数指针

	A obj;//定义A类对象obj
	//(*pf)();//err //想要调用成员函数,有一个this指针
	// 对象调用成员函数指针时,使⽤.*运算符
	//先对pf解引用得到这个函数,调用对象的func函数
	(obj.*pf)();
	return 0;
}

这里使用 .* 操作符就可以通过函数指针调用对象的成员函数~

》 操作符 至少有⼀个类类型参数 ,注意 不能通过运算符重载改变内置类型对象的含义 (比如不能把加变成减) 如: int operator+(int x, int y) //err 没有类类型参数

》 ⼀个类需要重载哪些运算符,是看哪些运算符重载后有意义~ 》 像Date类重载operator-就有意义,但是重载operator+就没有意义~

》 重载++运算符时,有 前置++和后置++ , 运算符重载函数名都是operator++ 》 为了更好的区分它们~所以C++规定, 后置++重载时,增加一个int形参,跟前置++构成函数重载 ,这里加int 形参只是为了好区分,没有什么十分特别的含义~

例:

代码语言:javascript
复制
#include<iostream>
using namespace std;

class Example
{
private:
	int _x;
	int _y;
public:
	//构造函数
	Example(int x = 1, int y = 1)
	{
		_x = x;
		_y = y;
	}
	//前置++
	Example& operator++()
	{
		cout << "前置++" << endl;
		//先++再使用
		//也就是自己++后返回
		(*this)._x += 1;
		(*this)._y += 1;
		return *this;//返回使用引用减少拷贝次数
	}
	//后置++
	Example operator++(int)//后置++重载时,增加一个int形参
	{
		cout << "后置++" << endl;
		Example tmp = *this;
		//先使用再++
		(*this)._x += 1;
		(*this)._y += 1;
		return tmp;
		//不可以引用返回,tmp是临时对象,需要拷贝一份,避免后面销毁
	}
	//打印
	void Print()
	{
		cout << _x << "," << _y << endl;
	}
};
int main()
{
	Example e1(2, 2);
	Example e2(2, 2);
	//前置++
	Example e3 = ++e1;
	cout << "e3 = ++e1" << endl;
	e3.Print();
	//后置++
	Example e4 = e2++;
	cout << "e4 = e2++" << endl;
	e4.Print();

	cout << "e1" << endl;
	e1.Print();
	cout << "e2" << endl;
	e2.Print();
	return 0;
}

通过运算符重载也就达到了我们想要的效果~

<<和>>运算符重载

接下来我们来看看<<和>>运算符重载~

一般我们打印内容可以直接使用Print函数,使用<<和>>对类类型是不太现实的~所以我们就可以进行运算符重载,直接以相应的格式输出或者输入内容~ 》 重载<<和>>时(流插入和流提取运算符),需要重载为全局函数 》 我们知道<<和>>是 二元运算符 , 有两个操作数(这里一个是cout/cin,一个是类) 》通过查询我们可以发现 cout是ostream类型,cin是istream类型~ 》注意: ostream类型和istream类型是不支持拷贝的 它们之所以内置类型可以直接使用,是因为编译器以及重载好了不同的内置类型,但是自定义类型就需要我们自己实现~

》 如果重载为成员函数,this指针默认抢占了第一个形参位置,第⼀个形参位置是左侧运算对象,调用时就变成了 【 对象<<cout】,不符合我们使用习惯以及代码可读性~

》重载为全局函数把ostream/istream放到第⼀个形参位置,第二个形参位置是类类型对象

代码语言:javascript
复制
#include<iostream>
using namespace std;

class Example
{
private:
	int _x;
	int _y;
public:
	//构造函数
	Example(int x = 1, int y = 1)
	{
		_x = x;
		_y = y;
	}
	//err
	//成员函数第一个形参隐含this指针
	//参数列表就是【类,cout】,不符合我们使用习惯以及代码可读性
	/*void operator<<(ostream& out)
	{
		out << "(_x,_y)=" << "(" << _x << "," << _y << ")" << endl;
	}*/

	//使用友元函数
	//ostream& out 只可以使用引用,ostream类不支持拷贝
	friend void operator<<(ostream& out, const Example& e);
};

//类外使用友元函数
void operator<<(ostream& out, const Example& e)
{
	out << "(_x,_y) = " << "(" << e._x << "," << e._y << ")" << endl;
}

int main()
{
	Example e1(2, 2);
	Example e2(6, 6);
	cout << e1;
	cout << e2;
	return 0;
}

但是这里还有一个问题,就是不能连续输出~这里我们就需要给它优化一下~

》我们知道<<从左向右结合,只需要给一个返回值对象也就是cout就可以了~ 》返回值对象作为下一次的左操作数

代码语言:javascript
复制
//类外使用友元函数
ostream& operator<<(ostream& out, const Example& e)
{
	out << "(_x,_y) = " << "(" << e._x << "," << e._y << ")" << endl;
	return out;
}

这就达到了我们想要的效果~

我们来看看流提取(>>)运算符重载:

注意:(类型变为istream) istream& operator>>(istream& in, Example& e) // Example& e 不能加const,因为我们需要向里面写内容,是修改内容了的~

代码语言:javascript
复制
#include<iostream>
using namespace std;

class Example
{
private:
	int _x;
	int _y;
public:
	//构造函数
	Example(int x = 1, int y = 1)
	{
		_x = x;
		_y = y;
	}
	friend ostream& operator<<(ostream& out, const Example& e);
	friend istream& operator>>(istream& in, Example& e);
};

//类外使用友元函数
ostream& operator<<(ostream& out, const Example& e)
{
	out << "(_x,_y) = " << "(" << e._x << "," << e._y << ")" << endl;
	return out;
}

istream& operator>>(istream& in, Example& e)
// Example& e 不能加const,我们需要向里面写内容,是修改了的
{
	cout << "请输入x、y:" << endl;
	in >> e._x >> e._y;
	return in;
}
int main()
{
	Example e1(2, 2);
	Example e2(6, 6);
	cout <<"最开始:" << e1 << e2;
	cin >> e1 >> e2;
	cout << "cin后:" << e1 << e2;
	return 0;
}

赋值运算符重载

知道了运算符重载的概念~接下来,我们来看看一个比较特殊的运算符重载,赋值运算符重载~

》在内置类型里面我们可以使用赋值运算符(=)把一个变量的值传给另外一个变量

》对于自定义类型就不可以直接使用赋值运算符(=),因为这个行为是编译器没有定义的~ 》对于自定义类型,我们就可以使用赋值运算符重载~ 》注意:赋值运算符重载是⼀个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,需要与跟拷贝构造区分,拷贝构造用于⼀个对象拷贝初始化给另⼀个新创建的对象~

例:

代码语言:javascript
复制
#include<iostream>
using namespace std;

class Example
{
private:
	int _x;
	int _y;
public:
	//构造函数
	Example(int x = 1, int y = 1)
	{
		_x = x;
		_y = y;
	}

	//e2 = e1;
	void operator=(const Example& e)//第一个形参隐含this指针~
	{
		_x = e._x;
		_y = e._y;
	}
	friend ostream& operator<<(ostream& out, const Example& e);
	friend istream& operator>>(istream& in, Example& e);
};

》同时这里还有一个点需要优化一下~上面的代码不可以实现连续赋值~

》与<<和>>运算符类似,赋值运算符也是有返回值的,这样就可以实现连续赋值了~

代码语言:javascript
复制
//e3 = e2 = e1;
//引用返回,减少拷贝次数
Example& operator=(const Example& e)//第一个形参隐含this指针~
{
	if (this != &e)//如果不是自己给自己赋值,就进行赋值操作~
	{
		_x = e._x;
		_y = e._y;
	}
	return *this;//返回第一个,可以进行新的赋值
}

这样就可以实现连续赋值啦~

接下来,我们来总结一下赋值运算符重载的特点~

1. 赋值运算符重载是一个运算符重载,规定 必须重载为成员函数 (与其他的运算符有点区别) 》 赋值运算重载的 参数建议写成const 当前类类型引用 , 减少传值传参拷贝次数,同时不希望对象被修改

2. 有返回值 ,这样才可以 支持连续赋值 》 同时 返回值类型建议写成当前类类型引用,引用返回可以减少拷贝次数提高效率

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

比如上面的Example类注释掉赋值运算符重载,依然可以实现赋值,这就使用了编译器自动生成的 默认赋值运算符重载~

4. 》 像Date和Example这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的赋值运算符重载就可以完成需要的拷贝,所以不需要我们显示实现赋值运算符重载。 》 像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的赋值运算符重载完成的值拷贝/浅拷贝不符合我 们的需求,就需要我们实现深拷贝(对指向的资源也进行拷贝) 同时这里需要注意的是, 不能自己给自己赋值,如果自己给自己赋值,那么就会出问题~下面会结合例子来讲~ 》 像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的赋值运算符重载会调用Stack的赋值运算符重载, 也不需要我们显示实现MyQueue的赋值运算符重载。 》 小技巧 : 如果一个类显示实现了析构并释放资源,那么就需要我们显示写赋值运算符重载,否则就不需要~

例如栈赋值运算符重载~

代码语言:javascript
复制
//涉及资源,需要显示写赋值运算符重载
//st3 = st1
Stack& operator=(const Stack& st)
{
	//避免原来的st3空间不够,释放原来的空间,开辟与st3一样大的空间
	free(_a);
	_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);//开辟与st1一样大的空间
	if (_a == nullptr)
	{
		perror("malloc fail");
		exit(1);//开辟失败,退出程序
	}
	memcpy(_a, st._a, sizeof(STDataType) * st._top);//把st1上的值拷贝给st3
	_top = st._top;
	_capacity = st._capacity;
	return *this;//返回
}

初始:

st3=st1赋值后:

这也就完成了赋值操作,还有一个点是这里需要判断是不是自己给自己赋值~

》如果不判断的话,像上面的代码就会出问题~ 因为首先就释放了原来的空间,那么它新开的空间里面已经是随机值了~

》同时避免自己给自己赋值也可以减少浪费,提高效率~

正确代码:

代码语言:javascript
复制
//涉及资源,需要显示写赋值运算符重载
//st3 = st1
Stack& operator=(const Stack& st)
{
	if (this != &st)//不能自己给自己赋值
	{
		//避免原来的st3空间不够,释放原来的空间,开辟与st3一样大的空间
		free(_a);
		_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);//开辟与st1一样大的空间
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(1);//开辟失败,退出程序
		}
		memcpy(_a, st._a, sizeof(STDataType) * st._top);//把st1上的值拷贝给st3
		_top = st._top;
		_capacity = st._capacity;
	}
	return *this;//返回
}

这样就没有问题啦~

const成员函数

我们知道const可以修饰变量,那么const修饰成员函数又有什么特殊的意义呢?

》 将 const修饰的成员函数称之为const成员函数 (如果我们不希望函数内部的修改成员变量就可以加const)

比如上面的代码成员函数Print不加const就会造成权限放大~

》 const修饰成员函数放到成员函数参数列表的后面 (理解为规定) 》 const实际修饰该成员函数隐含的this指针 指向的内容 ,表明 在该成员函数中不能对类的任何成员进行修改 。 》 比如const 修饰Example类的Print成员函数, Print隐含的this指针由 Example* const this 变为 const Example* const this

例:

代码语言:javascript
复制
#include<iostream>
using namespace std;

class Example
{
private:
	int _a;
	int _b;
public:

	Example(int x,int y)
	{
		_a = x;
		_b = y;
	}
	//Print隐含的this指针由 Example* const this 变为 const Example* const this
	void Print()const
		//不希望函数内部修改成员变量
		// 在参数列表后面加const
	{
		cout << "_a = " << _a << "," << "_ b = " << _b << endl;
	}
};

int main()
{
	Example e1(1, 1);
	const Example e2(2, 2);

	e1.Print();
	e2.Print();//Print成员函数如果不使用const修饰,就会存在权限放大
}

这样就没有问题了~

取地址运算符重载

》 取地址运算符重载 分为 普通取地址运算符重载 和 const取地址运算符重载(一个用于普通对象,一个用于const修饰的对象), 不写编译器也会自动生成一份

例:

1.使用编译器自己生成的~

2.自己显示写

》 一般这两个函数编译器自动生成的就足够我们使用,不需要去显示实现 。 》 如果一些很特殊的场景,比如我们不想让别人取到当前类对象的地址,就可以自己实现一份,然后胡乱返回⼀个地址(有点小坏,但是也是为了代码安全性嘛)

例:

代码语言:javascript
复制
#include<iostream>
using namespace std;

class Example
{
private:
	int _a;
	int _b;
public:

	Example(int x,int y)
	{
		_a = x;
		_b = y;
	}
	//Print隐含的this指针由 Example* const this 变为 const Example* const this
	void Print()const
		//不希望函数内部修改成员变量
		// 在参数列表后面加const
	{
		cout << "_a = " << _a << "," << "_ b = " << _b << endl;
	}

	//普通版本——用于普通对象
	Example* operator&()
	{
		return this;//this指针就是当前对象地址
		也可以 return 假地址
	}

	//const版本——用于const修饰的对象
	const Example* operator&()const
		//Example* operator&()const  不可以这样写,返回就权限放大了
	{
		return this;//this指针就是当前对象地址
		//也可以 return 假地址
	}
};

int main()
{
	Example e1(1, 1);
	const Example e2(2, 2);

	e1.Print();
	e2.Print();//Print成员函数如果不使用const修饰,就会存在权限放大

	cout << &e1 << endl;
	cout << &e2 << endl;//不显示写&运算符重载,也可以使用编译器自动生成的~
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-12-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 运算符重载
    • 运算符重载的出现
    • 运算符重载规则
    • 运算符重载注意点
    • <<和>>运算符重载
  • 赋值运算符重载
  • const成员函数
  • 取地址运算符重载
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档