C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > 返回值是类名和返回值是引用区别

C++返回值是类名和返回值是引用的区别及说明

作者:四月晴

这篇文章主要介绍了C++返回值是类名和返回值是引用的区别及说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

返回值是类名和返回值是引用的区别

返回非引用类型

函数的返回值用于初始化在调用函数时创建的临时对象(temporary object),如果返回类型不是引用,在调用函数的地方会将函数返回值复制给临时对象。

在求解表达式的时候,如果需要一个地方存储其运算结果,编译器会创建一个没命名的对象,这就是临时对象。C++程序员通常用temporary这个术语来代替temporary object。

用函数返回值初始化临时对象与用实参初始化形参的方法是一样的。

当函数返回非引用类型时,其返回值既可以是局部对象,也可以是求解表达式的结果。

返回引用类型

当函数返回引用类型时,没有复制返回值,相反,返回的是对象本身。

千万不要返回局部对象的引用!千万不要返回指向局部对象的指针!

当函数执行完毕时,将释放分配给局部对象的存储空间。此时对局部对象的引用就会指向不确定的内存!返回指向局部对象的指针也是一样的,当函数结束时,局部对象被释放,返回的指针就变成了不再存在的对象的悬垂指针。

返回引用时,要求在函数的参数中,包含有以引用方式或指针方式存在的,需要被返回的参数。

如果返回对象,最后多执行一次拷贝构造函数,如果返回引用,直接返回现存对象

#include <iostream>
using namespace std;

class Timer
{
public:
    Timer();
    Timer(int, int, int);
    friend Timer  &operator+(Timer&, Timer&);
    friend Timer operator-(Timer&, Timer&);
    friend ostream& operator<<(ostream &out, Timer &t);
    friend istream& operator>>(istream &in, Timer &t);
private:
    int hour, minute, second;
};

Timer::Timer()
{
    hour = 0;
    minute = 0;
    second = 0;
}

Timer::Timer(int hour, int minute, int second)
{
    this->hour = hour;
    this->minute = minute;
    this->second = second;
}

Timer & operator+(Timer& a, Timer &b)
{
    a.second = a.second + b.second;
    a.minute = a.minute + b.minute;
    a.hour = a.hour + b.hour;
    if (a.second >= 60)
    {
        a.second = a.second - 60;
        a.minute++;
    }
    if (a.minute >= 60)
    {
        a.minute = a.minute - 60;
        a.hour++;
    }
    return a;
}

Timer operator-(Timer &a, Timer &b)
{
    Timer c;
    c.hour = a.hour - b.hour;
    c.minute = a.minute - b.minute;
    c.second = a.second - b.second;
    if (c.second < 0)
    {
        c.second += 60;
        c.minute--;
    }
    if (c.minute < 0)
    {
        c.minute += 60;
        c.hour--;
    }
    return c;
}

ostream& operator<<(ostream &out, Timer &t)
{
    out << t.hour << ":" << t.minute << ":" << t.second << endl;
    return out;
}

istream& operator>>(istream&in, Timer &t)
{
    cout << "Input hours and minutes." << endl;
    in >> t.hour >> t.minute >> t.second;
    return in;
}

int main()
{
    Timer t1, t2, t3, t4;
    cin >> t1 >> t2;
    cout << "t1=" << t1;
    cout << "t2=" << t2;
    t3 = t1 + t2;
    cout << "t3=t1+t2=" << t3;
    t4 = t1 - t2;
    cout << "t4=t1-t2=" << t4;
    return 0;
}

刚开始我将函数声明为:

friend Timer&operator+(Timer&,Timer&);
friend Timer&operator-(Timer&,Timer&);

对于<<,>>的重载声明的和长代码一样。但是这样做之后我发现出现异常。

在Time &operator+(Timer&t1,Timer&t2)里我声明了一个Timer的局部变量,而当函数调用结束后,该指针变成了悬挂指针。

所以,一定谨记不要在返回值为引用的函数中返回局部变量。

C++函数返回值和返回引用问题

C++函数的返回过程基本可以分为两个阶段,返回阶段和绑定阶段,根据两个阶段中需要返回的值的类型不同(返回值和引用),和要绑定的值的类型(绑定值和引用)会产生不同的情况。

最基本的规则是先返回,再绑定,返回和绑定的时候,都有可能发生移动或者拷贝构造函数的调用来创建临时对象,并且只会发生一次。更具体的,当返回值的时候,函数返回之前,会调用一次拷贝构造函数或移动构造函数,函数在绑定到值的时候,会发生一次拷贝构造函数或移动构造函数,如果返回时已经调用了构造函数,则绑定时不会再调用,而是直接绑定。返回引用或者绑定到引用,在各自阶段,不会产生构造函数的调用。

下面举例阐述各种可能的情况(例子中很多情况我们是不该这样写的,本文只讨论如果这样做了会怎样)。

关于右值引用和移动构造函数的解释,后面会写篇东西做个总结。这里仅需要一条规则,那就是在需要调用移动构造函数的情况,如果类没有定义移动构造函数,则调用它的拷贝构造函数,后面对未定义移动构造函数的情况,不再赘述。

为叙述方便,定义绑定值和返回值:

绑定值就是函数赋给的变量,返回值就是函数的返回值,最终的调用形式类似:

返回值 fun()
{
    .....
    return 返回值;
}

绑定值 = func();

定义一个类:

class myClass
{
public:
    //构造函数
    myClass()
    {
        cout << "construct" << endl;
    }
    //拷贝构造函数
    myClass(const myClass& rhs)
    {
        cout << "copy construct" << endl;
    }
    //移动构造函数
    myClass(myClass&& rhs)
    {
        cout << "move consturct" << endl;
    }
    //析构函数
    ~myClass()
    {
        cout << "desctruct" << endl;
    }
};

1.绑定值类型为值类型

返回值类型为值类型,返回的是局部变量:

在函数返回阶段,调用类的移动构造函数创建返回值,并绑定到绑定值上。移动构造函数的调用发生在函数返回阶段。

例子:

myClass fun()
{
    myClass mc;
    return mc;
}
 
int main()
{
    myClass result = fun();
}

输出:

construct
move consturct
desctruct
desctruct

2.绑定值类型为值类型

返回值类型为值类型,返回的是局部变量的引用:

首先,这不是一个好的做法,企图返回一个局部变量的引用结果通常并不会令人满意。

函数调用结束后,函数帧被回收(实际上只是设置了栈顶的地址),函数栈中的临时变量不再有意义。

但是这么做通常也不会造成什么副作用,因为在函数返回阶段,会调用拷贝构造函数(注意,即使定义了移动构造函数也不会调用),将生成的新的临时对象,绑定到绑定值上。

例子:

myClass fun()
{
    myClass mc;
    myClass& mcRef = mc;
        //如果是想返回这个局部变量的引用,并且在函数外部对其进行修改,那必然要失望了。
    return mcRef;
}
 
int main()
{
    myClass result = fun();
}

输出:

construct
copy construct
desctruct
desctruct

3.绑定值类型为值类型

返回值类型为值类型,返回的是全局变量的引用:

和2中的情形类似,但是由于全局变量的生命周期超过当前函数,所以即使函数返回,变量仍然存活。在函数返回阶段,仍然会调用拷贝构造函数,将产生得临时对象绑定到绑定值上,却依然不能返回全局对象。

例子:

通常,返回作为参数而传递进来的引用的情况更加常见,此时的参数对于函数来说,和全局变量类似,即函数返回,变量的生命周期也不会结束。

myClass fun(myClass& param)
{
        //最终只能得到param的拷贝
    return param;
}
 
int main()
{
    myClass mc;
    myClass result = fun(mc);
}

输出:

construct
copy construct
desctruct
desctruct

4.绑定值类型为值类型

返回值类型为引用类型,返回的是局部变量或局部变量的引用:

和情况2情形类似的是,希望以引用的方式返回局部变量或者局部变量的引用,通常也不能取得令人满意的结果;但与情况2的情形不同的是,情况2基本不会产生什么副作用,情况2中在函数返回阶段对对象进行拷贝,此时对象完整;但是在本情况中,就会产生严重的副作用。

由于返回值类型为引用类型,在函数返回阶段,并不会调用拷贝构造函数,而在绑定阶段,绑定值是值类型,才会产生拷贝构造函数的调用,而此时函数已经返回,拷贝函数拷贝的是函数中的临时变量,此时已经析构了,再进行拷贝,只会得到一个被析构对象的拷贝,通常,这会产生严重的错误。

简单的说,这种情况下,会先析构临时对象,在拷贝这个临时对象。

例子:

myClass& fun()
{
    myClass mc;
    //或者myClass mcRef = mc; return mcRef
    return mc;
}
 
int main()
{
    myClass result = fun();
}

输出:

construct
desctruct
copy construct
desctruct

5.绑定值类型为值类型

返回值类型为引用类型,返回的是全局变量的引用:

基本的情况和2是类似的,即并不能真的的到全局变量的引用,但是也不会产生什么副作用(除非调用者就是希望或者这个全局变量,并进行修改)。由于全局变量在函数结束之后并不会被析构,所以对它调用拷贝构造函数是安全的。和情况4类似,拷贝函数的调用也不是发生在函数返回阶段,而是发生在绑定阶段。

例子:

同样使用参数为引用类型的函数举例:

myClass& fun(myClass& param)
{
    return param;
}
 
int main()
{
    myClass mc;
    myClass result = fun(mc);
}

输出:

construct
copy construct
desctruct
desctruct

上述5中情况是将返回值绑定到值类型的情形,所有情形中,均会出现拷贝或移动构造函数的调用,最终绑定之后,都不可能得到原来的对象,企图通过这种方式来修改原对象,必然会失败。

如果绑定值类型为引用类型,返回值类型为值类型,返回的是局部变量,局部变量的引用或者全局变量会怎么样呢?

这种情况编译器会报错,由于函数返回的是值类型,必然会产生移动构造函数或者拷贝构造函数,产生一个临时对象,而临时对象是一个右值,我们常说的引用,其实是左值引用,而一个右值对象是不能够绑定到一个左值引用的,所以编译器会报错。

我们可以将函数的返回值版绑定到右值引用变量(使用myClass&&),我们下面讨论返回右值引用的情况。但是,有时候返回一个右值引用并且使用它,可能并不能产生想要的结果(如本文最后例子)。

6.绑定值类型为右值引用

返回值类型为值类型,返回的是局部变量或局部变量的引用或全局变量的引用:

函数返回阶段,由于是返回值类型,所以会调用移动构造函数或者拷贝构造函数,创建一个不具名的临时变量(其实它就是一个右值),可以把它绑定到一个右值引用上。

但是对它的修改是没有什么意义的,本来函数创建的这个不具名临时变量,会在函数返回之后被析构,由于我们增加了一个指向它的引用,所以这个临时变量的生命周期被延长了,直到指向它的引用离开作用域,它才会析构。

例子:

myClass fun()
{
    myClass mc;
    return mc;
}
int main()
{
    myClass&& result = func();
}

输出:

construct
move consturct
desctruct
desctruct 

myClass fun()
{
    myClass mc;
    myClass& mcRef = mc;
    return mcRef;
}
int main()
{
    myClass&& result = fun();
}

输出:

construct
copy construct
desctruct
desctruct

7.绑定值类型为为引用类型

返回值类型为引用类型,返回局部变量或局部变量的引用:

由于函数返回阶段和绑定阶段都是引用类型,所以不会产生任何拷贝或移动构造函数的调用,最终绑定值类就是函数的局部变量,但是由于函数返回后,局部变量已经被析构,在保存这个局部变量的引用没有意义,往往会引起一些诡异的错误。

例子:

myClass& fun()
{
    myClass mc;
    //或者myClass& mcRef = mc; return mcRef;
    return mc;
}
 
int main()
{
        //在函数返回之后,我们甚至还能使用这个result,但是仅仅是编译器将原来result的位置继续解释成result变量,
        //很有可能很快这个位置会被新的栈帧覆盖,继续使用result,可能会出现“诡异”的问题
    myClass& result = fun();
}

输出:

construct
desctruct

8.绑定值类型为引用类型

返回值类型为引用类型,返回全局变量的引用:

通常,当我们这是我们的真实意图,函数在返回阶段和绑定阶段均不存在构造函数的调用,最终绑定值就是需要返回的全局变量,对最终绑定值的任何修改,均会反映到全局变量上。

例子:

依然使用参数为引用类型的函数举例。

myClass& fun(myClass& mc)
{
    return mc;
}
int main()
{
    myClass mc;
    myClass& result = fun(mc);
}

输出:

construct
desctruct

9.一个思考

右值是不能绑定到左值引用的,但是当把右值付给左值会怎样呢?

如下

int main()
{
    myClass mc;
    myClass result = move(mc);
}

答案是会调用移动构造函数创建一个新的对象,并且绑定到绑定值。

输出:

construct
move consturct
desctruct
desctruct

另一个非常类似的例子:

int main()
{
    myClass mc;
    myClass&& mcRightRef = move(mc);
    myClass result = mcRightRef;
}

和第一个非常类似,但是result的生成是调用拷贝构造函数而非移动构造函数完成的。

输出:

construct
copy construct
desctruct
desctruct

这就涉及到了移动构造函数调用的时机,更多关于右值,右值引用和移动构造函数详细的研究,待续。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文