C++运算符重载

C++运算符重载详解:让自定义类型拥有原生运算符的能力

1. 运算符重载的基本概念

运算符重载是C++一项强大的特性,它允许我们为自定义类型(类或结构体)重新定义运算符的行为。通过运算符重载,我们可以让自定义类型像内置类型一样使用标准的运算符语法,使代码更加直观和自然

从本质上讲,运算符重载是函数重载的一种特殊形式。当我们重载一个运算符时,实际上是在定义一个特殊的成员函数或全局函数,函数名由operator关键字后接要重载的运算符组成

cpp 复制代码
a + b;        // 等价于 a.operator+(b) 或 operator+(a, b)
a == b;       // 等价于 a.operator==(b) 或 operator==(a, b)

运算符重载提供了语法糖,让代码更加直观和易读。比较以下两种写法:

cpp 复制代码
// 使用运算符重载
list1 + list2;

// 不使用运算符重载
list1.concat(list2);

第二种写法显然更加符合直觉,让自定义类型与内置类型有一致的操作方式

2. 算术运算符重载

2.1 加法运算符重载

在代码中,复数类的加法运算符重载展示了运算符重载的基本用法:

cpp 复制代码
Complex operator+(Complex& a, Complex& b)
{
    Complex ret;
    ret.real = a.real + b.real;
    ret.image = a.image + b.image;
    return ret;
}

运算符重载有两种实现方式:​成员函数形式全局函数形式

成员函数形式 隐含一个this指针参数,只需要一个显式参数:

cpp 复制代码
class Complex {
public:
    Complex operator+(const Complex& other) {
        Complex ret;
        ret.real = this->real + other.real;
        ret.image = this->image + other.image;
        return ret;
    }
};

全局函数形式需要两个显式参数,通常需要声明为类的友元函数以访问私有成员

cpp 复制代码
class Complex {
    friend Complex operator+(const Complex& a, const Complex& b);
    // ...
};

代码中使用了全局函数形式,并将它们声明为友元函数,这样可以访问Complex类的私有成员realimage

2.2 减法运算符重载

减法运算符的重载与加法类似,只需改变运算逻辑:

cpp 复制代码
Complex operator-(Complex& a, Complex& b)
{
    Complex ret;
    ret.real = a.real - b.real;
    ret.image = a.image - b.image;
    return ret;
}

3. 流运算符重载

3.1 输出运算符<<重载

输出运算符<<的重载需要特别注意返回类型和参数类型。代码中实现了这一功能:

cpp 复制代码
ostream& operator<<(ostream& cout, Complex& other)
{
    cout << other.real << "+" << other.image << "i";
    return cout;
}

这里有几个关键点:

  1. 返回类型必须是ostream&(引用),这样才能支持链式输出如cout << a << b << c;
  2. 第一个参数是ostream&类型,通常是cout或其变体
  3. 第二个参数是要输出的对象
  4. 函数返回输出流对象本身,使链式操作成为可能

如果返回void类型,将无法实现链式输出。这是因为cout << c << endl会被解析为(cout << c) << endl,如果cout << c返回void,那么void << endl就是非法的。

3.2 避免不必要的拷贝

使用const Complex& other而不是Complex other的好处:避免拷贝构造。当对象较大时,传引用可以显著提高性能。正确的声明应该是:

cpp 复制代码
ostream& operator<<(ostream& cout, const Complex& other);

这里的const确保不会意外修改对象状态。

4. 自增运算符重载

自增运算符有前置和后置两种形式,需要分别处理。

4.1 前置自增运算符

cpp 复制代码
Complex& operator++()//前置++
{
    this->real += 1;
    return *this;
}

前置++返回引用 ,这是为了保持与内置类型一致的行为。这样++a本身可以作为左值使用

4.2 后置自增运算符

cpp 复制代码
Complex operator++(int)//后置++
{
    Complex a = *this;//保存原始值
    this->real += 1;
    return a;//返回原始值
}

后置++通过int参数与前置版本区分,这个参数仅用于区分,并不实际使用

。它返回的是而不是引用,因为返回的是局部对象,不能返回引用。

注释中提到的,后置++返回临时对象,这个对象在函数结束后会被销毁,所以不能返回引用。

5. 赋值运算符重载

赋值运算符重载需要特别注意深拷贝自赋值问题。

cpp 复制代码
Jeff& operator=(Jeff &a)
{
    if (age)
    {
        delete age;
        age = NULL;
    }
    age = new int;//分配新内存
    *age = *a.age;
    return *this;
}

这里有几个重要考虑:

  1. 检查自赋值 :虽然代码中没有显式检查,但a = a这样的自赋值应该安全处理
  2. 释放旧资源:在分配新资源前释放已有资源,防止内存泄漏
  3. 深拷贝:创建新内存并复制内容,而不是简单复制指针
  4. 返回引用 :支持链式赋值a = b = c

改进版本应该包含自赋值检查:

cpp 复制代码
Jeff& operator=(const Jeff &a)
{
    if (this != &a) { // 自赋值检查
        if (age) {
            delete age;
        }
        age = new int(*a.age);
    }
    return *this;
}

6. 关系运算符重载

关系运算符重载通常返回bool值,用于比较对象。

cpp 复制代码
bool operator==(const Point& a)const
{
    return this->x == a.x && this->y == a.y;
}

bool operator<(const Point& a)const
{
    int c = x * x + y * y;
    int d = a.x * a.x + a.y * a.y;
    return c < d;
}

代码中通过比较点到原点的距离来定义<运算符,这是一种常见的做法。注意这些函数被声明为const,因为它们不应该修改对象状态。

7. 函数调用运算符重载

函数调用运算符()的重载创建了所谓的仿函数​(functor)。

cpp 复制代码
int operator()(int a, int b)
{
    data++;
    return a + b + data;
}

仿函数比普通函数更灵活,因为它们可以保持状态。如你的示例所示,每次调用都会增加data的值,这是普通函数无法做到的。

仿函数可以记录调用过程中的状态,比普通函数更加灵活,常用于STL算法中的定制行为

8. 运算符重载的规则与最佳实践

8.1 可重载的运算符

C++允许重载大部分运算符,包括:

  • 算术运算符:+, -, *, /, %
  • 关系运算符:==, !=, <, >, <=, >=
  • 逻辑运算符:&&, ||, !
  • 赋值运算符:=, +=, -=, *=, /=
  • 下标运算符:[]
  • 函数调用运算符:()
  • 流运算符:<<, >>
  • 自增自减:++, --

8.2 不可重载的运算符

有些运算符不能重载,包括:

  • 成员访问运算符:.
  • 成员指针访问运算符:.*
  • 作用域解析运算符:::
  • 条件运算符:?:(三目运算符)
  • sizeof运算符
  • typeid运算符

8.3 最佳实践

  1. 保持语义一致性:重载的运算符应该保持与内置类型相似的语义
  2. 考虑返回值类型
    • 算术运算符通常返回新对象
    • 复合赋值运算符通常返回引用
    • 关系运算符返回bool
  3. 正确处理常量性 :不修改对象的函数应声明为const
  4. 遵循三/五法则:如果定义了拷贝构造函数、拷贝赋值运算符、析构函数中的一个,通常需要定义其他相关函数

9. 总结

运算符重载是C++面向对象编程的重要特性,它让自定义类型能够以更自然的方式集成到语言中。通过合理使用运算符重载,我们可以编写出更加直观、易维护的代码。

关键要点:

  1. 运算符重载的本质是函数重载,遵循函数重载的规则
  2. 选择成员函数还是全局函数形式取决于具体需求
  3. 流运算符<<>>通常重载为全局友元函数
  4. 赋值运算符需要处理自我赋值和深拷贝问题
  5. 前置和后置自增/自减运算符通过参数区分
  6. 函数调用运算符重载创建仿函数,可以保持状态

合理使用运算符重载可以极大提高代码的可读性和易用性,但也要避免滥用,保持运算符的直观语义。当你面对自定义类型需要类似内置类型的操作时,运算符重载是一个强大的工具。

完整代码(杂糅版选取复制):

cpp 复制代码
#include<iostream>
using namespace std;
#include<string>


int main()
{
	//1.加法运算符
	int a = 520;
	int b = 1314;
	cout << a + b<<endl;
	//2.字符串拼接
	string c = "520";
	string d = "1314";
	cout << c + d << endl;

}
复数类:
class Complex
{
	friend ostream& operator<<(ostream& cout, Complex& other);
	friend Complex operator+(Complex& a, Complex& b);
	friend Complex operator-(Complex& a, Complex& b);
public:
	Complex() :real(0), image(0)
	{

	}
	Complex (int real, int image)
	{
		this->real = real;
		this->image = image;
	}
	//Complex operator+(Complex& other)//加了operator后可以进行a+b操作,这个是成员函数形式
	//{
	//	Complex ret;
	//	ret.real = this->real + other.real;//this是指针用->表示,other是引用对象用.表示
	//	ret.image = this->image + other.image;
	//	return ret;
	//}
	void print()
	{

		cout << real << "+" << image<<"i" << endl;//cout传进来了
	}
private:
	int real;
	int image;
};
Complex operator+(Complex& a,Complex&b)//加了operator后可以进行a+b操作,全员函数重载+改-就变-了
{
	Complex ret;
	ret.real = a.real+b.real;//this是指针用->表示,other是引用对象用.表示
	ret.image = a.image+b.image;
	return ret;
}
Complex operator-(Complex& a, Complex& b)//加了operator后可以进行a+b操作,全员函数重载+改-就变-了
{
	Complex ret;
	ret.real = a.real - b.real;//this是指针用->表示,other是引用对象用.表示
	ret.image = a.image - b.image;
	return ret;
}
//void operator<<(ostream& cout, Complex& other)//void 不行
//{
//	cout << other.real << "+" << other.image<<"i" << endl;
//}
ostream& operator<<(ostream& cout, Complex& other)//我左移后需要不断的输出所以要返回ostream可以继续加,实现链式输出!!!
{
	cout << other.real << "+" << other.image << "i";
	return cout;
}//补充知识点为什么ostream要加&,因为cout是ostream类型的函数,全局只有一个并且不希望外部调用也不希望内部调用,调用的话会自动删除!!!可以用f12看拷贝那一块的源码
//去掉&后会自动调用拷贝函数,然后拷贝函数就会被删除就无法调用了!!!
int main()
{
	Complex a(10,20);
	Complex b(5,8);
	Complex c = a + b; //a.add(b);
	Complex d = a - b;
	//c.print();
	//d.print();
	////左移函数重载

	cout << c << endl;//因为左移函数没有返回值void类型,我们要返回ostream这个类型才可以继续进行返回操作
	return 0;

}


函数的递增

class Complex
{
	friend ostream& operator<<(ostream& cout, const Complex &other);//Complex other每次都会走拷贝构造,已知这个不能修改,我们加一个&就可以避免拷贝构造了
	friend Complex operator+(Complex& a, Complex& b);
	friend Complex operator-(Complex& a, Complex& b);

private:
	int real;
	int image;

public:
	Complex() :real(0), image(0)
	{

	}
	Complex(int real, int image)
	{
		this->real = real;
		this->image = image;
	}
Complex& operator++()//前置++,不加引用就会生成新的对象,加了之后就是指向自己的
	{
		this->real += 1;
		return *this ;
	}
Complex operator++(int)//后置++,加引用就会返回临时的变量但是我,不想要这个临时的我要本身的,所以不加&让编译器自己生成一个拷贝函数
{
	Complex a = *this;//等于对象本身,临时的变量会被析构掉所以会返回随机值
	this->real += 1;
	return a;//返回原来的对象
}
};
ostream& operator<<(ostream& c,const Complex& a)
{
	cout << a.real << "+" << a.image << "i";
	return c;
}

int main()
{
	int x = 1;
	cout << ++x<<endl;
	cout << ++x<<endl;

	Complex a(10, 20);
	/*++a;*/
	//cout << ++a << endl;
	//cout << ++(++a) << endl;
	//cout << a << endl;//第一++成功了,再调用一次就会生成新的对象与原来的a没有关系了所以要加引用确定原本对象;
	cout << a++ << endl;
	cout << (a++)++ << endl;
	cout << a << endl;//加了&后这边就会报错为什么因为a的返回值是一个可修改的左值,所以我们可以用const去修饰这个返回值,这样就可以避免这样的事情发生,这样也是规范的写法



}
赋值重载运算
class Jeff
{
	
public:
	Jeff():age(NULL)
	{
		
	};
	Jeff(int age)
	{
		this->age = new int;
		*(this->age) = age;
	}
int *age;
~Jeff()
{
	if (age != NULL)
	{
		delete age;
age = NULL;
	}
	
}
Jeff& operator=(Jeff &a)
{
	if (age)
	{
		delete age;
		age = NULL;
	}
	age = new int;//新的内存,析构不会析构到这一快
	*age = *a.age;
	return *this;
}



};
int main()
{

	Jeff a(1);
	Jeff b(2);
	cout << *(a.age) << endl;//内存泄漏
	cout << *(b.age) << endl;
	a = b;//a b两个对象本来有那个内存的,第一次a调用时析构函数把a的对象销毁了,第二次b调用时由于两个都指向同一块地址,b调用完成后又一次析构,这时就造成二次析构了

	cout << *(a.age) << endl;
	cout << *(b.age) << endl;
	Jeff c(3);
	a = b = c;


	return 0;
}

关系运算符重载
class Point
{
	int x, y;
public:
	Point():x(0), y(0)
	{

	};
	Point(int x, int y)
	{
		this->x = x;
		this->y = y;
	}
	bool operator==(const Point& a)const
	{
		return this->x == a.x && this->y == a.y;
	  }
	bool operator<(const Point& a)const
	{
		int c = x * x + y * y;
		int d = a.x * a.x + a.y * a.y;
		return c < d;

	}

	bool operator>(const Point& a)const
	{
		if (*this==a)
		{
			return false;
		}
		else if (*this < a)
		{
			return false;
		}
		else {
			return true;
		}
	}
};
int main()
{
	Point a(2,6);
	Point b(1,8);
	if (a > b)
	{
		cout << "a到原点的距离大于b" << endl;
	}
	else if (a == b)
	{
		cout << "a到原点的距离等于b" << endl;
	}
	else {
		cout << "a到原点的距离小于b" << endl;
	}

	return 0;

}
 


函数调用运算符重载
class HJM
{
private:
	int data;
public:
	HJM() :data(0)
	{

	};
	int operator()(int a, int b)
	{
		data++;
		return a + b + data;
	};//仿函数调用可以记录当前的状态
	
};
int Add(int a, int b)
	{
		return a + b;
	}
int main()
{
	HJM add;
	cout << add(10, 5) << endl;
	cout << add(10, 5) << endl;
	cout << add(10, 5) << endl;
	cout << add(10, 5) << endl;
	cout << add(10, 5) << endl;
	cout << add(10, 5) << endl;
	cout << add(10, 5) << endl;
	cout << Add(10, 5)<< endl;
	cout << Add(10, 5) << endl;
	cout << Add(10, 5) << endl;
	cout << Add(10, 5) << endl;
	cout << Add(10, 5) << endl;
	return 0;
}
相关推荐
YouEmbedded2 小时前
解码智能指针
开发语言·c++·unique_ptr·shared_ptr·auto_ptr·weak_ptr
她说..2 小时前
Spring AOP场景3——接口防抖(附带源码)
java·后端·spring·java-ee·springboot
海上彼尚2 小时前
Go之路 - 7.go的函数
开发语言·后端·golang
计算机毕设指导62 小时前
基于微信小程序的积分制零食自选平台【源码文末联系】
java·spring boot·mysql·微信小程序·小程序·tomcat·maven
神仙别闹2 小时前
基于QT(C++)实现(图形界面)连连看
java·c++·qt
BioRunYiXue2 小时前
双荧光素酶报告基因实验
java·运维·服务器·数据库·人工智能·数据挖掘·eclipse
Geoking.2 小时前
深度理解 Java synchronized —— 从原理到实战
java·开发语言
sailing-data2 小时前
【UI Qt】入门笔记
开发语言·qt·ui