引用的本质是对一个变量起别名;
引用不会导致新的对象产生(即不会调用构造函数、拷贝构造函数、移动构造函数),当被引用的对象析构时,那么这个引用就成为了一个悬垂引用,使用这个悬垂引用,会导致程序产生未定义行为
左值引用与右值引用
1.对左值的引用即为左值引用,对右值的引用即为右值引用
2.const T&
引用 和 右值引用
,都具有延长临时对象生命周期的能力,但是注意:这种延长生命周期的能力,不能跨作用域,比如以下示例:
- 示例1
const int &func()
{
return 100;
}
const int& ref = func();
//再使用ref时,ref已经变为悬垂引用,因为ref引用的临时对象 100, 已经超出了作用域
const int& ref2 = 100;
//这种对临时变量 100的生命周期进行延长,是ok的
- 示例2
class Test
{
public:
Test(int x) : m_x(x)
{
qDebug() << "construct" << this ;
}
Test (const Test& other) : m_x(other.m_x)
{
qDebug() << "copy construct" << this ;
}
Test (Test&& other) : m_x(other.m_x)
{
qDebug() << "move construct" << this ;
}
~Test()
{
qDebug() << "distruct test" << this ;
}
int m_x = -1;
};
const Test& foo(const Test& t) //测试临时变量的生命周期
{
return t;
}
Test& foo2() //测试跨作用域的左值引用,生命周期
{
Test t(10);
return t;
}
Test&& foo3() //测试跨作用域的右值引用生命周期(这种写法非常危险!!!)
{
Test t(20);
return std::move(t);
}
Test foo4() //std::move 会阻止返回值优化 RVO
{
Test t(30);
return std::move(t);
}
Test foo5() //返回值优化 RVO
{
Test t(30);
return t;
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
// const Test& ret = foo(10);
//首先会通过10,创建一个临时Test对象,然后返这个临时变量的引用,但是这一行结束后,临时变量的生命周期即结束,ret成为悬垂引用, 此处参考:https://www.cnblogs.com/apocelipes/p/18291697 临时变量的生命周期到完整表达式结束
Test& ret2 = foo2();
//返回的是 foo2中局部对象t的左值引用,使用ret绑定到这个左值引用,但是引用不能跨作用域,出了函数 foo2之后,t对象即销毁,ret2成为悬垂引用
Test ret3 = foo2();
//虽然返回的是 foo2中局部对象t的左值引用,但是不是使用引用的形式接收返回值,所以ret3是使用拷贝构造函数创建的一个全新的对象,出了函数 foo2之后,t对象即销毁,但是并不影响ret3对象
Test&& ret4 = foo3();
//返回的是 foo3中局部对象t的右值引用,使用ret绑定到这个右值引用,但是引用不能跨作用域,出了函数 foo3之后,t对象即销毁,ret4 成为悬垂引用
Test ret5 = foo3();
//首先返回的是 foo3中局部对象t的右值引用,这段代码的本意是希望通过移动构造函数来创建一个全新的ret5对象,但是std::move会阻止编译器的返回值优化;这里无论是使用右值引用的形式接收返回值(ret4),还是使用左值的形式接收返回值(ret5), 都会出现垂悬引用,此处非常危险,不要使用std::move返回局部变量的右值引用,且函数返回值还是 && 类型
Test ret6 = foo5();
//直接返回局部变量,在现在编译器中,编译器会返回值优化(简称 RVO),直接调用构造函数生成ret6对象
return a.exec();
}
foo3()
的写法非常危险,返回局部变量的右值引用,且函数返回值还是 && 类型,会造成垂悬引用
foo4()
函数的效率并没有foo5()
的效率高,因为foo5()
使用了RVO,而foo4()
由于std::move
无法使用RVO,会多一次移动构造
操作; 最推荐的写法还是 foo5()
,返璞归真,编译器做了所有优化,程序员的使用负担最小
3.使用左值来构造一个对象,那就是调用的对象的拷贝构造函数;使用右值来构造一个对象,那就是调用的对象的移动构造函数,若该类没有移动构造函数,则调用拷贝构造函数
结论:
一个函数,无论返回的是一个Test对象,还是一个包含Test对象的STL的容器,都应该使用最朴素的方式,直接在函数中返回一个临时变量,并且函数的返回值是最朴素的类型,如果foo5()
,就可以利用编译器的RVO来实现最佳效果,程序员无需担心其他