返回值和右值引用的传递问题实测

测试类

class Test2  
{  
public:  
    Test2() {}  
    Test2(const char* str);  
    Test2(const Test2& o);  
    Test2(Test2&& o);  
    virtual ~Test2();  
    Test2& operator=(const Test2& o);  
    Test2& operator=(Test2&& o);  
    void swap(Test2& o);  
    const char* cstr() const { return _blocks ? _blocks : ""; }  
protected:  
    char* _blocks;  // 保存字符串的缓冲区  
};  

可以看到,这个类中包含了C++11标准中规定的若干元素:

  • 默认构造函数(可缺省);
  • 参数构造函数(可缺省);
  • 析构函数;
  • copy构造函数;
  • move构造函数(转移构造函数);
  • 赋值运算符;
  • move赋值运算符(转移赋值运算);
  • 对象交换函数;

其中,关键的几个函数实现如下:

#include<string.h>
#include<iostream>

/**
* 参数构造器
* @param [in] str 字符串值
*/
Test2::Test2(const char* str) :
    _blocks(NULL)
{
    if(str)
         _blocks = ::strdup(str);//#include<string.h>和free()函数成对出现
    std::cout<<"Test2(const char* str)"<<std::endl;
}

/**
* 拷贝构造函数
* @param [in] o 同类型的另一个对象引用
*/
Test2::Test2(const Test2& o) :
    _blocks(NULL)
{
    if (o._blocks)
        _blocks = ::strdup(o._blocks);
    std::cout<<"Test2(const Test2& o)"<<std::endl;
}

/**
* Move构造函数
* @param [in] o 同类型的另一个对象右值引用
*/
Test2::Test2(Test2&& o) :
    _blocks(NULL)
{
    swap(o);
    std::cout<<"Test2(Test2&& o)"<<std::endl;
}

/**
* 析构函数
*/
Test2::~Test2()
{
    if (_blocks)
        ::free(_blocks);
    _blocks = NULL;
    std::cout<<"~Test2()"<<std::endl;
}

/**
* 赋值运算符重载
* @param [in] o 同类型的另一个对象引用
* @return 当前类型的另一个引用
*/
Test2& Test2::operator=(const Test2& o)
{
    if (this != &o)
        Test2(o).swap(*this);

    std::cout<<"operator=(const Test2& o)"<<std::endl;
    return *this;
}

/**
* 右值引用赋值运算符重载
* @param [in] o 同类型的另一个对象右值引用
* @return 当前类型的另一个引用
*/
Test2& Test2::operator=(Test2&& o)
{
    if (this != &o)
    {
        swap(o);
        o.~Test2();
    }
    std::cout<<"operator=(Test2&& o)"<<std::endl;
    return *this;
}

/**
* 交换两个对象
* @param [in] o 同类型的另一个对象
*/
void Test2::swap(Test2& o)
{
    std::swap(_blocks, o._blocks);
}

第一个函数,返回函数内部产生的局部变量:

Test2 return_object()
{
    Test2 res = "kkkk";
    return res;
}

int main()
{
    Test2 t1 = return_object();
    std::cout<<t1.cstr()<<std::endl;
    t1 = return_object();
    return 0;
}
//结果
//第一行输出
//Test2(const char* str)
//kkkk
//~Test2()
//两行共输出
//Test2(const char* str)
//kkkk
//Test2(const char* str)
//~Test2()
//operator=(Test2&& o)
//~Test2()
//~Test2()

结论:

  1. 第一行代码中,只在调用函数内部执行了一次参数构造函数(构造局部对象),没有发生copy构造函数(或者move构造函数)的调用,即可以认为在函数内部实例化的局部对象就是返回值变量t1,这应该是编译器优化的结果;
  2. 第二行代码执行时,变量t1已经被初始化,所以赋值运算是必然会发生的,此时除过在调用函数内部执行了一次参数构造函数(构造局部对象)外,还执行了一次move赋值运算,可见编译器认为函数的返回值是右值。由于有了move赋值运算符,所以没有调用”赋值运算符“,相当于将函数内部的局部对象(右值)转移到了t1变量(左值)中,完成了右到左的转化(减少了一次构造和析构);

第二个函数,返回函数内部产生的局部变量的引用:

这个函数一看就是错误的,返回局部变量的引用或指针都是不允许的,因为在函数返回前,局部变量就会被析构,导致返回的引用是无效引用(已经游离)

Test2& return_reference()
{
    Test2 res = "test";
    return res;
}

int main()
{
    Test2 t2 = return_reference();
    std::cout<<t2.cstr()<<std::endl;
    t2 = return_reference();
    return 0;
}
//结果
//第一行输出
//Test2(const char* str)
//~Test2()
//Test2(const Test2& o)
//
//~Test2()
//两行共输出
//Test2(const char* str)
//~Test2()
//Test2(const Test2& o)
//
//Test2(const char* str)
//~Test2()
//Test2(const Test2& o)
//~Test2()
//operator=(const Test2& o)
//~Test2()

结论:

  1. 第一行代码中,在调用函数内部执行了参数构造函数,之后又执行了copy构造函数,其含义是将返回的局部对象引用,通过copy构造函数来构造变量t2对象,但结果是变量t2不一定可以构造成功,即使构造成功了其值也不正确,显然在调用copy构造函数的时候,局部对象已经析构,copy的值无效;
  2. 第二行代码中,在调用函数内部执行了参数构造函数,之后又执行了copy赋值函数,结果和第一行代码类似;

第三个函数,返回函数内部产生局部变量的右值引用:

Test2 return_right_reference()
{
    Test2 res = "lala";
    return std::move(res);// move函数在这里的作用是将res的引用类型转换为右值引用类型
}

int main()
{
    Test2 t3 = return_right_reference();
    std::cout<<t3.cstr()<<std::endl;
    t3 = return_right_reference();
    return 0;
}
//结果
//第一行输出
//Test2(const char* str)
//Test2(Test2&& o)
//~Test2()
//lala
//~Test2()
//两行共输出
//Test2(const char* str)
//Test2(Test2&& o)
//~Test2()
//lala
//Test2(const char* str)
//Test2(Test2&& o)
//~Test2()
//~Test2()
//operator=(Test2&& o)
//~Test2()
//~Test2()

结论:

  1. 第一行代码中,除了调用参数构造函数外,还调用了一次move构造函数,这是由于返回值变成了局部对象的右值引用,和变量t3类型不同,所以又额外的调用了一次move构造函数对变量t3进行初始化;
  2. 第二行代码中,情况就比较复杂了。照例通过参数构造函数,但返回的是其右值引用,所以又调用了一次move构造函数,通过该右值引用产生了一个临时的Test2对象(右值对象),最后通过一个move赋值运算将临时的Test2对象转移给变量t3;

第四个函数,对第三个函数进行修改:

Test2&& return_right_reference2()
{
    Test2 res = "test";
    return std::move(res);  //move函数在这里的作用是将res的引用类型转换为右值引用类型
}

int main()
{
    Test2 t4 = return_right_reference2();
    std::cout<<t4.cstr()<<std::endl;
    t4 = return_right_reference2();
    return 0;
}

结论:(运行失败,没有输出)

  这段代码执行的结果和“第二个函数”一样,返回局部变量的引用(不管是左值还是右值)都不会有正确结果。

总结:

  最后发现,最朴素的写法反而是执行效率最高的写法(“第一个函数”),这种写法充分的利用了编译器在构造对象时进行的优化以及move赋值运算带来的优势,避免了对象在传递过程中产生的临时对象以及引发的构造和析构;这也体现了move赋值运算存在的必要性。
  无论如何,都不能在函数内部返回临时变量的指针或引用,无论该引用是左值引用还是右值引用。C++11也从来没有认为变量的控制权被转移后析构就不再发生了。所以要在函数内部产生一个对象并返回,正确的做法是:1)将对象建立在堆内存上并返回地址;2)返回局部对象,并通过copy复制运算符在函数外复制该局部对象的副本;3)返回局部对象(是一个右值),并通过move复制运算符将返回的局部对象转移到另一个对象中;
  move函数不能乱用,C++在一些场合下,隐含着右值的概念(比如函数返回值就是右值),此时将值进行类型转换都会导致额外的不必要开销(例如将返回值必须是“右值”,如果将其转为“右值引用”,编译器仍要生成代码将其转回“右值”的对象,等于做了一堆无用功)。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值