c++中有些重载运算符为什么要返回引用

  • 核心主旨:事实上运算符返回void、返回对象本身、返回对象的引用都是可以的,并不是说一定要返回引用,只不过这三者的含义不同。但是有一点,运算符的出现要保证能够连续的运算。

返回void

先看下面的表达式:

void operator+(classA &a,classA &b)
{
    //重载函数的定义
}

int main()
{
    classA a,b,c,d;
    a=b+c+d;
}

那么最上面的代码中,a、b、c、d4个类的对象,如果我们重载“+”运算符的函数返回值是void,那么c+d这个表达式返回值就是一个void,然后a=b+void????,很明显不对,编译器会说类型不匹配。所以还是那一点,重载函数写完,你不能让该运算符失去连续运算这一特性。

这里说一下,其实如果你硬要写成void,那你不能写a=b+c+d; 你可以写成b+c+d; 这样语法是没问题,编译器也能过的,就是没啥。。。。。对吧,没啥用。这也就是我说返回void也是可以的,看你的需要求了。

返回类的对象

​ 那上面的例子中,如果你想让a=b+c+d可以正确运行,那重载函数的返回值得是类的对象或者对象的引用。所以我们重载运算符返回值为类的对象是为了实现连续的运算。

​ 那如果返回对象,代码的结果是啥样呢?这里我们用“+”运算符做例子:

#include<iostream>
using namespace std;

class Complex
{
public:
    Complex(double r=0.0,double i=0.0):real(r),imag(i){}
    Complex(const Complex &cc)
    {
        this->real = cc.real;
        this->imag = cc.imag;
        cout<<"copy constructor is done!!!"<<endl;
    }

    Complex operator+(Complex &c1);         //这里我们返回的是类的对象
    friend ostream &operator<<(ostream &out,const Complex &c);    //这个函数先不管,只是为了输出复数类重载一下"<<"运算符

private:
    double real;
    double imag;
};

Complex Complex::operator+(Complex &c1)
{
    this->real+=c1.real;
    this->imag+=c1.imag;
    return *this;               //这里是返回了类的对象哦,所以是要调用拷贝构造的
}

ostream &operator<<(ostream &out,const Complex &c)
{
    out<<"("<<c.real<<","<<c.imag<<")";
    return out;
}

int main()
{
    Complex c1(5,4),c2(2,10),c3;
    cout<<"c1="<<c1<<endl;
    cout<<"c2="<<c2<<endl;
    c3=c1+c2;                   //主要看这一句
    cout<<"c3=c1+c2="<<c3<<endl;

    return 0;
}

程序输出结果:

c1=(5,4)
c2=(2,10)
copy constructor is done!!!
c3=c1+c2=(7,14)

​ 以上就是一个复数类重载”+“运算符的例子,当我们返回对象的时候,就可以做一个连续的运算,而不会像返回void类型一样使得表达式无法进行(当然啦,还是再说一下,语法没有问题,就是没啥结果)。

​ 那么可以看到我们重载”+“运算符函数的定义最后是return *this,即一个新的对象,是会去调用拷贝构造的。

返回类的对象的引用

​ 既然返回类的对象已经可以满足重载后运算符的连续运算这一性质,那为啥有的重载运算符要给引用呢,不多此一举咩?

​ 那这里先给大家补个引用的知识,C++中引用的作用(部分作用):

  • 是一个变量的别名,且不分配内存

  • 是一种绑定的关系,永久性婚姻,即声明的时候必须初始化,一经声明,不可更改

  • 可以对引用再次引用,多次引用后,某一变量具有多个别名

    这里解释第三点:

int a;
int &b=a;
int &c=b;

那么a改变了,b和c也跟着变

​ 那么以上的特性,体现了引用是一个提高效率的东西,那么在重载运算符中,如果返回引用,那么将不会去调用拷贝构造,甚至是析构。看如下代码:

#include<iostream>
using namespace std;

class Complex
{
public:
    Complex(double r=0.0,double i=0.0):real(r),imag(i){}
    Complex(const Complex &cc)
    {
        this->real = cc.real;
        this->imag = cc.imag;
        cout<<"copy constructor is done!!!"<<endl;
    }
    Complex &operator+(Complex &c1);            //这里我们返回的是类的对象的引用
    friend ostream &operator<<(ostream &out,const Complex &c);    //这个函数先不管,只是为了输出复数类重载一下"<<"运算符

private:
    double real;
    double imag;
};

Complex & Complex::operator+(Complex &c1)
{
    this->real+=c1.real;
    this->imag+=c1.imag;
    return *this;               //这里是返回了类的对象的引用哦,不用调构造了,因为是一个已经存在的对象,不是新的对象
}

ostream &operator<<(ostream &out,const Complex &c)
{
    out<<"("<<c.real<<","<<c.imag<<")";
    return out;
}

int main()
{
    Complex c1(5,4),c2(2,10),c3;
    cout<<"c1="<<c1<<endl;
    cout<<"c2="<<c2<<endl;
    c3=c1+c2;                   //主要看这一句
    cout<<"c3=c1+c2="<<c3<<endl;

    return 0;
}

输出结果:

c1=(5,4)
c2=(2,10)
c3=c1+c2=(7,14)

​ 如果返回对象的引用,那么就不会调用拷贝构造函数,当然啦也不会调用析构,因为返回的*this是一个旧的对象,不是新的,所以这样效率就上去了。

总结:重载运算符函数返回值的问题就大概分以上三点,那其实上面讲到的东西并不全,比如有关拷贝构造的知识等,我在下面进行补充,大家可以有兴趣看一下。那可爱在这里给大家展示3种不同写法的运算符重载函数(我们就用”+”运算符为例),如果还不会的小伙伴,可以看看哟!

运算符重载函数——成员函数类内定义版本

class Complex
{
public:
    Complex(double r=0.0,double i=0.0):real(r),imag(i){}
    Complex(const Complex &cc)
    {
        this->real = cc.real;
        this->imag = cc.imag;
        cout<<"copy constructor is done!!!"<<endl;
    }
    Complex operator+(Complex &c1)
    {
        this->real+=c1.real;
        this->imag+=c1.imag;
        return *this;
    }
private:
    double real;
    double imag;
};

运算符重载函数——成员函数类外定义版本

class Complex
{
public:
    Complex(double r=0.0,double i=0.0):real(r),imag(i){}
    Complex(const Complex &cc)
    {
        this->real = cc.real;
        this->imag = cc.imag;
        cout<<"copy constructor is done!!!"<<endl;
    }
    Complex operator+(Complex &c1);         
private:
    double real;
    double imag;
};

Complex Complex::operator+(Complex &c1)
{
    this->real+=c1.real;
    this->imag+=c1.imag;
    return *this;
}

运算符重载函数——友元函数版本

class Complex
{
public:
    Complex(double r=0.0,double i=0.0):real(r),imag(i){}
    Complex(const Complex &cc)
    {
        this->real = cc.real;
        this->imag = cc.imag;
        cout<<"copy constructor is done!!!"<<endl;
    }
    friend Complex operator+(Complex &c1,Complex &c2);    
private:
    double real;
    double imag;
};

Complex operator+(Complex &c1,Complex &c2)
{
    c1.real+=c2.real;
    c1.imag+=c2.imag;
    return c1;
}
大家不要乱写运算符重载函数啊,因为你对某个运算符重载后,STL类库种例如string,vector都是要遵循你写的这个重载,你别不遵守相关语法规则,使得C++的模板库你用了后报错了,结果查到最后发现是重载函数写错了,根本不是模板库的相关语法错误。

拷贝构造函数

​ 这里我们进一步研究一下拷贝构造函数。众所周知,拷贝构造就是复制一个一模一样的东西,它的三个应用场景:

  • 1、当我调用的函数形参是一个对象,就需要

  • 2、当我创建类的对象的时候,用另一个类的对象去给它赋值,就需要

  • 3、如果函数的返回值是类的对象,函数执行完成返回调用者时。

    那请看下面的例子,还是刚才的例子,我们在main函数加了点东西,还加了一个func函数:

#include<iostream>
using namespace std;

class Complex
{
public:
    Complex(double r=0.0,double i=0.0):real(r),imag(i){}
    Complex(const Complex &cc)
    {
        this->real = cc.real;
        this->imag = cc.imag;
        cout<<"copy constructor is done!!!"<<endl;
    }
    Complex &operator+(Complex &c1);            //这里我们返回的是类的对象
    //Complex operator+(Complex &c1);           //这里我们返回的是类的对象
    friend ostream &operator<<(ostream &out,const Complex &c);    //这个函数先不管,只是为了输出复数类重载一下"<<"运算符

private:
    double real;
    double imag;
};

Complex & Complex::operator+(Complex &c1)
{
    this->real+=c1.real;
    this->imag+=c1.imag;
    return *this;               //这里是返回了类的对象哦,所以是要调用拷贝构造的
}

ostream &operator<<(ostream &out,const Complex &c)
{
    out<<"("<<c.real<<","<<c.imag<<")";
    return out;
}

Complex func()          //新加的函数
{
    Complex temp(10,20);
    cout<<"func......."<<endl;
    return temp;                //返回类的对象
}

int main()
{
    Complex c1(5,4),c2(2,10),c3;
    cout<<"c1="<<c1<<endl;
    cout<<"c2="<<c2<<endl;
    c3=c1+c2;                   //主要看这一句
    cout<<"c3=c1+c2="<<c3<<endl;

    //以下两句话是等价的,新加的
    Complex c4=func();
    //Complex c4(func());

    return 0;
}

程序输出结果:

​ 大家发现问题了嘛,为啥只有一个func的输出,Complex c4(func函数返回的对象)应该会调用拷贝构造函数啊,也就是创建c4这个对象的时候,得调用其拷贝构造函数,而且func函数return temp应该也会调用拷贝构造啊,结果一次都没调用,为啥没有调用呢?

​ 答案是:RVO(return value optimization),也就是G++对返回值进行了优化,看下图:

​ 懂了嘛,添加这个选项,两个拷贝构造的调用都出来了,所以有的时候没有输出你想要的,可能是编译器优化掉了,并不是这个过程没有发生。那这里-fno-elide-constructors表示将C11的RVO优化关闭。那关于返回值引用的具体内容,可以我总结的《返回值优化》。

小插曲

​ 不知道大家有没有一个疑惑,为啥拷贝构造函数的形参的是一个对象的引用:

class Complex
{
    Complex(const Complex &cc){};
}

​ 这个是一个标准的拷贝构造函数对吧,形参是一个引用,那为啥不能是一个对象呢,例如下面这个代码:

class Complex
{
    Complex(Complex cc){};
}

​ 那为啥不能写成这样呢?我给大家拆解一下:

class Complex
{
    //如果你这么写,当c2(c1)这句执行后,会有Complex cc=c1,那此时岂不是又要调用拷贝构造函数对cc进行初
    //始化?那就陷入一个死循环状态,可以理解吧
    Complex(Complex cc){};

    //那如果你加了引用,为啥不会出现这样的死循环
    //因为const Complex &cc=c1,这句话并不会再调用拷贝构造函数,这是把c1和引用cc绑定在一起,这两是一
    //个东西,他不是初始化懂吧,而Complex cc=c1这句话是一个初始化的语句,那你对一个对象进行初始化,给的
    //参数还是另一个对象,那你不得调用拷贝构造嘛,是吧。
    Complex(const Complex &cc){};
}

int main()
{
    Complex c1;
    Complex c2(c1);         //这句话啥意思,不就是用c1来为c2做初始化么,也就是要调用构造喽
}

那为啥要加const呢?还是以上面代码为例来解释:

class Complex
{

    Complex(Complex cc){};

    //首先你要知道在这个例子中,哪里会调用拷贝构造
    //main函数的Complex c2(c1);会调用拷贝构造,即用c1来初始化c2,
    //那么对应到函数的参数就是  const Complex &cc=c1,对吧
    //如果没有const,那么c1是一个右值,cc是一个左值,右值是不能赋给左值的
    //但加了const就可以,const (type) &是一个万能引用,既可以被右值赋值,也可以被左值赋值
    //所以一般来说,只要形参不在函数内修改其内容,我们一般都是给const &的,例如运算符重载函数、拷贝构造、移动构造
    Complex(const Complex &cc){};
}

int main()
{
    Complex c1;
    Complex c2(c1);         
}

lionの金库