运算符重载

下面我会以一个向量Vector类进行讲解:设向量X=(x1,x2,...,xn)和Y=(yl,y2,t..,yn),它们之间的加、减分别定义为: X+Y=(xl+yl,x2+y2,...,xn+yn),X-Y=(x1-y1,x2-y2,...,xn-yn)。


#include 
using namespace std;

class Vector
{
private:
	int _x1;
	int _x2;
	int _x3;
public:
	// 构造函数
	Vector(int x1 = 0, int x2 = 0, int x3 = 0)
		:_x1(x1), _x2(x2), _x3(x3)
	{}
	Vector(const Vector& v)
		:_x1(v._x1), _x2(v._x2), _x3(v._x3)
	{}

};

一、运算符重载的定义

运算符重载是指在C++中,可以对已有的运算符赋予新的含义或功能,使得它们能够用于自定义的数据类型或类对象。通过运算符重载,可以使得用户自定义的类型支持与内置类型相似的操作,包括算术运算、逻辑运算、关系运算等。operator 是关键字,后面跟着要重载的运算符的符号,然后是参数列表和具体的实现代码。

具体就是说,对于基本类型来说,我们可以直接使用 +操作符,如:

int a = 1;
int b = 2;
int c = a + b;

但是,对于自定义类型来说,由于+运算符只能用于基本类型的运算,所以+不能直接实现自定义类型对象之间的加法,如:

Vector v1(1,2,3);
Vector v2(2,3,4);
Vector v3;
v3 = v1 + v3;

如果,我们要想实现这种效果,就需要自己进行重载,下面我会实现的。

其实操作符可以看作为一个函数,我们可以用不同函数实现方法得到不同的结果,而且它们使用的是同一个名字。

二、运算符重载的规则

不是所有的操作符都可重载,除了以下5个运算符之外,其余的都可以重载。

1.不可重载的运算符

(1).(成员访问运算符)。
(2)*(成员指针访问运算符)。
(3):(作用域分辨符)。
(4)sizeof(计算数据大小运算符)。
(5)?:(三目运算符)
前面两个在C++中提供访问成员的基本功能,禁止重载可以避免混乱。:和sizeof的操作对象
是类型,而不是表达式。三目运算符本质上是if语句,不值得重载。

2.运算符重载的规则

运算符重载时,可以根据自定义类型的特殊需要,对原有操作符给出新的实现,但不能改变
原有操作符的基本语义,操作符的优先级、结合性和所需要的操作数个数。例如,不能将+改成
减运算,也不能改成单目运算等。

3.运算符重载的方法

1) 重载为类的成员函数

下面我以实现+和-操作符进行演示。

对于Vector加法运算的要求是,将两个Vector对象中每一个成员变量进行相加。

对于 +操作符 来说,操作符左边的操作数就相当于调用函数的对象,右操作数就是该函数的参数。所以,我们可以把 +重载函数 作为Vector类成员函数。

同时为了实现连加的效果,要为这个函数设置一个返回值。如果返回值为void时,就不能实现连加的效果:

在实现v1+v2之后由于函数没有返回值,所以空+v3,发生错误。
正确的写法应该是:

Vector operator+(const Vector& v)
{
    Vector ret;
    ret._x1=_x1+v._x1;
    ret._x2=_x2+v._x2
    ret._x3=_x3+v._x3;
    return ret;
}

修正后的代码中,operator+方法的返回类型被修改为Vector类型。在该方法内部,我们创建一
个新的Vector对象ret,然后将当前向量对象和参数向量对象的对应分量相加,并将结果保存在
ret中,然后进行ret+v3。最后,我们通过return ret;返回了相加后的结果给v3。

前置++、后置++;前置--、后置--的运算符重载:
// 前置++
Vector& operator++()
{
    ++_x1;
    ++_x2;
    ++_x3;
    return *this;
}

// 后置++
Vector operator++(int)
{
    Vector temp(*this);
    ++_x1;
    ++_x2;
    ++_x3;
    return temp;
}

// 前置--
Vector& operator--()
{
    --_x1;
    --_x2;
    --_x3;
    return *this;
}

// 后置--
Vector operator--(int)
{
    Vector temp(*this);
    --_x1;
    --_x2;
    --_x3;
    return temp;
}

前置++ 和前置-- 返回引用类型,并直接对成员变量进行递增或递减操作,然后返回自身引用。后置++ 和后置-- 则需要创建一个临时对象保存当前对象的值,再对成员变量进行递增或递减操作,并返回保存的临时对象。

后置++/--的函数参数要有一个int类型的参数。

在 C++ 中,后置++ 运算符的参数列表是一个整数类型,这个整数类型是用来区分前置++ 和后置++ 的。前置++ 不需要参数,因为它对对象进行修改并直接返回对象本身,所以不需要额外的参数。

而后置++ 会返回修改前的值,需要先将原始值保存到临时变量中,再对对象进行修改,最后返回保存的临时变量。但是,我们不能只通过返回值来区分前置和后置++,因为两种运算符的返回值都是对象本身。因此,C++ 标准规定,后置++ 运算符必须要有一个 int 类型的占位参数,以区分前置和后置++。当运算符被调用时,编译器会自动传入一个值为 0 的 int 类型参数,这个参数并不会被使用,仅用于区分前置和后置++。

如果你不加 int 参数,编译器就会把它理解为前置++。

同时后置++/--重载的实现可以使用对前置++/--的复用

// 前置++
Vector& operator++()
{
    ++_x1;
    ++_x2;
    ++_x3;
    return *this;
}

// 后置++
Vector operator++(int)
{
    Vector temp(*this);
    ++(*this)
    return temp;
}

// 前置--
Vector& operator--()
{
    --_x1;
    --_x2;
    --_x3;
    return *this;
}

// 后置--
Vector operator--(int)
{
    Vector temp(*this);
   --(*this)
    return temp;
}

2)重载为类的非成员函数

如果将运算符重载函数作为全局函数进行重载,这样就会导致该函数无法访问类的私有成员,进而不能进行打印。

如果不重载为类的类成员函数,但又想要访问类型私有成员变量,可以使用 友元函数

  • 友元函数的介绍

友元函数是一种特殊的函数,它具有访问类的私有成员的权力。友元函数可以在类的外部定义,但是在函数声明前使用 friend 关键字进行声明,并且在类的作用域内生效。

以下是友元函数的一些特点和用途:

  1. 访问私有成员:友元函数可以访问类的私有成员变量和私有成员函数,即使这些成员在类的外部是不可见的。这为某些特定情况下需要直接访问类的私有数据提供了一种方式,例如重载运算符或者实现非成员函数操作类的私有数据。

  2. 灵活性:友元函数可以独立于类的对象存在,在类的外部被调用。这使得友元函数不受类的对象限制,可以直接访问类的私有成员,而不需要通过对象来调用。

  3. 类之间的协作:友元函数可以用于不同类之间的协作。当两个类之间需要共享私有成员或者需要进行特定操作时,可以将其中一个类的成员函数声明为另一个类的友元函数,从而实现彼此之间的交互。

需要注意的是,虽然友元函数可以访问类的私有成员,但它不是类的成员函数,因此在调用时不会自动传递类的对象指针或者对象引用。友元函数可以作为全局函数定义,也可以是其他类的成员函数。

下面我实现流插入<<操作符的实现来介绍这一种方法。

  • 流插入<< 操作符的介绍
#include
int main()
{
    std::cout << "Hello C++!";
    return 0;
}

在这个程序中,我们使用流插入操作符向屏幕打印一个字符串 "Hello C++!" ,前面我们说过,操作符的使用就相当于一个函数,左操作数是用来调用函数的对象,右操作数是函数的参数。

所以,这里的 cout 其实是类型为类ostream的一个对象,而字符串 "Hello C++!" 则作为参数,操作符<<是ostream类的一个成员函数。

同时,使用操作符<<进行打印基本类型,它能够自动识别数据类型也是因为运算符重载。

运算符重载_第1张图片

例如:

运算符重载_第2张图片

重载函数 operator<< 返回 ostream& 引用的原因是为了实现链式输出。

在 C++ 中,使用 << 运算符进行输出时,通常会连续使用多个运算符来输出不同的值。例如:

cout << "Value 1: " << value1 << " Value 2: " << value2;

如果 operator<< 返回的不是引用,而是一个新的 ostream 对象,那么上述代码就无法正常工作,因为每个 << 运算符只能接收一个右操作数。

通过返回 ostream& 引用,重载函数可以将输出流对象按照顺序进行连接,使得多个输出可以连续执行。同时,返回引用还可以保证对同一个流对象进行多次输出时,仍然是对同一个对象进行操作,避免了创建临时对象和多次拷贝的开销,提高了效率。

因此,返回 ostream& 引用可以实现链式输出,提高代码的可读性和效率。

  • 流插入<<的重载

由于标准库里面只实现了基本类型的流插入重载,如果我们想要用<<打印Vector中的对象时,就需要我们自己实现。

        ① 流插入操作符不能写成类成员函数

这是因为成员函数的调用方式是通过对象来调用的,即通过左操作数调用,操作符将被错误地解释为成员函数的调用。而在流插入操作符中,左操作数是用于输出数据的流对象,而不能是类的实例对象

为了能够正确地将数据输出到流中,并且使得类成员能够调用这个函数,我们需要使用全局函数或友元函数的形式来重载流插入操作符。这样可以保证左操作数是流对象,并且右操作数是要输出的数据,从而符合 C++ 的语法和规范。

如果将流插入操作符写成类成员函数,使用场景如下:

#include 
using namespace std;

class Vector
{
private:
	int _x1;
	int _x2;
	int _x3;
public:
	// 构造函数
	Vector(int x1 = 0, int x2 = 0, int x3 = 0)
		:_x1(x1), _x2(x2), _x3(x3)
	{}
	Vector(const Vector& v)
		:_x1(v._x1), _x2(v._x2), _x3(v._x3)
	{}

	Vector operator+(const Vector& v)
	{
		Vector ret;
		ret._x1 = _x1 + v._x1;
		ret._x2 = _x2 + v._x2;
		ret._x3 = _x3 + v._x3;
		return ret;
	}

    // 第一种实现方法--重载为类成员函数
    ostream& operator<<(ostream& out)
    {
	    out << _x1 << " " << _x2 << " " << _x3 << endl;
        reutrn out;
    }
};


int main()
{
	Vector v1(1, 2, 3);
	Vector v2(2, 3, 4);
	Vector v3(1,1,1);
	v3 = v1 + v2 + v3;
	 
	v3 << cout; 
	//v3.operator<<(cout);

	return 0;
}

使用时需要写成    v3 << cout; (等价于这种写法:v3.operator<<(cout);  )

第一个参数是左操作数,第二个参数是右操作数;这样如果写在Vector类中,左操作数就必须是Vector的一个对象,不能使cout作为左操作数。而标准库里面的写法为:cout << v3;  

所以对于<<操作符来说,不能将其重载为类成员函数,同时这里为了实现连续流插入的应用场景,需要一个返回值。

        ② 流插入操作符不能写成全局函数

既然,我们想要将cout作为左边操作数,就要重载为非类成员函数。

但是如果直接重载为全局函数就不能,访问类私有成员变量了。

#include 
using namespace std;

class Vector
{
private:
	int _x1;
	int _x2;
	int _x3;
public:
	// 构造函数
	Vector(int x1 = 0, int x2 = 0, int x3 = 0)
		:_x1(x1), _x2(x2), _x3(x3)
	{}
	Vector(const Vector& v)
		:_x1(v._x1), _x2(v._x2), _x3(v._x3)
	{}

	Vector operator+(const Vector& v)
	{
		Vector ret;
		ret._x1 = _x1 + v._x1;
		ret._x2 = _x2 + v._x2;
		ret._x3 = _x3 + v._x3;
		return ret;
	}

};


// 第二种实现方法--全局函数
ostream& operator<<(ostream& out, const Vector& v)
{
	out << v._x1 << " " << v._x2 << " " << v._x3 << endl;
	return out;
}


int main()
{
	Vector v1(1, 2, 3);
	Vector v2(2, 3, 4);
	Vector v3(1,1,1);
	v3 = v1 + v2 + v3;
	cout << v3;
	
	return 0;
}

作为全局函数,这样cout就能够作为左操作数了,但是又导致访问不了私有成员变量的问题。所以也不能重载为全局函数。

由上面介绍的友元函数可以解决不能访问私有成员变量的问题。

        ③ 流插入操作符写成友元函数
#include 
using namespace std;

class Vector
{
private:
	int _x1;
	int _x2;
	int _x3;
public:
	// 构造函数
	Vector(int x1 = 0, int x2 = 0, int x3 = 0)
		:_x1(x1), _x2(x2), _x3(x3)
	{}
	Vector(const Vector& v)
		:_x1(v._x1), _x2(v._x2), _x3(v._x3)
	{}

	Vector operator+(const Vector& v)
	{
		Vector ret;
		ret._x1 = _x1 + v._x1;
		ret._x2 = _x2 + v._x2;
		ret._x3 = _x3 + v._x3;
		return ret;
	}

	friend ostream& operator<<(ostream& out, const Vector& v);
	friend istream& operator>>(istream& in, Vector& v);

};



// 第二种实现方法--友元函数
ostream& operator<<(ostream& out, const Vector& v)
{
	out << v._x1 << " " << v._x2 << " " << v._x3 << endl;
	return out;
}

int main()
{
	Vector v1(1, 2, 3);
	Vector v2(2, 3, 4);
	Vector v3(1,1,1);
	v3 = v1 + v2 + v3;
	cout << v3;
	
	return 0;
}
        ④<<并不是只能用友元函数实现

迭代器也可以用来访问类的私有成员。如果一个类中有一个私有成员容器(如 std::string),我们可以使用迭代器来访问容器中的元素,从而间接访问私有成员。

ostream& operator<<(ostream& out, const string& s)
{
	for (auto ch : s)
	{
		out << ch;
	}
	return out;
}
        ⑤ 总结

对于没有迭代器的类,如果你想重载运算符 << 并将其放在全局作用域中,而不使用友元函数,则可能会遇到问题。

在 C++ 中,如果要通过重载运算符 << 来输出一个对象的私有成员,通常需要使用友元函数或者将运算符重载函数声明为类的成员函数。如果类没有迭代器或其他方法来访问其私有成员,那么将运算符重载函数放在全局作用域中是无法访问这些私有成员的。

3)=运算符的介绍

无论是对于内置类型还是自定义类型,当= 操作符用于普通值拷贝,是不需要进行运算符重载的,编译器会自动生成一个浅拷贝的重载函数。

#include 
using namespace std;

class Vector
{
private:
	int _x1;
	int _x2;
	int _x3;
public:
	// 构造函数
	Vector(int x1 = 0, int x2 = 0, int x3 = 0)
		:_x1(x1), _x2(x2), _x3(x3)
	{}
	Vector(const Vector& v)
		:_x1(v._x1), _x2(v._x2), _x3(v._x3)
	{}

	Vector operator+(const Vector& v)
	{
		Vector ret;
		ret._x1 = _x1 + v._x1;
		ret._x2 = _x2 + v._x2;
		ret._x3 = _x3 + v._x3;
		return ret;
	}

	Vector operator-(const Vector& v)
	{
		Vector ret;
		ret._x1 = _x1 - v._x1;
		ret._x2 = _x2 - v._x2;
		ret._x3 = _x3 - v._x3;
		return ret;
	}
	friend ostream& operator<<(ostream& out, const Vector& v);
};

// 第二种实现方法--友元函数
ostream& operator<<(ostream& out, const Vector& v)
{
	out << v._x1 << " " << v._x2 << " " << v._x3 << endl;
	return out;
}

int main()
{
	Vector v1(1, 2, 3);
	Vector v2(2, 3, 4);
	Vector v3(1,1,1);
	v3 = v1 + v2 + v3;
	cout << v3;

	return 0;
}

在上面一个代码中,我们使用了=操作符,并且它的左右操作数并不是自定义类型,而是Vector类型的。虽然我们没有对=进行运算符重载,但是这个程序还是能够正常运行的。

对于自定义类型,如果没有显式重载赋值运算符(=操作符),C++会提供一个默认的赋值运算符。这个默认的赋值运算符会逐一复制类的所有成员变量,包括私有成员变量。因此,即使没有显式重载赋值运算符,也可以使用 = 操作符来进行赋值操作。

然而,使用默认的拷贝赋值运算符可能会导致一些问题。特别是在处理动态分配的内存或其他资源时,如果原对象被修改或销毁,而目标对象仍然持有对资源的引用或指针,那么目标对象就可能访问到无效的内存或资源,导致程序崩溃或产生不可预测的行为。

为了避免这种问题,如果类中存在指针成员或者动态分配的资源,建议显式地重载赋值运算符,以实现深拷贝,确保每个对象都有自己独立的资源副本。

如std::vector和std::string如果直接使用默认的赋值运算符,则进行的就是浅拷贝,这样会导致最后析构函数时会对同一块空间释放两次,导致错误。

对于深拷贝问题,我会在模拟实现STL中进行详细说明。


今天的分享就到这里了,如果,你感觉这篇博客对你有帮助的话,就点个赞吧!感谢感谢……

你可能感兴趣的:(开发语言,C++,运算符重载)