【C++】5、运算符重载

一、运算符重载是什么

重载这个概念我们并不陌生,在刚刚接触C++的时候就碰见了,随后就一直在用,但那都是对于函数的,我们通过一个函数名便可以用来写出多个功能的函数,这无疑是C++相较于C语言的一个很大的亮点,但C++的强大远不止如此,就比如按照C的逻辑,你就绝对想不到就连运算符都有重载这一说。

跟函数的重载一样,运算符重载就是对已有的运算符添加新含义,但要注意,运算符是一定的,我们不能去制造一个运算符

事实上,我们已经见过了两个运算符的重载了,仔细想想我们的输入输出,是不是用到了 << >> 这两个,在C语言中我们已经知道,他们分别代表左移和右移,当然,在C++中他们依然代表左右移动,但此时已经不仅限于此了,我们还用他们来配合输入输出。

二、运算符重载怎么写

我们虽然把运算符重载单独列了出来,但不可否认的是,他本身就是一种函数重载,这取决于其实现方式

#include<iostream>

using namespace std;

class A
{
private:
    int x, y;
public:
    A(int x1 = 0, int y1 = 0): x(x1), y(y1) {}
    ~A() {}

    A operator +(A a)
    {
        return A(x + a.x, y + a.y);
    }

    void show()
    {
        cout << "x = " << x << endl;
        cout << "y = " << y << endl;
    }
};

int main()
{
    A a1(1, 1), a2(2, 3);
    A b = a1 + a2;

    b.show();

    return 0;
}


先看看所谓的运算符重载,在本例程中其实就是这一个函数:

    A operator +(A a)
    {
        return A(x + a.x, y + a.y);
    }

看看眼熟不眼熟,难道是构造函数的亲兄弟?

可惜并不是,开头的 A 代表的是返回值类型,表示的是类名,operation 是一个专门用来表示运算符重载的关键字,然后更靠右的那个加号,当然是我们要重载的运算符了,最后就是小括号大括号了,妥妥的参数列表和函数体啊,这分明就是一个纯纯的函数啊,所以说运算符重载本来就是属于函数重载。

按照这个逻辑,那个加号其实就完全可以理解为函数名了哈哈哈哈,没毛病但没有意义,我们需要记住,这里的成员函数应该是 operator+()

这里顺便做一个总结,对于运算符重载的格式:

返回类型 operator 运算符(参数列表)
{
    函数体;
}

最后,就是如何去理解这种写法了,由于知识的局限,这里直接引用一个博客的解释(原博客404了,放不了链接,):

在A类中重载了运算符+,该重载只对A类对象有效。

执行b=a1+a2;语句时,加号优先级大于赋值,先执行加法,编译器检测到+号左边是一个A类对象(+号具有左结合性,所以 先检测左边),就会调用成员函数operator+(),也就是转换为下面的形式:

b=a1.operator+(a2); //a1是要调用函数的对象,a2是函数的实参

可见:

重载运算符并没有改变其原来的功能,只是增加了对自定义数据类型的运算功能。

三、运算符重载的特点

  • 重载运算符并没有改变原来的功能,只是增加了对自定义数据类型的运算功能
  • 在重载过程中,不能改变运算符的优先级和操作参数的个数,以及结合性也不能改变
  • 只能在已有的运算符上重载
  • 不能重载的符号:空间所属符 ::,计算大小的单目运算符 sizeof(),三目运算符 `?:
  • 涉及到输入和输出的,只能通过友元函数进行

四、可以重载的运算符

1、复习一下优先级

2、可以重载的运算符

运算符种类 符号 符号 符号 符号 符号 符号 符号 符号 符号
算术运算符 + - * / % ++ --
位移运算符 << >> & | ! ~ ^
逻辑运算符 && || !
关系运算符 > < <= >= !=
赋值运算符 = += -= *= /= |= ^= <<= >>=
其他运算符 [] () -> new delete new[] delete[]

五、运算符重载实现

1、单目运算符 ++

  • 单目双目三目指的是需要的变量个数
  • 有些人对单目运算符的重载陷入了一个误区,就是只以为实现一个加一操作即可,顶多还在括号中加一个int来区分前置还是后置,但是我们其实很早就知道,前置加加和后置加加是存在先加后用还是先用后加的区别的。
  • 前置++:b = a1.operator++(); // a1是要调用函数的对象,operator++() 是成员函数的名字
  • 后置++:b = a1.operator++(0); // a1 是要调用函数的对象,operator++(0) 是成员函数的名字,这个额外的参数没有实际的作用,只是用于区分两种递增形式,很多人在此处直接写一个int。

#include <iostream>

using namespace std;

class A {
    int x;

public:
    A(int x) : x(x) {}

    // 前置递增运算符重载
    A& operator++() {
        ++x;
        return *this;
    }

    // 后置递增运算符重载
    A operator++(int) {
        A tmp(*this);
        ++(*this);
        return tmp;
    }

    void show() {
        cout << x << endl;
    }
};

int main() {
    A a(5);
    A a1(5);
    A b = ++a;
    A c = a1++;

    b.show(); // 输出:6
    c.show(); // 输出:5

    a.show(); // 输出:6
    a1.show(); // 输出:6

    return 0;
}

在前置递增运算符的重载函数中,我们首先对对象自身进行了递增操作 ++x,然后返回了递增后的对象的引用 *this。这样,当我们使用前置递增运算符时,可以直接在原对象上进行递增,并且能够连续进行多次递增操作。

在后置递增运算符的重载函数中,我们首先创建了一个副本 tmp,并将当前对象的值赋给副本。然后,我们对对象自身进行递增操作 ++(*this)。最后,返回保存了递增前值的副本 tmp。这样,当我们使用后置递增运算符时,可以先返回递增前的对象的副本,然后再对原对象进行递增。

在主函数中的代码示例中,我们分别使用了前置递增运算符和后置递增运算符,并将递增后的对象赋值给了不同的变量。通过输出这些变量的值,我们可以观察到前置递增返回的是递增后的结果,而后置递增返回的是递增前的结果。


#include<iostream>

using namespace std;

class A
{
private:
    int x, y;
public:
    A(int x1 = 0, int y1 = 0): x(x1), y(y1) {}
    ~A() {}

    //单目运算符 ++ 前置形式
    A operator ++()
    {
        this->x += 1;
        this->y += 1;

        return *this; // 返回本身
    }

    //单目运算符 ++ 后置形式
    A operator++ (int)
    {
        A old = *this; // 利用默认拷贝构造函数赋值=
        this->x += 1;
        this->y += 1;

         return old;
    }

    void show()
    {
        cout <<"(" << "x = " << x << ", ";
        cout << "y = " << y << ")" << endl;
    }
};

int main()
{
    A a(1, 1);
    A a1(1, 1);
    A b = ++a;
    A b1 = a1++;

    b.show();
    b1.show();

    a.show();
    a1.show();
    return 0;
}

虽然实现出来了加与用的顺序问题,但如何实现的我个人还是没有理解开,很是遗憾,只能以后回看了

2、双目运算符 +

这里的内容参考了博客:https://www.weixueyuan.net/view/6379.html

(1)、类中运算符重载

    A operator +(A a)
    {
        return A(x + a.x, y + a.y);
    }

这是上面那个例程的运算符重载部分,应该是属于最简单的写法了,直接利用了构造函数来创建一个新的A类对象,写起来很是方便。

讲师的方法麻烦的多,竟然是先设一个对象,然后运算后返出来,虽然麻烦,但有时候还是有用的,但太过于原始,中间可以间杂引用跟this指针,用来当例程不错,实际运用可能直接返回一个新构造就完全OK


接下来看一个难一点的例程:

#include <iostream>
using namespace std;

class complex
{
public:
    // 构造
    complex();
    complex(double a);
    complex(double a, double b);
    
    // 运算符重载
    complex operator+(const complex & A)const;
    complex operator-(const complex & A)const;
    complex operator*(const complex & A)const;
    complex operator/(const complex & A)const;
    
    // 打印函数的声明
    void display()const;
    
private:
    double real;   //复数的实部
    double imag;   //复数的虚部
};

complex::complex()
{
    real = 0.0;
    imag = 0.0;
}

complex::complex(double a)
{
    real = a;
    imag = 0.0;
}

complex::complex(double a, double b)
{
    real = a;
    imag = b;
}

//打印复数
void complex::display()const
{
    cout<<real<<" + "<<imag<<" i ";
}

//重载加法操作符
complex complex::operator+(const complex& A)const
{
   return complex(A.real + real, A.imag + imag);
}

//重载减法操作符
complex complex::operator-(const complex & A)const
{
    complex B;
    B.real = real - A.real;
    B.imag = imag - A.imag;
    return B;
}

//重载乘法操作符
complex complex::operator*(const complex & A)const
{
    complex B;
    B.real = real * A.real - imag * A.imag;
    B.imag = imag * A.real + real * A.imag;
    return B;
}

//重载除法操作符
complex complex::operator/(const complex & A)const
{
    complex B;
    double square = A.real * A.real + A.imag * A.imag;
    B.real = (real * A.real + imag * A.imag)/square;
    B.imag = (imag * A.real - real * A.imag)/square;
    return B;
}

int main()
{
    complex c1(4.3, -5.8);
    complex c2(8.4, 6.7);
    complex c3;
   
    //复数的加法
    c3 = c1 + c2;
    cout<<"c1 + c2 = ";
    c3.display();
    cout<<endl;
   
    //复数的减法
    c3 = c1 - c2;
    cout<<"c1 - c2 = ";
    c3.display();
    cout<<endl;
   
    //复数的乘法
    c3 = c1 * c2;
    cout<<"c1 * c2 = ";
    c3.display();
    cout<<endl;
   
    //复数的除法
    c3 = c1 / c2;
    cout<<"c1 / c2 = ";
    c3.display();
    cout<<endl;
   
    return 0;
}

其实这里的程序也不算难,顶多就是运算符重载的数量多,我真正想说的是这里的两个const:
complex operator+(const complex & A)const;

参数列表中的那个const是为了不对原来的对象修改,另外这里用引用来避免了对实参的拷贝,提高了运行的效率。

而函数加上const的后缀,则是表示此函数不修改类成员变量(即当前this指针指向对象的成员变量),如果在函数里修改了则编译报错。

(2)、顶层函数中的写法(类外直接重载)

博客中还提到了顶层函数这个说法,我有些不明白,去查了一下后发现原来指的就是普通函数,但这其实也引出了一个问题,我们可以看到,刚刚的重载都是发生在类中的,那么若是在顶层函数中重载运算符该如何操作呢?

博客https://www.weixueyuan.net/view/6381.html中给出了解决的办法:

学习将操作符重载函数声明为类成员函数时,我们不断强调二元操作符的函数参数为一个,一元操作符重载函数不需要函数参数。如果以顶层函数的形式重载操作符时,二元操作符重载函数必须有两个参数,一元操作符重载必须有一个参数。

将操作符重载函数声明为顶层函数时,必须至少有一个类对象参数,否则编译器无法区分操作符是系统内建的还是程序设计人员自己定义的,有了一个类对象参数之后,系统则会根据情况调用内建或自定的操作符。

顺便贴出博客中的例程:

#include <iostream>
using namespace std;

class complex
{
public:
    complex();
    complex(double a);
    complex(double a, double b);
    
    double getreal() const { return real; }
    double getimag() const { return imag; }
    void setreal(double a){ real = a; }
    void setimag(double b){ imag = b; }
    void display()const;
    
private:
    double real;   //复数的实部
    double imag;   //复数的虚部
};

complex::complex()
{
    real = 0.0;
    imag = 0.0;
}

complex::complex(double a)
{
    real = a;
    imag = 0.0;
}

complex::complex(double a, double b)
{
    real = a;
    imag = b;
}

//打印复数
void complex::display()const
{
    cout<<real<<" + "<<imag<<" i ";
}

//重载加法操作符
complex operator+(const complex & A, const complex &B)
{
    complex C;
    C.setreal(A.getreal() + B.getreal());
    C.setimag(A.getimag() + B.getimag());
    return C;
}

//重载减法操作符
complex operator-(const complex & A, const complex &B)
{
    complex C;
    C.setreal(A.getreal() - B.getreal());
    C.setimag(A.getimag() - B.getimag());
    return C;
}

//重载乘法操作符
complex operator*(const complex & A, const complex &B)
{
    complex C;
    C.setreal(A.getreal() * B.getreal() - A.getimag() * B.getimag() );
    C.setimag(A.getimag() * B.getreal() + A.getreal() * B.getimag() );
    return C;
}

//重载除法操作符
complex operator/(const complex & A, const complex & B)
{
    complex C;
    double square = A.getreal() * A.getreal() + A.getimag() * A.getimag();
    C.setreal((A.getreal() * B.getreal() + A.getimag() * B.getimag())/square);
    C.setimag((A.getimag() * B.getreal() - A.getreal() * B.getimag())/square);
    return C;
}

int main()
{
    complex c1(4.3, -5.8);
    complex c2(8.4, 6.7);
    complex c3;
   
    c3 = c1 + c2;
    cout<<"c1 + c2 = ";
    c3.display();
    cout<<endl;

    c3 = c1 - c2;
    cout<<"c1 - c2 = ";
    c3.display();
    cout<<endl;

    c3 = c1 * c2;
    cout<<"c1 * c2 = ";
    c3.display();
    cout<<endl;

    c3 = c1 / c2;
    cout<<"c1 / c2 = ";
    c3.display();
    cout<<endl;

    return 0;
}


上面的例程是对于双目运算符的,在这里也给一个单目的例程:

#include <iostream>

using namespace std;

class A
{
    int x, y;
public:
    A() {}
    A(int x, int y): x(x), y(y) {}
    ~A() {}

    int getX()const
    {
        return x;
    }
    int getY()const
    {
        return y;
    }
    void setX(int x1)
    {
        x = x1;
    }
    void setY(int y1)
    {
        y = y1;
    }

    void display()
    {
        cout << "x: " << x << endl;
        cout << "y: " << y << endl;
    }
};

A operator++(const A& a1)
{
    A b;

    b.setX(a1.getX() + 1);
    b.setY(a1.getY() + 1);

    return b;
}

int main()
{
    A a1(1, 1);
    A b = ++a1;

    b.display();

    return 0;
}

(3)、类中声明类外实现的

介绍完顶层函数的,我发现博客中还提到了一个类中声明类外实现的,这里也写一个:

#include <iostream>

using namespace std;

class A
{
    int x, y;
public:
    A() {}
    A(int x, int y): x(x), y(y) {}
    ~A() {}

    A operator++()const;

    void display()
    {
        cout << "x: " << x << endl;
        cout << "y: " << y << endl;
    }
};

A A::operator++()const
{
    A b = *this;

    b.x++;
    b.y++;

    return  b;
}

int main()
{
    A a1(1, 1);
    A b = ++a1;

    b.display();

    return 0;
}

(4)、介绍一种新的调用运算符重载函数的方法

#include <iostream>

using namespace std;

class A
{
    int x, y;
public:
    A() {}
    A(int x, int y): x(x), y(y) {}
    ~A() {}

    int getX()const
    {
        return x;
    }
    int getY()const
    {
        return y;
    }
    void setX(int x1)
    {
        x = x1;
    }
    void setY(int y1)
    {
        y = y1;
    }

    void display()
    {
        cout << "x: " << x << endl;
        cout << "y: " << y << endl;
    }
};

A operator+(const A& a1, const A& a2)
{
    A b;

    b.setX(a1.getX() + a2.getX());
    b.setY(a1.getY() + a2.getY());

    return b;
}

int main()
{
    A a1(1, 1), a2(2, 2);

    A b1 = a1 + a2;
    b1.display();
    cout << "-------\n";

    A b2 = operator+(a1, a2);
    b2.display();

    return 0;
}

这就是我们上面提到的另一种写法了,这样可以调用其实充分表达了运算符重载在本质上就是一种函数重载。

可以看见,这种方法其实就是成员函数调用的方法,显然,相较于直接进行加法,这种方法不太直观,但是指针操作符“->”、下标操作符“[]”、函数调用操作符“()”和赋值操作符“=”只能以成员函数的形式进行操作符重载。


利用ChatGPT对上面提到的四个操作符进行了重载,综合在了一个例程中,暂时没看大懂,以后回看:

#include <iostream>
using namespace std;

class MyClass {
    int data[5];

public:
    MyClass() {
        for (int i = 0; i < 5; i++) {
            data[i] = i;
        }
    }

    // 重载下标操作符 []
    int& operator[](int index) {
        return data[index];
    }

    // 重载函数调用操作符 ()
    void operator()(int value) {
        for (int i = 0; i < 5; i++) {
            data[i] = value;
        }
    }

    // 重载赋值操作符 =
    MyClass& operator=(const MyClass& other) {
        if (this != &other) {
            for (int i = 0; i < 5; i++) {
                data[i] = other.data[i];
            }
        }
        return *this;
    }

    void display() {
        for (int i = 0; i < 5; i++) {
            cout << data[i] << " ";
        }
        cout << endl;
    }
};

int main() {
    MyClass obj1;
    MyClass obj2;

    // 使用下标操作符访问数组元素
    cout << "obj1[3]: " << obj1[3] << endl;

    // 使用函数调用操作符设置所有数组元素的值
    obj1(5);

    // 使用赋值操作符将 obj1 的值赋给 obj2
    obj2 = obj1;

    // 显示 obj1 和 obj2 的数组元素
    cout << "obj1: ";
    obj1.display();

    cout << "obj2: ";
    obj2.display();

    return 0;
}

(5)、调用中带有数字

#include <iostream>

using namespace std;

class A
{
    int x, y;
public:
    A() {}
    A(int x): x(x) {}
    A(int x, int y): x(x), y(y) {}
    ~A() {}

    int getX()const
    {
        return x;
    }
    int getY()const
    {
        return y;
    }
    void setX(int x1)
    {
        x = x1;
    }
    void setY(int y1)
    {
        y = y1;
    }

    void display()
    {
        cout << "x: " << x << endl;
        cout << "y: " << y << endl;
    }
};

A operator+(const A& a1, const A& a2)
{
    A b;

    b.setX(a1.getX() + a2.getX());
    b.setY(a1.getY() + a2.getY());

    return b;
}

int main()
{
    A a1(1, 1), a2(2, 2);

    A b1 = a1 + a2;
    b1.display();
    cout << "-------\n";

    A b2 = operator+(a1, a2);
    b2.display();
    cout << "-------\n";

    b2 = b1 + 3;
    b2 = 3 + b2;
    b2.display();
    cout << "-------\n";

    return 0;
}

可能一样看上去不知道发生了啥,首先看一下b2 = b1 + 3;b2 = 3 + b2; 这两行代码,奇不奇怪,对象竟然能跟数值相加!这说明什么呢?只有一种可能,那就是这个3在这里本来就被当成对象了。

真的是这样吗?还真就是了。我们在这里对其的解释是:b2 = b1.operator+(3); b2 = b2.operator+(3); 这个3带入到operator+()函数中,当然就成了一个对象。

为什么能这样认为呢?我们看一下程序开头,有一个构造:A(int x): x(x) {},所以这里的单个数值被当成数值其实可以的,系统因为这里有构造在形式上给他做支撑,并没有报错,但一旦删掉这个构造:

这里可能有人就要耍耍小聪明了,既然我可以构造定义x,那再构造一个y,不是就能完整了吗?

我就是这个大聪明,单独试了试y,这样与单独的x构造其实没半点区别,当然可以。

但是若是x和y同时构造了,这就此时的构造就有问题了,因为系统认为此时的构造是相同的,至于x和y本身的区别,这算个卵,构造看的是形参的类型,所以说,此时系统不出意外的不知道选哪一个,报错:


这里还是有一种角度的,数字跟对象能相加,是因为我这里有一个重载作为支撑的:A operator+(const A& a1, const A& a2),其实这里可以看到,其实也是将数字当成了对象看待。


结合两种角度,我改变角度1的例程来进行验证:

#include <iostream>

using namespace std;

class A
{
    int x, y;
public:
    A() {}
    A(int x): x(x) {}
    A(int x, int y): x(x), y(y) {}
    ~A() {}

    int getX()const
    {
        return x;
    }
    int getY()const
    {
        return y;
    }
    void setX(int x1)
    {
        x = x1;
    }
    void setY(int y1)
    {
        y = y1;
    }

    void display()
    {
        cout << "x: " << x << endl;
        cout << "y: " << y << endl;
    }

    A operator+(const A& a1)
    {
        A b;

        b.setX(a1.getX() + this->x);
        b.setY(a1.getY() + this->y);

        return b;
    }
};

// A operator+(const A& a1, const A& a2)
// {
//     A b;

//     b.setX(a1.getX() + a2.getX());
//     b.setY(a1.getY() + a2.getY());

//     return b;
// }

int main()
{
    A a1(1, 1), a2(2, 2);

    A b1 = a1 + a2;
    b1.display();
    cout << "-------\n";

    A b2 = a1.operator+(a2);
    b2.display();
    cout << "-------\n";

    b2 = b1 + 3;
    b2 = (A)3 + b2;
    b2.display();
    cout << "-------\n";

    return 0;
}

由于我将顶层函数中的运算符重载换到了类中重载,直接把角度1从事实的角度上干掉了,角度2是正确的无疑,但我们也看到了修改后的例程中其实我还做了一次强转:b2 = (A)3 + b2;,这个说明了角度1并不是一无是处,角度1有一点还是正确的,那就是3在此处确实是作为一个对象出现的。

(6)、友元函数在运算符重载中的运用

在上面的例子中,我们以顶层函数的形式取进行操作符重载的时候其实是遇到了障碍的,就是无法直接访问A类中的私有变量x和y,于是不得不在public中定义了setX,setY,getX,getY这四个函数来调用x和y,这样一来就使得操作符重载有些臃肿,还不如直接将x和y直接设成公有,但既然我们把x和y设成私有就是为了不让别人随意用,难道就没有一种更好的方法以逃脱这个两难?

当然是有的,这个问题的本质就是我们想要用私有变量,现在我们没关系自然拿不到,但如果我们成为了朋友,那你的东西除了花呗不都是我的了吗!

没错,我们要用的方法就是友元函数。


简单回忆一下用法,将函数声明放在一个类里面,函数本体就能使用该类中的资源。

#include <iostream>
using namespace std;

class complex
{
public:
    complex();
    complex(double a);
    complex(double a, double b);

    friend complex operator+(const complex& A, const complex& B);
    friend complex operator-(const complex& A, const complex& B);
    friend complex operator*(const complex& A, const complex& B);
    friend complex operator/(const complex& A, const complex& B);

    void display()const;

private:
    double real;   //复数的实部
    double imag;   //复数的虚部
};

complex::complex()
{
    real = 0.0;
    imag = 0.0;
}

complex::complex(double a)
{
    real = a;
    imag = 0.0;
}

complex::complex(double a, double b)
{
    real = a;
    imag = b;
}

//打印复数
void complex::display()const
{
    cout << real << " + " << imag << " i ";
}

//重载加法操作符
complex operator+(const complex& A, const complex& B)
{
    complex C;
    C.real = A.real + B.real;
    C.imag = A.imag + B.imag;
    return C;
}

//重载减法操作符
complex operator-(const complex& A, const complex& B)
{
    complex C;
    C.real = A.real - B.real;
    C.imag = A.imag - B.imag;
    return C;
}

//重载乘法操作符
complex operator*(const complex& A, const complex& B)
{
    complex C;
    C.real = A.real * B.real - A.imag * B.imag;
    C.imag = A.imag * B.real + A.real * B.imag;
    return C;
}

//重载除法操作符
complex operator/(const complex& A, const complex& B)
{
    complex C;
    double square = A.real * A.real + A.imag * A.imag;
    C.real = (A.real * B.real + A.imag * B.imag) / square;
    C.imag = (A.imag * B.real - A.real * B.imag) / square;
    return C;
}

int main()
{
    complex c1(4.3, -5.8);
    complex c2(8.4, 6.7);
    complex c3;

    c3 = c1 + c2;
    cout << "c1 + c2 = ";
    c3.display();
    cout << endl;

    c3 = c1 - c2;
    cout << "c1 - c2 = ";
    c3.display();
    cout << endl;

    c3 = c1 * c2;
    cout << "c1 * c2 = ";
    c3.display();
    cout << endl;

    c3 = c1 / c2;
    cout << "c1 / c2 = ";
    c3.display();
    cout << endl;

    return 0;
}

3、输入输出重载

唉,讲师压根没解释,直接上格式,只好copy一下博主的了,链接:https://www.weixueyuan.net/view/6382.html

在C++中,系统已经对左移操作符 << 和右移操作符 >> 分别进行了重载,使其能够用作输入输出操作符,但是输入输出的处理对象只限于系统内建的数据类型。

系统重载这两个操作符是以系统类成员函数的形式进行的,因此 cout << var;语句可以理解为:cout.operator << (var);

如果我们自己定义了一种新的数据类型,需要用输入输出操作符去处理,则需要重载 >><<这两个操作符,我们之前重载过的几个运算符都是如此,只在有自己的作用域才是我们给他的新定义,此时遇见左右移运算符需要重载,并不应该感到奇怪。

就以本文前面的complex类为例,我们此时了解,系统重载的 >><<并不适用于complex类,所以,如果想要直接输入输出复数的话,就必须对这两个操作符进行重载。

我们可以重载输入操作符,使之读入两个double类型数据,并且将之转换为一个复数,并存入到一个复数类对象中,我们采用顶层函数的形式来实现输入操作符的重载。

istream & operator>>(istream & in, complex & A)
{
	in >> A.real >> Aimag;
	return in;
}

istream 是指输入流,这个将会在后面讲到。因为重载操作符函数需要用到complex类的私有成员变量,为了方便,我们将这个函数声明为complex类的友元关系。其声明关系如下:
friend istream & operator>>(istream & in, complex & a);

该函数可以按照如下方式使用:

complex c;
cin >> c;

有了这两个语句后,我们输入(这里用ENTER代表回车):
1.45 2.34 ENTER

这两个数据分别成为了复数类对象c的实部和虚部,而 cin >> c; 语句可理解为 operator<<(cin, c);

在重载输入操作符时,传递参数采用的是引用的方式,输入的参数里包含一个istream流的引用,返回值仍然为该引用,因此,哦们可以连续驶入多个complex类的对象:

complex c1, c2;
cin >> c1 >> c2 <<endl;

同样的,我们也可以将输出操作符进行重载,使之能够输出复数。函数在类内部的声明如下:
friend ostream & operator<<(ostream & out, complex & a);

顶层函数的实现如下:

ostream & operator<<(ostream & out, complex & a)
{
	out << a.real << "+" << a.imag << "i";
	return out;
}

与istream一样,ostream同样为了能够直接访问complex类的私有成员变量,我们将其在类中声明为complex类的友元函数。该输出操作符重载函数可以连续输出多个complex类对象。

#include <iostream>
using namespace std;

class complex
{
public:
    complex();
    complex(double a);
    complex(double a, double b);

    friend complex operator+(const complex& A, const complex& B);
    friend complex operator-(const complex& A, const complex& B);
    friend complex operator*(const complex& A, const complex& B);
    friend complex operator/(const complex& A, const complex& B);
    friend istream& operator>>(istream& in, complex& A);
    friend ostream& operator<<(ostream& out, complex& A);

    void display()const;

private:
    double real;   //复数的实部
    double imag;   //复数的虚部
};

complex::complex()
{
    real = 0.0;
    imag = 0.0;
}

complex::complex(double a)
{
    real = a;
    imag = 0.0;
}

complex::complex(double a, double b)
{
    real = a;
    imag = b;
}

//打印复数
void complex::display()const
{
    cout << real << " + " << imag << " i ";
}

//重载加法操作符
complex operator+(const complex& A, const complex& B)
{
    complex C;
    C.real = A.real + B.real;
    C.imag = A.imag + B.imag;
    return C;
}

//重载减法操作符
complex operator-(const complex& A, const complex& B)
{
    complex C;
    C.real = A.real - B.real;
    C.imag = A.imag - B.imag;
    return C;
}

//重载乘法操作符
complex operator*(const complex& A, const complex& B)
{
    complex C;
    C.real = A.real * B.real - A.imag * B.imag;
    C.imag = A.imag * B.real + A.real * B.imag;
    return C;
}

//重载除法操作符
complex operator/(const complex& A, const complex& B)
{
    complex C;
    double square = A.real * A.real + A.imag * A.imag;
    C.real = (A.real * B.real + A.imag * B.imag) / square;
    C.imag = (A.imag * B.real - A.real * B.imag) / square;
    return C;
}

//重载输入操作符
istream& operator>>(istream& in, complex& A)
{
    in >> A.real >> A.imag;
    return in;
}

//重载输出操作符
ostream& operator<<(ostream& out, complex& A)
{
    out << A.real << " + " << A.imag << " i ";
    return out;
}

int main()
{
    complex c1(4.3, -5.8);
    complex c2(8.4, 6.7);
    complex c3;

    c3 = c1 + c2;
    cout << "c1 + c2 = " << c3 << endl;

    c3 = c1 - c2;
    cout << "c1 - c2 = " << c3 << endl;

    c3 = c1 * c2;
    cout << "c1 * c2 = " << c3 << endl;

    c3 = c1 / c2;
    cout << "c1 / c2 = " << c3 << endl;

    return 0;
}

4、赋值运算符

我们知道,系统自己是有一套原本的运算符规则的,但适用的都是系统原生的数据类型,假如我们自己定义了数据类型,再去用系统原生的操作符,大概率是行不通的,所以说我们重载运算符的目的有时候不是为了改变操作符的含义,而是为了兼容性,就比如此时的赋值运算符。

当然,仅对于赋值运算符来说,在C++中的原生功能并不只是继承的C里面的,就比如我们之前的拷贝构造函数:

complex c1(4.3, -5.8);
conplex c2;
c2 = c1;
cout << c1 << endl;
cout << c2 << endl;

利用前面我们定义的complex类,我们先定义了两个complex类的对象c1和c2,c1对象通过带参构造函数初始化,然后用c1来初始化c2,最后输出这两个复数类对象。

注意在complex类中,我们并未定义拷贝构造函数,也没有重载赋值操作符,但是上面例子中 c2 = c1; 并未出现语法错误,根据程序运行结果得知,该赋值操作成功地完成了。

原因我们也是知道的,系统默认给类提供了一个拷贝构造函数和一个赋值操作符,而数据一对一的拷贝也满足我们复数类的需求了。

但系统提供的默认构造函数也有一定的缺陷,例如当类中的成员变量包含指针的时候,会导致一些意想不到的程序漏洞,此时就需要重新定义一个拷贝构造函数,在相同的情况下,系统提供的赋值操作符也无法满足我们的需求,必须要进行重载。

#include<iostream>
using namespace std;

class Array
{
public:
    Array()
    {
        length = 0;
        num = NULL;
    };
    Array(int* A, int n);
    Array(Array& a);

    Array& operator= (const Array& a);

    void setnum(int value, int index);
    int* getaddress();
    void display();
    int getlength()
    {
        return length;
    }

private:
    int length;
    int* num;
};

Array::Array(Array& a)
{
    if(a.num != NULL)
    {
        length = a.length;
        num = new int[length];
        for(int i = 0; i < length; i++)
        {
            num[i] = a.num[i];
        }
    }
    else
    {
        length = 0;
        num = 0;
    }
}

//重载赋值操作符
Array& Array::operator= (const Array& a)
{
    if(this != &a)
    {
        delete[] num;
        if(a.num != NULL)
        {
            length = a.length;
            num = new int[length];
            for(int i = 0; i < length; i++)
            {
                num[i] = a.num[i];
            }
        }
        else
        {
            length = 0;
            num = 0;
        }
    }
    return *this;
}

Array::Array(int* A, int n)
{
    num = new int[n];
    length = n;
    for(int i = 0; i < n; i++)
    {
        num[i] = A[i];
    }
}

void Array::setnum(int value, int index)
{
    if(index < length)
    {
        num[index] = value;
    }
    else
    {
        cout << "index out of range!" << endl;
    }
}

void Array::display()
{
    for(int i = 0; i < length; i++)
    {
        cout << num[i] << " ";
    }
    cout << endl;
}

int* Array::getaddress()
{
    return num;
}

int main()
{
    int A[5] = {1, 2, 3, 4, 5};
    Array arr1(A, 5);
    arr1.display();
    Array arr2(arr1);
    arr2.display();
    arr2.setnum(8, 2);
    arr1.display();
    arr2.display();
    cout << arr1.getaddress() << " " << arr2.getaddress() << endl;

    arr1 = arr2;
    arr1.display();
    arr2.display();
    arr2.setnum(9, 3);
    arr1.display();
    arr2.display();
    cout << arr1.getaddress() << " " << arr2.getaddress() << endl;
    return 0;
}

看主函数中的 arr1 = arr2; ,这个语句调用了类中的操作符重载函数,我们可以将这一语句理解为:
arr1.operator = (arr2);

该语句会执行赋值操作符重载函数中的函数体,在函数体中,我们为arr1重新开辟了一个内存空间,因此就可以规避arr1和arr2中的num指向同一块存储区域的风险,如此一来,使用系统默认提供的赋值操作符所带来的风险就可以避免了。

在这之后的语句中,我们还修改了arr2中的数据,并没有影响到arr1,可见确实将风险化解了。

当然,如果类中没有包含动态分配内存的指针成员变量时,我们使用系统提供的默认拷贝构造函数即可,无须多此一举去重新定义和重载。

5、数组下标运算符

前面已经提到,下标操作符 [] 必须要以类的成员函数的形式进行重载。其在类中的声明如下:

返回类型 & operator[](参数);

或:
const 返回类型 &operator[](参数);

如果使用第一种声明方式,操作符重载函数不仅可以访问对象,同时还可以修改对象。如果使用第二种声明方式,则操作符重载函数只能访问对象,不能修改对象。

在我们访问数组时,通过下标去访问数组中的元素并不具有检查边界溢出的功能,我们可以重载下标操作符使之具有相应的功能。

#include <iostream>

using namespace std;

class Array
{
    int len;
    int* ptr;

public:
    Array(int len = 0)
    {
        // 获取长度和空间设置
        this->len = len;
        this->ptr = new int [len];
    }

    int& operator[](int index)
    {
        return this->ptr[index];
    }
};

int main()
{
    Array array(10);

    for(int i = 0; i < 10; i++)
    {
        cin >> array[i];
    }

    for(int i = 0; i < 10; i++)
    {
        cout << array[i] << " ";
    }

    cout << endl;

    return 0;
}

6、强制类型转化符

在C++中,类型的名字(包括类的名字)本身也是一种运算符,即类型强制转换运算符。

类型强制转化运算符是单目运算符,可以被重载,但是只能被重载为成员函数,不能重载为全局变量。

经过适当重载之后,(类型名)对象 这个对对象进行强制类型转换的表达式就等价于 对象.operator 类型名(),即变成对运算符函数的调用。

#include <iostream>

using namespace std;

//定义类
class Data {
public:
    Data( int number ): number( number ) {}
    //定义强制类型转化
    operator int()
    {
        return ( int )this->number;
    }
    operator double()
    {
        return ( double )this->number;
    }
    operator long()
    {
        return ( long )this->number;
    }

private:
    int number;
};

int main()
{
    Data data( 20 );
    //int型转化
    int idata = data;
    cout << idata << endl;
    double ddata = data;
    cout << ddata << endl;
    long ldata = data;
    cout << sizeof( ldata ) << endl;
    return 0;
}

这里给一个录屏上的综合例程:

定义一个字符串类MyString,类中包含私有成员char *str;int len;两个成员,完成赋值、获取对应位数据、输入、输出、字符串的拼接

#include <iostream>
#include <cstring>

using namespace std;

class MyString
{
public:
    MyString(int len)
    {
        this->str = new char (len + 1);
        this->len = len;
    }
    MyString(const char* str = NULL)
    {
        if(str == nullptr)
        {
            this->str = new char[1];
            *(this->str) = '\0';
        }
        else
        {
            this->str = new char[strlen(str) + 1];
            strcpy(this->str, str);
        }
    }
    ~MyString()
    {
        delete [] this -> str;
    }

    MyString& operator=(const MyString& data);
    char& operator[](int index);
    MyString& operator+(MyString& data);


    friend istream& operator>>(istream& in, MyString& my);
    friend ostream& operator<<(ostream& out, MyString& my);

private:
    char* str;
    int len;
};

istream& operator>>(istream& in, MyString& my)
{
    in >> my.str;
    return in;
}

ostream& operator<<(ostream& out, MyString& my)
{
    out << my.str;
    return out;
}


// 赋值
MyString& MyString::operator=(const MyString& data)
{
    this->str = new char[strlen(data.str) + 1];
    strcpy(this->str, data.str);

    return *this;
}

// 获取对应位数据
char& MyString::operator[](int index)
{
    return this->str[index];
}

// 字符串拼接
MyString& MyString::operator+(MyString& data)
{
    strcat(this->str, data.str);
    return *this;
}

int main()
{
    MyString str("helloworld");
    MyString src("hahaha");

    cout << src << endl;

    src = str;
    cout << src << endl;

    cout << src[4];
    cout << endl;

    src + str;
    cout << src << endl;


    return 0;
}

六、异常处理

讲义一言难尽,还是参考的微学苑的博客,但是理解起来还是有些问题,可以看看:https://www.51cto.com/article/652528.html,这前半部分对异常机制的理解感觉可以看看

目前异常处理不被我放在重点上,只看了眼之前写的,遇到了再说。

1、什么是异常处理

写程序的时候遇到bug是不可避免的,但一些bug很明显,明显到一眼可以看出的拼写错误,但是也有些bug藏得很深,比如一些野指针。

我们此时研究的bug的鸡贼程度大致处于二者之间,说具体一点,用微学苑站长列举的例子,常见的异常有:用户计算触发的程序时,将除数输入为0,又例如当我们需要打开一个文件的时候却发现该文件已经被删除了……

类似的情况还是很多的,尤其是对于本人来说,所以,我们还是很希望自己编写的程序能够在异常的情况下能及时做出相应的处理,而不至于程序莫名其妙中断或者终止运行,毕竟debug也是挺难受的。

为此,在C++中,有一种机制叫做抛出异常,即一个函数能够检测出异常并且将异常返回,当异常抛出后,函数调用者直接就捕获到了该异常,并对该异常进行处理,此时的步骤叫做异常捕获。

C++中throw关键字用于抛出异常,catch关键字用于捕获异常,try关键字来尝试捕获异常。我们通常将捕获的语句放在try{}语句块中,而将异常处理语句置于catch{}语句块中。

2、异常机制的使用

抛出异常的基本语法:throw 表达式;,可以看到,抛出异常由 throw 关键字加一个表达式构成。抛出异常后需要捕获异常以及异常处理程序,其基本语法如下:

try
{
	// 可能抛出异常的语句
}
catch(异常类型1)
{
	// 异常类型1的处理程序
}
catch(异常类型2)
{
	// 异常类型2的处理程序
}
......
catch(异常类型n)
{
	// 异常类型n的处理程序
}

由try程序块捕获throw抛出的异常,然后根据异常类型运行catch程序块中的异常处理程序。catch程序块的顺序可以是任意的,不过均需要放在try程序块之后。

下面看一个简单的例子:

#include <iostream>
#include <exception>

using namespace std;

int main()
{
    char str[10];
    // 异常检测
    try
    {
        char c1 = str[20]; // 下标越界,c1未知
        cout << c1 << endl;
    }
    catch(exception& e) // 异常未抛出,无法捕捉
    {
        cout << "c1 访问越界\n";
    }


    return 0;
}


没有报错,我们跑一下:

我们发现啥都没有,为什么呢,仔细看一下代码,然后对比一下刚刚说的步骤,我们发现少了抛出异常的步骤。

但这里却是不能用throw来抛出异常:

#include <iostream>
#include <exception>

using namespace std;

int main()
{
    char str[10];
    // 异常检测
    try
    {
        char c1 = str[20]; // 下标越界,c1未知
        cout << c1 << endl;
    }
    catch(exception& e) // 异常未抛出,无法捕捉
    {
        cout << "c1 访问越界\n";
    }
    cout << "-----\n";

    char strs[10]; // 字符串类
    throw strs;
    // 异常检测
    try
    {
        char c2 = strs[20];
        cout << c2 << endl;
    }
    catch(exception& e)
    {
        cout << "c2访问越界\n";
    }

    return 0;
}

看了下录屏发现讲师用了一个字符串函数at:

查了下C++库函数表,发现at函数也是可以进行抛出异常的,于是:

#include <iostream>
#include <exception>

using namespace std;

int main()
{
    char str[10];
    // 异常检测
    try
    {
        char c1 = str[20]; // 下标越界,c1未知
        cout << c1 << endl;
    }
    catch(exception& e) // 异常未抛出,无法捕捉
    {
        cout << "c1 访问越界\n";
    }
    cout << "-----\n";

    string strs; // 字符串类
    // 异常检测
    try
    {
        char c2 = strs.at(20);
        cout << c2 << endl;
    }
    catch(exception& e)
    {
        cout << "c2访问越界\n";
    }

    return 0;
}

除了不知道为什么要用at,这个@符号到底是啥,一个都没有讲,我真的是服。

再看讲师的例程,我依旧不知道其中咋运作的,代码看得懂表面,大致知道发生了啥,但这异常到底是啥东西他倒是一句都不提。

#include <iostream>
#include <exception>

using namespace std;

class Data
{
public:
    Data() {}
    ~Data() {}
};

// 异常产生函数
void fun(int n)
{
    if(0 == n)
    {
        throw 0; // 抛出整形异常
    }
    else if(1 == n)
    {
        throw "error"; // 抛出字符串异常
    }
    else if(n > 3)
    {
        throw 1.0; // 抛出浮点异常
    }
}

int main()
{
    // 检测异常
    try
    {
        // 设置选择
        int n;
        cin >> n;

        // 检测fun函数中的异常
        fun(n);
    }
    catch(int i)  // 捕获异常
    {
        cout << "整形异常" << i << endl;
    }
    catch(const char* ptr)
    {
        cout << ptr << endl;
    }
    catch(...)
    {
        cout << "all" << endl;
    }


    return 0;
}

3、异常机制的特点

  • catch捕获异常时的的类型必须和throw抛出的异常类型一致
  • throw抛出异常可以是子类的类的对象(指针、引用),catch可以按照 父类对象的方式捕捉
posted @ 2023-07-05 15:22  fhzy  阅读(131)  评论(0)    收藏  举报