五. 运算符重载

发布时间 2023-12-04 17:18:44作者: Beasts777

文章参考:

C++ 运算符重载_c++ 重载=-CSDN博客

1. 概述

如果不进行特殊处理,C++默认的运算符只能对基本的常量或变量进行运算,不能用于对象之间的运算。但有时我们希望对这些运算符功能进行拓展,让他们能够支持更多的运算。运算符重载应运而生。它能够赋予已有的运算符多重含义,使得同一运算符面对不同类型的数据产生不同的效果。

格式如下:

返回值 opetator 运算符 (形参表)
{
    ...
}

包含被重载的运算符的表达式会被编译为对运算符函数的调用,运算符的操作数称为函数的实参,运算的结果就是函数的返回值。运算符可以多次重载。

运算符可以被重载为全局函数,也可以重载为成员函数。一般建议重载为成员函数,这样可以更好的体现运算符和类的关系。下面是一个例子:

  • 代码:

    #include <iostream>
    #include <cstring>
    using namespace std;
    
    class Suqare{
    public:
        int _w;
        int _l;
    public:
        Suqare(){}
        Suqare(int w, int l): _w(w), _l(l){}
        ~Suqare(){}
        // 成员函数操作符重载
        Suqare operator + (const Suqare &s);
    };
    Suqare Suqare::operator+(const Suqare &s){
        return Suqare(this->_w + s._w, this->_l + s._l);
    }
    // 定义一个全局操作符重载函数
    Suqare operator - (const Suqare &s1, const Suqare &s2){
        return Suqare(s1._w - s2._w, s1._l - s2._l);
    }
    int main(void){
        Suqare s1(1,2), s2(2,4), s3;
        s3 = s1 + s2;
        cout << s3._w << "," s3._l << << endl;
        s3 = s2 - s1;
        cout << s3._w << "," s3._l << << endl;
        return 0;
    }
    
  • 输出:

    3,6
    1,2
    
  • 结论:从上例可以看出,如果是成员函数进行操作符重载,那么形参数量可以减一,因为可以通过this来获取当前对象本身的属性。

2. 赋值运算符重载

可以对赋值运算符=进行重载,从而对对象进行快速赋值。c++中规定,“=”只能重载为成员函数

EG:

  • 代码:实现字符串的拷贝功能

    #include <iostream>
    #include <cstring>
    using namespace std;
    class String {
    private:
        char * str;
    public:
        String() :str(NULL) { }
        // 最后一个const表示这是一个常函数,避免对变量进行修改。第一个const表示返回的字符串只读,也不许外界修改。
        const char * c_str() const { return str; };
        String & operator = (const char * s);
        ~String();
    };
    String & String::operator = (const char * s)
    //重载"="以使得 obj = "hello"能够成立
    {
        if (str)
            delete[] str;
        if (s) { //s不为NULL才会执行拷贝
            str = new char[strlen(s) + 1];
            strcpy(str, s);
        }
        else
            str = NULL;
        return *this;
    }
    String::~String()
    {
        if (str)
            delete[] str;
    };
    int main()
    {
        String s;
        s = "Good Luck,"; //等价于 s.operator=("Good Luck,");
        cout << s.c_str() << endl;
        // String s2 = "hello!"; //这条语句要是不注释掉就会出错
        s = "Shenzhou 8!"; //等价于 s.operator=("Shenzhou 8!");
        cout << s.c_str() << endl;
        return 0;
    }
    
  • 输出:

    Good Luck,
    Shenzhou 8!
    

3. 深拷贝和浅拷贝

浅拷贝:

当我们使用=,通过一个已经初始化好的对象对另一个对象进行赋值时,对于基础的数据类型,将会采用复制的操作。而对于如数组、指针等指向一片空间的区域,采用的是复制地址而不是复制地址处的内容,这就是浅拷贝。显然,当涉及到内存操作时,浅拷贝可能会出现注入两个指针指向同一空间的情况,这会导致在销毁对象时对同一空间的多次释放,从而引发错误。

深拷贝:

通过重载=赋值操作符,我们可以对默认的赋值方式进行修改,从而实现在对指针类变量赋值时额外再开辟一片内存,并将内容拷贝过去,而不是只拷贝地址。这样就能避免重复释放空间的错误。

4. 运算符重载为友元函数

有时候我们不想对成员函数进行运算符重载,全局函数又无法直接访问类的非共有成员,这时就可以使用友元函数。

EG:

  • 代码:

    #include <iostream>
    using namespace std;
    
    class Complex{
    private:
    	double real, imag;
    public:
    	Complex(double r = 0.0, double i = 0.0): real(r), imag(i) { }
    	friend Complex operator+(Complex& a, Complex& b) {
    		Complex temp;
    		temp.real = a.real + b.real;
    		temp.imag = a.imag + b.imag;
    		return temp;
    	}
    	void display() {
    		cout << real;
    		if (imag > 0) cout << "+";
    		if (imag != 0) cout << imag << "i" << endl;
    	}
    };
    
    int main()
    {
    	Complex a(2.3, 4.6), b(3.6, 2.8), c;
    	a.display();
    	b.display();
    	c = a + b;
    	c.display();
    	c = operator+(a, b);
    	c.display();
    
    	return 0;
    }
    
  • 输出:

    2.3+4.6i
    3.6+2.8i
    5.9+7.4i
    5.9+7.4i
    

5. 运算符重载实现可变长数组

在C++中,我们可以通过通过运算符重载实现一个可变长数组类。它拥有以下特点:

  • 初始化对象是指定数组的元素数量。
  • 可以动态添加数组。
  • 自动完成内存的分配和动态释放问题。
  • 可以正常访问下标。

代码如下:

#include <iostream>
#include <cstring>
using namespace std;
class CArray
{
	int size; //数组元素的个数
	int* ptr; //指向动态分配的数组
public:
	CArray(int s = 0); //s代表数组元素的个数
	CArray(CArray & a);
	~CArray();
	void push_back(int v); //用于在数组尾部添加一个元素 v
	CArray & operator = (const CArray & a); //用于数组对象间的赋值
	int length() const { return size; } //返回数组元素个数
	int & operator[](int i)
	{ //用以支持根据下标访问数组元素,如“a[i]=4;”和“n=a[i];”这样的语句
		return ptr[i];
	};
};
CArray::CArray(int s) : size(s)
{
	if (s == 0)
		ptr = NULL;
	else
		ptr = new int[s];
}
CArray::CArray(CArray & a)
{
	if (!a.ptr) {
		ptr = NULL;
		size = 0;
		return;
	}
	ptr = new int[a.size];
	memcpy(ptr, a.ptr, sizeof(int) * a.size);
	size = a.size;
}
CArray::~CArray()
{
	if (ptr) delete[] ptr;
}
CArray & CArray::operator=(const CArray & a)
{ //赋值号的作用是使 = 左边对象中存放的数组的大小和内容都与右边的对象一样
	if (ptr == a.ptr) //防止 a=a 这样的赋值导致出错
		return *this;
	if (a.ptr == NULL) { //如果a里面的数组是空的
		if (ptr)
		delete[] ptr;
		ptr = NULL;
		size = 0;
		return *this;
	}
	if (size < a.size) { //如果原有空间够大,就不用分配新的空间
		if (ptr)
			delete[] ptr;
		ptr = new int[a.size];
	}
	memcpy(ptr, a.ptr, sizeof(int)*a.size);
	size = a.size;
	return *this;
}
void CArray::push_back(int v)
{ //在数组尾部添加一个元素
	if (ptr) {
		int* tmpPtr = new int[size + 1]; //重新分配空间
		memcpy(tmpPtr, ptr, sizeof(int) * size); //复制原数组内容
		delete[] ptr;
		ptr = tmpPtr;
	}
	else //数组本来是空的
		ptr = new int[1];
	ptr[size++] = v; //加入新的数组元素
}
int main()
{
	CArray a; //开始的数组是空的
	for (int i = 0; i<5; ++i)
		a.push_back(i);
	CArray a2, a3;
	a2 = a;
	for (int i = 0; i<a.length(); ++i)
		cout << a2[i] << " ";
	a2 = a3; //a2 是空的
	for (int i = 0; i<a2.length(); ++i) //a2.length()返回 0
		cout << a2[i] << " ";
	cout << endl;
	a[3] = 100;
	CArray a4(a);
	for (int i = 0; i<a4.length(); ++i)
		cout << a4[i] << " ";
	return 0;
}

输出:

0 1 2 3 4
0 1 2 100 4

其中,[]是双目运算符,a[i]等价于a.operator[](i)

6. C++重载输入输出运算符

6.1 输出<<

在C++中,左移运算符<<被和cout一起用作输出,左移运算符本身并没有这种效果,之所以能做到这一步,是因为它被重载了。

coutostream的对象,它们都是在头文件<iostream>中被声明的。ostream将<<进行了重载,并且为了适应多种类型的输出,将其重载了多次,例如:

  • 输出字符串:

    ostream & ostream::operator << (const char *s)
    {
        // 输出s的代码
        return *this;
    }
    
  • 输出整形:

    ostream & ostream::operator << (int n)
    {
    	// 输出整形n的代码
    	return *this;
    }
    

重载函数的返回类型是ostream的引用,依旧是ostream类型,因此我们可以链式调用重载函数,例如:

cout << "this is a string" << 5;
// 等价于
(cout.operator<<("this is a string")).operator<<(5);

6.2 输入>>

和输出类似,右移运算符>>istream类中被多次重载,并通过istream的对象cin进行调用,这些也都是定义在头文件<iostream>中的。一个重载案例是:

istream & istream::operator << (const char *s)
{
    // 输入s的代码
    return *this;
}

6.3 案例

问题:

现在有一个Complex类,假定 c 是 Complex 复数类的对象,现在希望写cout<<c;就能以 a+bi 的形式输出 c 的值;写cin>>c;就能从键盘接受 a+bi 形式的输入,并且使得 c.real = a, c.imag = b。

分析:

显然,需要对ostream和istream进行重载,但是这两个类在标准头文件中,无法修改,因此只能使用全局函数来进行运算符重载。此外,如果使用全局函数,想要访问到Complex的数据成员,需要将其声明为友元函数。

代码:

#include <iostream>
#include <string>
#include <cstdlib>
using namespace std;
class Complex
{
	double real,imag;
public:
	Complex( double r=0, double i=0):real(r),imag(i){ };
	friend ostream & operator<<( ostream & os,const Complex & c);
	friend istream & operator>>( istream & is,Complex & c);
};
ostream & operator<<( ostream & os,const Complex & c)
{
	os << c.real << "+" << c.imag << "i"; //以"a+bi"的形式输出
	return os;
}
istream & operator>>( istream & is,Complex & c)
{
	string s;
	is >> s; //将"a+bi"作为字符串读入, "a+bi" 中间不能有空格
	int pos = s.find("+",0);
	string sTmp = s.substr(0,pos); //分离出代表实部的字符串
	c.real = atof(sTmp.c_str());//atof库函数能将const char*指针指向的内容转换成 float
	sTmp = s.substr(pos+1, s.length()-pos-2); //分离出代表虚部的字符串
	c.imag = atof(sTmp.c_str());
	return is;
}
int main()
{
	Complex c;
	int n;
	cin >> c >> n;
	cout << c << "," << n;
	return 0;
}

输出:

13.2+133i 87
13.2+133i,87

7. 强制类型转换重载

在C++中,类的名字本身也是一种运算符,也就是类型强制转换运算符。需要注意的是:

  • 类型强制运算符是单目运算符,只能被重载为成员函数,不能重载为全局函数。

EG:

  • 代码:

    #include <iostream>
    using namespace std;
    class Complex
    {
        double real, imag;
    public:
        Complex(double r = 0, double i = 0) :real(r), imag(i) {};
        operator double() { return real; } //重载强制类型转换运算符 double
    };
    int main()
    {
        Complex c(1.2, 3.4);
        cout << (double)c << endl; //输出 1.2
        double n = 2 + c; //等价于 double n = 2 + c. operator double()
        cout << n; //输出 3.2
    }
    
  • 输出:

    1.2
    3.2
    

8. C++重载++和--运算符

自增运算符++、自减运算符--都可以被重载,但是它们有前置、后置之分。

++为例,假设 obj 是一个 CDemo 类的对象,++objobj++本应该是不一样的,前者的返回值应该是 obj 被修改后的值,而后者的返回值应该是 obj 被修改前的值。如果如下重载++运算符:、

CDemo & CDemo::operator ++ ()
{
    //...
    return * this;
}

那么不论obj++还是++obj,都等价于obj.operator++()无法体现出差别。

为了解决这个问题,C++ 规定,在重载++或--时,允许写一个增加了无用 int 类型形参的版本,编译器处理++或--前置的表达式时,调用参数个数正常的重载函数;处理后置表达式时,调用多出一个参数的重载函数。来看下面的例子:

#include <iostream>
using namespace std;
class CDemo {
private:
	int n;
public:
	CDemo(int i=0):n(i) { }
	CDemo & operator++(); //用于前置形式
	CDemo operator++( int ); //用于后置形式
	operator int ( ) { return n; }
	friend CDemo & operator--(CDemo & );
	friend CDemo operator--(CDemo & ,int);
};
CDemo & CDemo::operator++()
{//前置 ++
	n ++;
	return * this;
}
CDemo CDemo::operator++(int k )
{ //后置 ++
	CDemo tmp(*this); //记录修改前的对象
	n++;
	return tmp; //返回修改前的对象
}
CDemo & operator--(CDemo & d)
{//前置--
	d.n--;
	return d;
}
CDemo operator--(CDemo & d,int)
{//后置--
	CDemo tmp(d);
	d.n --;
	return tmp;
}
int main()
{
	CDemo d(5);
	cout << (d++ ) << ","; //等价于 d.operator++(0);
	cout << d << ",";
	cout << (++d) << ","; //等价于 d.operator++();
	cout << d << endl;
	cout << (d-- ) << ","; //等价于 operator-(d,0);
	cout << d << ",";
	cout << (--d) << ","; //等价于 operator-(d);
	cout << d << endl;
	return 0;
}

输出:

5,6,7,7
7,6,5,5