C++重温笔记(六): 运算符重载

1. 写在前面

c++在线编译工具,可快速进行实验: https://www.bejson.com/runcode/cpp920/

这段时间打算重新把c++捡起来, 实习给我的一个体会就是算法工程师是去解决实际问题的,所以呢,不能被算法或者工程局限住,应时刻提高解决问题的能力,在这个过程中,我发现cpp很重要, 正好这段时间也在接触些c++开发相关的任务,所有想借这个机会把c++重新学习一遍。 在推荐领域, 目前我接触到的算法模型方面主要是基于Python, 而线上的服务全是c++(算法侧, 业务那边基本上用go),我们所谓的模型,也一般是训练好部署上线然后提供接口而已。所以现在也终于知道,为啥只单纯熟悉Python不太行了, cpp,才是yyds。

和python一样, 这个系列是重温,依然不会整理太基础性的东西,更像是查缺补漏, 不过,c++对我来说, 已经5年没有用过了, 这个缺很大, 也差不多相当重学了, 所以接下来的时间, 重温一遍啦 😉

资料参考主要是C语言中文网光城哥写的C++教程,然后再加自己的理解和编程实验作为辅助,加深印象。 关于更多的细节,还是建议看这两个教程。

今天这篇文章算是小清新,整理点稍微简单的内容,运算符重载,重载呢, 就是赋予新的含义,函数重载,可以让一个函数名有多种功能,不同情况下进行不同操作, 运算符重载类比过来, 同一个运算符可以有不同的功能。而这就是这篇文章内容啦。

主要内容:

  • C++运算符重载初识
  • C++运算符重载规则
  • C++重载运算符到底应该以成员函数还是全局函数(右元函数)形式重载?
  • C++重载各种运算符示例
  • 小总

Ok, let’s go!

2. C++运算符重载初识

上面聊到,运算符重载可以让相同的运算符有不同的功能,比如"+"这个运算符,可以对不同类型的数据进行加法操作, 再比如"<<"既是位移运算符,又可以配合cout做输出,这其实背后是C++做好的一些重载。

那么, 如果我们想自己重载运算符, 让符号再实现一波新功能,应该怎么做呢? 比如让"+"实现复数的加法运算,而不仅仅是int或者float运算, how do it?

两种方式。

2.1 类内重载运算符

就像上面的这个需求, + 实现复数之间的运算,这时候,就可以先定义一个复数类,然后把重载运算符的实现逻辑作为类的成员出现,like this:

class complex{
public:
    complex();
    complex(double real, double imag);
public:
    //声明运算符重载
    complex operator+(const complex &A) const;
    void display() const;
private:
    double m_real;  //实部
    double m_imag;  //虚部
};
complex::complex(): m_real(0.0), m_imag(0.0){ }
complex::complex(double real, double imag): m_real(real), m_imag(imag){ }
//实现运算符重载
complex complex::operator+(const complex &A) const{
    complex B;
    B.m_real = this->m_real + A.m_real;
    B.m_imag = this->m_imag + A.m_imag;
    return B;
}
void complex::display() const{
    cout<<m_real<<" + "<<m_imag<<"i"<<endl;
}
int main(){
    complex c1(4.3, 5.8);
    complex c2(2.4, 3.7);
    complex c3;
    c3 = c1 + c2;
    c3.display();   // 6.7 + 9.5i
    return 0;
}

运算符重载其实就是定义一个函数,在函数体内实现想要的功能, 当用到该运算符时,编译器会自动调用这个函数,即运算符重载是通过函数实现的,本质上是函数重载

格式:

返回值类型 operator 运算符名称 (形参表列){
    //TODO:
}

operator是关键字,专门用于定义重载运算符的函数。 可以将operator 运算符名称 这一部分看做函数名,即operator +运算符重载函数除了函数名有特定方式,其他地方和普通函数没啥区别

上面例子的过程倒是感觉有点意思, 首先,complex类中重载了运算符+,该重载只对complex对象有效。 当执行c3=c1+c2的时候,由于+具有左结合性,编译器检测到+号左边是一个complex对象,就会调用该对象的成员函数operator +(), 进行下面的转换:

c3 = c1.operator+(c2);

上面运算符重载的更简洁方式:

complex complex::operator+(const complex &A)const{
    return complex(this->m_real + A.m_real, this->m_imag + A.m_imag);  
    // return这里会创建一个匿名对象,创建这个临时对象的时候调用构造函数,return将这个临时对象作为函数返回值
}

这,就是运算符重载背后的故事!

2.2 全局范围内

运算符重载函数不仅可以作为类的成员函数, 还可以作为全局函数。上面的代码如果改写成全局版的, 应该是下面这样:

class complex{
public:
    complex();
    complex(double real, double imag);
public:
    void display() const;
    //声明为友元函数
    friend complex operator+(const complex &A, const complex &B);
private:
    double m_real;
    double m_imag;
};
complex operator+(const complex &A, const complex &B);

//在全局范围内重载+
complex operator+(const complex &A, const complex &B){
    complex C;
    C.m_real = A.m_real + B.m_real;
    C.m_imag = A.m_imag + B.m_imag;
    return C;
}

此时,如果执行到c3=c1+c2语句时,编译器检测到+号两边都是complex对象,就会转换为下面的函数调用:

c3 = operator+(c1, c2);

3. C++运算符重载时的规则

运算符时通过函数重载实现的, 但有几点注意:

  1. 并不是所有的运算符都可以重载, 能够重载的运算符包括: + - * / % ^ & | ~ ! = < > += -= *= /= %= ^= &= |= << >> <<= >>= == != <= >= && || ++ -- , ->* -> () [] new new[] delete delete[], 自增自减运算符等。 而长度运算符sizeof, 条件运算符:?, 成员选择符.和域解析运算符::不能被重载
  2. 重载不能改变运算符的优先级和结合性。
  3. 重载不会改变运算符的用法,原有有几个操作数,操作数在左边还是右边,这些都不会改变。 例如~号右边只有一个操作数, +号总是出现在两个操作数之间,重载后也必须如此。
  4. 运算符重载函数不能有默认的参数,否则就改变了运算符操作数的个数,这显然错误
  5. 运算符重载函数既可以作为类的成员函数,也可以作为全局函数
    • 将运算符重载函数作为类的成员函数时,二元运算符的参数只有一个,一元运算符不需要参数。 之所以少一个参数,是因为这个参数是隐含的。比如complex operator+(const complex & A) const;,当执行c3=c1+c2时,会被转换为c3=c1.operator+(c2);
    • 将运算符重载函数作为全局函数时,二元操作符就需要两个参数,一元操作符需要一个参数,而且其中必须有一个参数是对象,好让编译器区分是程序员自定义运算符,防止程序员修改用于内置类型的运算符性质。比如下面这种操作是不允许的:
      int operator + (int a,int b){
          return (a-b);
      }
      
      +号原来是对两个数相加,现在企图通过重载使它的作用改为两个数相减, 如果允许这样重载的话,那么表达式4+3的结果是 7 还是 1 呢?显然,这是绝对禁止的。如果有两个参数, 这两个参数可以都是对象,也可以一个是对象,一个是C++内置类型的数据:
      complex operator+(int a, complex &c){
          return complex(a+c.real, c.imag);
      }
      
      它的作用是使一个整数和一个复数相加。

      另外,将运算符重载函数作为全局函数时,一般都需要在类中将该函数声明为友元函数。 原因很简单,该函数大部分情况下,需要使用类的private成员。
  6. 箭头运算符->, 下标运算符[],函数调用运算符(),赋值运算符=只能以成员函数的形式重载。

那么,此时可能有的一个问题,既然重载运算符,有全局函数和成员函数两种方式, 那么我们当遇到运算符重载需求的时候,应该用哪一种方式呢? 会有什么不同?

4. 成员函数 VS 全局函数重载运算符

不同运算符应该采用不同的重载方式, 不能一股脑都写作成员函数或者全局函数, 比如+, -, *, /, ==, !=这种, 一般建议是全局函数的形式, 而如果是+=, -=, *=, /=这种, 一般建议是成员函数的形式, But, why?

这里会涉及到一个概念叫做转换构造函数, 关于这个概念的更深层原理, 后面会整理到, 这里只需要先看一个例子:

// 负数类
class Complex{
public:
    Complex(): m_real(0.0), m_imag(0.0){}
    Complex(double real, double imag): m_real(real), m_imag(imag){}
    Complex(double real): m_real(real), m_imag(0.0){}    // 转换构造函数  如果注释掉,就会报错
public:
    friend Complex operator+(const Complex &c1, const Complex &c2);
public:
    double real() const{return m_real;}
    double imag() const{return m_imag;}

private:
    double m_real;   // 实部
    double m_imag;   // 虚部
};

// 重载+运算符
Complex operator+(const Complex &c1, const Complex &c2){
    return Complex(c1.m_real + c2.m_real, c1.m_imag + c2.m_imag);
}
 
int main()
{
    Complex c1(25, 35);
    Complex c2 = c1 + 15.6;
    Complex c3 = 28.3 + c1;
    cout << c2.real() << "+" << c2.imag() << "i" << endl;   // 40.6+35i
    cout << c3.real() << "+" << c3.imag() << "i" << endl;   // 53.3+35i
    return 0;
}

主函数里面的2,3行代码,说明Complex类型可以和double类型相加, 这很奇怪,因为并没有针对这两个类型重载+, 那么这是怎么做到的呢?

实际上,编译器在检测到Complex和double相加时,会尝试先将double转换成Complex或者反过来把Complex转成double,然后才能相加,但如果转换失败或者都转换成功, 就会报错。

在上面例子里面, 编译器会先通过构造函数Complex(double real), 将double转换为Complex,再调用重载过的+进行计算, 整个过程类似下面这样:

在这里插入图片描述
即,小数被转换成了匿名的Complex对象。 这个转换过程中, 构造函数Complex(double real), 起到了至关重要的作用, 如果没有它, 转换就会失败, Complex也不能和double相加。

Complex(double real), 在作为普通构造函数的同时,还能将double类型转换为Complex类型,集合了"构造函数"和"类型转换"的功能, 所以被称为转换构造函数。 换句话说,转换构造函数用来将其他类型(bool, int, double等基本类型, 数组,指针,结构体,类等构造类型)转换为当前类型。

4.1 为什么要用全局函数的形式重载+

上面例子中,定义的operator + 是一个全局函数(友元函数), 而不是成员函数, 这样做是为了保证+运算符的操作数能够被对称的处理。换句话说,小数(double类型)在+左边和右边都是正确的。

如果将operator +定义为成员函数, 根据"+运算符具有左结合性"原则, Complex c2=c1+15.6会被转换成下面形式:

Complex c2 = c1.operator+(Complex(15.6));

这就是通过对象调用成员函数, 这是正确的。 但是对于Complex c3=28.2+c1; 编译器尝试转成下面的形式:

Complex c3 = (28.2).operator+(c1);

这个很显然是错误的,double类型没有以成员函数的形式重载+

也就是说,以成员函数的形式重载+, 只能计算c1+15.6, 不能计算28.2+c1, 这个是不对称的。

那为啥不能把28.2先转成Complex,然后再相加啊,类似这样:

Complex c3 = Complex(28.2).operator+(c1);

为什么不转呢? 这是因为C++只会对成员函数的参数进行类型转换,而不会对调用成员函数的对象进行转换。 比如:

obj.func(params); 

编译器不会尝试对obj进行类型转换,它有func()函数就调用,没有就报错。 而对于实参params, 编译器会"拼命的"将它转换为形参的类型。

4.2 为什么要用成员函数的形式重载+=

运算符重载的初衷,给类添加新的功能, 方便类的运算,它作为类的成员函数式理所当然的,是首选的。 But, 类的成员函数不能对称的处理数据,程序员必须在(参与运算)所有类型的内部都重载当前的运算符。 比如上面这个例子, 必须在Complex和double内部都重载+运算符,这样做不但会增加运算符重载的数目,还要在许多地方修改代码,显然不是我们希望的,所以C++为了折中, 允许以全局函数(友元函数)的方式重载运算符。

采用全局函数能使我们定义参数具有逻辑对称性的运算符,而与此相应的, 把运算符定义为成员函数能够保证在调用时,对第一个(左边)运算对象不出现类型转换

所以呢,有一部分运算符重载既可以是成员函数也可以是全局函数,但应该优先考虑成员函数,这样更符合运算符重载的初衷。 而有一些运算符必须是全局运算符, 才能保证参数的对称性。 除了C++规定的几个运算符必须以成员函数形成重载之外(->, [], (), =), 其他的没有必须强制。

5. C++重载各种运算符实例

5.1 重载数学运算符

这里玩的一个例子,就是对于复数进行完整的四则运算(+,-,*,/)以及+=,-=,*=,/=,一个原因是为了强化一下上面的理解,另外一个就是提高下动手能力,毕竟,talk is cheap! 可以在上面给出的网址里面进行操练起来了, 哈哈。

class Complex{
public:
    Complex(double real=0.0, double imag=0.0): m_real(real), m_imag(imag){}
    
    // 全局函数的形式重载对称性运算符
    friend Complex operator+(const Complex &c1, const Complex &c2);
    friend Complex operator-(const Complex &c1, const Complex &c2);
    friend Complex operator*(const Complex &c1, const Complex &c2);
    friend Complex operator/(const Complex &c1, const Complex &c2);
    friend bool operator==(const Complex &c1, const Complex &c2);
    
    // 成员函数的形式重载
    Complex & operator+=(const Complex &c);
    //Complex & operator-=(const Complex &c);
    //Complex & operator*=(const Complex &c);
    //Complex & operator/=(const Complex &c);
    
    double real() const {return m_real;}
    double imag() const {return m_imag;}
    
private:
    double m_real;
    double m_imag;
};

// 重载+运算符
Complex operator+(const Complex &c1, const Complex &c2){return Complex(c1.m_real + c2.m_real, c1.m_imag + c2.m_imag);}
// 重载-运算符
Complex operator-(const Complex &c1, const Complex &c2){return Complex(c1.m_real - c2.m_real, c1.m_imag - c2.m_imag);}
// 重载*运算符 (a+bi) * (c+di) = (ac-bd) + (bc+ad)i
Complex operator*(const Complex &c1, const Complex &c2){return Complex(c1.m_real * c2.m_real - c1.m_imag * c2.m_imag, c1.m_imag * c2.m_imag + c1.m_real * c2.m_imag);}
// 重载/运算符 (a+bi) / (c+di) = [(ac+bd) / (c²+d²)] + [(bc-ad) / (c²+d²)]i
Complex operator/(const Complex &c1, const Complex &c2){return Complex((c1.m_real*c2.m_real + c1.m_imag*c2.m_imag) / (pow(c2.m_real, 2) + pow(c2.m_imag,2)), (c1.m_imag*c2.m_real - c1.m_real*c2.m_imag) / (pow(c2.m_real, 2) + pow(c2.m_imag, 2)));}
// 重载==运算符
bool operator==(const Complex &c1, const Complex &c2){return ((c1.m_real==c2.m_real) && (c1.m_imag==c2.m_imag))? true: false;}

//重载+=运算符
Complex & Complex::operator+=(const Complex &c){
    this->m_real += c.m_real;
    this->m_imag += c.m_imag;
    return *this;
}

int main()
{
    Complex c1(25, 35);
    Complex c2(10, 20);
    
    Complex c3 = c1 + c2;
    cout << c3.real() << " " << c3.imag() << endl;
    return 0;
}

这个例子的演示, 主要是说明, 需要保留对称性的运算符+,-,*,/,==都用全局函数的形成重载,而不需要保留对称性的,优先考虑成员函数。

5.2 重载>><<

C++中,标准库已经对左移运算<<和右移运算>>分别进行重载,使其能够用于不同数据的输入和输出,但输入和输出对象只能是C++的内置类型和标准库所包含的类类型。

如果我们自己定义了一种新数据类型,需要用输入输出运算符处理,就必须重载, 这里显示下, 对<<>>重载,来输出上面定义的Complex类。当然,这个在C++标准库已经提供了相关运算。

这个例子比较简单,就是输入两个复数, 输出它们的和。

class Complex{
public:
    Complex(double real=0.0, double imag=0.0): m_real(real), m_imag(imag){}
    
    // 全局函数的形式重载对称性运算符
    friend Complex operator+(const Complex &c1, const Complex &c2);
    
    friend istream & operator >>(istream & in, Complex &A);
    friend ostream & operator <<(ostream & out, Complex &A);  

private:
    double m_real;
    double m_imag;
};

// 重载+运算符
Complex operator+(const Complex &c1, const Complex &c2){return Complex(c1.m_real + c2.m_real, c1.m_imag + c2.m_imag);}

istream & operator >>(istream & in, Complex & A){
    in >> A.m_real >> A.m_imag;
    return in;
}

ostream & operator <<(ostream & out, Complex & A){
    out << A.m_real << "+" << A.m_imag << "i";
    return out;
}

int main()
{
    Complex c1, c2;
    cin >> c1 >> c2;
    
    Complex c3 = c1 + c2;
    cout << c3 << endl;
    return 0;
}

5.3 重载[]

C++规定, 下标运算符[]必须以成员函数形式重载。该重载函数在类中声明如下:

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

或者

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

使用第一种方式声明, []不仅可访问元素,还可以修改元素, 而用第二种声明方式,[]只能访问而不能修改对象。

实际开发中,应该同时提供上面两种形式,这样做为了适应const对象,因为用过const对象只能调用const成员函数,如果不提供第二种形式,将无法访问const对象的任何元素。

下面一个具体例子演示如何重载[]。我们知道,有些较老的编译器不支持变长数组,例如 VC6.0、VS2010 等,这有时候会给编程带来不便,下面我们通过自定义的 Array 类来实现变长数组。

class Array{
public:
    Array(int length=0);
    ~Array();
    
    int & operator[](int i);
    const int & operator[](int i) const;
    
    int length() const {return m_length;}
    void display() const;
private:
    int m_length;
    int *m_p;
};

Array::Array(int length):m_length(length){
    if (length == 0){
        m_p = NULL;
    }else{
        m_p = new int[length];
    }
}

Array::~Array(){ delete[] m_p; }

int& Array::operator[](int i){
    return m_p[i];
}

const int & Array::operator[](int i) const{
    return m_p[i];
}

void Array::display() const{
    for (int i = 0; i < m_length; i++){
        if (i == m_length - 1){
            cout << m_p[i] << endl;
        }else{
            cout << m_p[i] << " ";
        }
    }
}

int main()
{
    int n;
    cin >> n;
    
    Array A(n);
    for (int i = 0, len = A.length(); i < len; i++){
        A[i] = i * 5;
    }
    A.display();
    
    const Array B(n);
    cout << B[n-1] << endl;  
    
    return 0;
}

重载[]运算符之后, 表达式arr[i]被转成了arr.operator[](i);

B是const对象,如果Array类没有提供const版本的operator[], 最后B[n-1]这里会出错,因为它试图调用非const版本的operator[], 虽然没有修改对象,但编译器看到const对象调用非const成员函数, 编译器就认为会修改对象,不允许。注释掉看下:

在这里插入图片描述

5.3 重载++--

自增++和自减–都是一元运算符,前置和后置形式都可以被重载。下面这个例子演示下:

// 秒表类
class stopwatch{
public:
    stopwatch(): m_min(0), m_sec(0){}
    
    void setzero(){m_min = 0; m_sec = 0;}
    stopwatch run();
    stopwatch operator++(); // ++i, 前置形式
    stopwatch operator++(int n);  // i++, 后置形式
    friend ostream & operator << (ostream &, const stopwatch &);

private:
    int m_min;
    int m_sec;
};

stopwatch stopwatch::run(){
    ++m_sec;
    if (m_sec == 60){
        m_min ++;
        m_sec = 0;
    }
    return *this;
}

stopwatch stopwatch::operator++(){return run();}
stopwatch stopwatch::operator++(int n){
    stopwatch s = *this;
    run();
    return s;
}

ostream &operator<<(ostream & out, const stopwatch & s){
    out << setfill('0') << setw(2) << s.m_min << ":" << setw(2) << s.m_sec;
    return out;
}

int main()
{
    stopwatch s1, s2;
    
    s1 = s2++;
    cout << "s1: " << s1 << endl;
    cout << "s2: " << s2 << endl;
    s1.setzero();
    s2.setzero();
    
    s1 = ++s2;
    cout << "s1: " << s1 << endl;
    cout << "s2: " << s2 << endl;
    
    return 0;
}

run() 函数一开始让秒针自增,如果此时自增结果等于60了,则应该进位,分钟加1,秒针置零。

  • operator++() 函数实现自增的前置形式,直接返回 run() 函数运行结果即可。
  • operator++ (int n) 函数实现自增的后置形式,返回值是对象本身,但是之后再次使用该对象时,对象自增了,所以在该函数的函数体中,先将对象保存,然后调用一次 run() 函数,之后再将先前保存的对象返回。在这个函数中参数n是没有任何意义的,它的存在只是为了区分是前置形式还是后置形式

6. 小总

C++进行运算符重载时, 需要注意几个问题:

  • 重载后运算符的含义应该符合原有用法习惯。 例如重载+运算符,别去搞减法,应该尽量保留运算符原有特性
  • C++规定, 运算符重载不改变运算符优先级
  • 下面运算符不能重载: .、.*、::、? :、sizeof
  • 重载运算符(), [], ->或者赋值运算符=时,只能重载为成员函数,不能重载为全局函数。

另外,下面的知识点要理解:

  • 运算符重载的实质是将运算符重载为一个函数,使用运算符表达式被解释为重载函数的调用
  • 运算符可被重载为全局函数。此时函数参数个数是运算符操作数个数,运算符操作数是函数实参
  • 运算符可重载为成员函数。此时函数参数个数是运算符操作数减一,运算符操作数有一个成为函数作用的对象,其余的成为函数实参
  • 必要时,需要重载赋值运算符=,避免两个对象内部的指针指向同一片存储空间
  • 运算符可重载为全局函数,然后声明为类的友元
  • <<>>是iostream中被重载,才成为所谓的"流插入运算符"和"流提取运算符"的。
  • 类型的名字可以作为强制类型转换运算符,也可以被重载为类的成员函数。 它能使得对象被自动转换为某种类型
  • 自增、自减运算符各有两种重载形式,用于区别前置用法和后置用法
  • 运算符重载不改变运算符优先级,重载运算符时,应该尽量保留运算符原本的特性。
  • 7
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值