C++后端高频面试方向:右值引用
右值引用
- 引入及概念:
C++98中提出了引用的概念,引用即别名,引用变量与其引用实体公共同一块内存空间,而引用的底层是通过指针来实现的,因此使用引用,可以提高程序的可读性
- 示例:引用交换变量
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
} 为了提高程序运行效率,C++11中引入了右值引用,右值引用也是别名,但其只能对右值引用
- 示例:
int Add(int a, int b)
{
return a + b;
}
int main()
{
//右值示例
const int&& ra = 10;
// 引用函数返回值,返回值是一个临时变量,为右值
int&& rRet = Add(10, 20);
return 0;
} 注:为了与C++98中的引用进行区分,C++11将该种方式称之为右值引用
1、左值和右值
- 概念:
- 左值与右值是C语言中的概念,但C标准并没有给出严格的区分方式
- 一般认为:左值可放在赋值符号的左边,右值可以放在复制符号的右边;或者能够取地址的称为左值,不能取地址的称为右值
注:左值也能放在赋值符号的右边,右值只能放在赋值符号的右边
- 示例:
int g_a = 10;
// 函数的返回值结果为引用
int& GetG_A()
{
return g_a;
}
int main()
{
int a = 10;
int b = a;
int* p=new int(0);
// a和b,p和*p都是左值,说明:左值既可放在=的左侧,也可放在=的右侧
const int c = 30;
//c = a;
// 特例:c虽然是左值,但是为const常量,只读不允许被修改
cout << &c << endl;
// c可以取地址,所以c严格来看也是左值
//b + 1 = 20;
// 编译失败:因为b+1的结果是一个临时变量,没有具体名称,也不能取地址,因此为右值
GetG_A() = 100;
return 0;
} - 关于左值与右值的区分一般认为:
- 普通类型的变量,因为有名字,可以取地址,都认为是左值
- const修饰的常量,不可修改,只读类型的,理论应该按照右值对待,但因为其可以取地址C++11认为其是左值
- 如果表达式的运行结果是一个临时变量或者对象,认为是右值
- 如果表达式运行结果或单个变量是一个引用则认为是左值
- 注意:
- 不能简单地通过能否放在=左侧右侧或者取地址来判断左值或者右值,要根据表达式结果或变量的性质判断
- 能得到引用的表达式一定能够作为引用,即为左值,否则就用常引用,即为右值
- C++11对右值进行了严格的区分:
- C语言中的纯右值,比如:a+b, 100
- 将亡值,也就是生命周期即将结束的变量,比如临时变量:表达式的中间结果、函数按照值的方式进行返回,匿名变量
2、左值引用和右值引用
在C++98中的普通引用与const引用在引用实体上的区别
- 示例:
int main()
{
// 普通类型引用只能引用左值,不能引用右值
int a = 10;
int& ra1 = a; // ra为a的别名
//int& ra2 = 10; // 编译失败,因为10是右值
//const引用既可以引用左值也可以引用右值
const int& ra3 = 10;
const int& ra4 = a;
return 0;
} - 注意:
- 普通引用只能引用左值,不能引用右值,const引用既可引用左值,也可引用右值
- C++11中右值引用:只能引用右值,一般情况不能直接引用左值
- 示例:
int main()
{
// 10纯右值,本来只是一个符号,没有具体的空间,
// 右值引用变量r1在定义过程中,编译器产生了一个临时变量,r1实际引用的是临时变量
int&& r1 = 10;
r1 = 100;
int a = 10;
int&& r2 = a; // 编译失败:右值引用不能引用左值
int&& r3=move(a);
return 0;
} - 注意:
- 右值引用只能引用右值,不能引用左值
- 右值引用可以进行引用move以后的左值,move表示将该变量识别为右值
- 右值引用本质上是将引用的右值内容存储到空间中,该右值引用变量具有名称和地址,所以右值引用变量是一个左值
3、右值引用
- 概念:
- 本质上引用都是用来减少拷贝,提高效率的
- 左值引用来解决大部分的场景,比如参数引用,返回值引用
- 右值引用是堆左值引用在一些盲区的补充,比如将亡值返回
- 右值引用的引入:
如果一个类中涉及到资源管理,用户必须显式提供拷贝构造、赋值运算符重载以及析构函数,否则编译器将会自动生成一个默认的,如果遇到拷贝对象或者对象之间相互赋值,就会出错
- 示例:
class String
{
public:
String(const char* str)
{
if (nullptr == str)
return;
_str = new char[strlen(str) + 1];
strcpy(_str, str);
cout << "构造" << endl;
}
String(const String& s)
: _str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
cout << "拷贝构造" << endl;
}
String & operator=(const String & s)
{
if (this != &s)
{
char* pTemp = new char[strlen(s._str) + 1];
strcpy(pTemp, s._str);
delete[] _str;
_str = pTemp;
}
cout << "拷贝赋值" << endl;
return *this;
}
String operator+(const String& s)
{
char* pTemp = new char[strlen(_str) + strlen(s._str) + 1];
strcpy(pTemp, _str);
strcpy(pTemp + strlen(_str), s._str);
String strRet(pTemp);
return strRet;
}
~String()
{
if (_str) delete[] _str;
}
private:
char* _str;
};
int main()
{
String s1("hello");
String s2("world");
String s3(s1 + s2);
return 0;
} - 示图:
- 解释:
- 在operator+中:strRet在按照值返回时,必须创建一个临时对象,临时对象创建好之后,strRet就被销毁了,最后使用返回的临时对象构造s3,s3构造好之后,临时对象就被销毁了
- 也就是说strRet、临时对象、s3每个对象创建后,都有自己独立的空间,而空间中存放内容也都相同,相当于创建了三个内容完全相同的对象,对于空间是一种浪费,程序的效率也会降低,而且临时对象确实作用不是很大
- 左值引用的短板:
但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回
4、移动语义
- 右值引用的应用:
C++11提出了移动语义概念,即:将一个对象中资源移动到另一个对象中的方式,可以有效缓解该问题
- 示图:
- 解释:
- 对于像strRet本来是左值,但是这样的在函数体内出作用域即销毁的变量,编译器会优化识别为是一种将亡值,即为右值
- 此处为值传递,会进行临时变量的拷贝,对于右值来说既能匹配参数类型是
- const左值引用的拷贝构造函数,也能匹配参数类型是右值引用的拷贝构造函数,但是编译器会进行匹配类型最合适的函数,即右值引用拷贝构造函数
- 这里的参数为右值引用的拷贝构造函数也叫做移动构造,即对将亡值进行资源的转移,转移到新的构造对象上,而对于将亡值是没有影响的
- 即在用strRet构造临时对象时,就会采用移动构造。而临时对象也是右值,因此在用临时对象构造s3时,也采用移动构造,将临时对象中资源转移到s3中,整个过程,只需要创建一块堆内存即可,既省了空间,又大大提高程序运行的效率
注:在C++11中如果需要实现移动语义,必须使用右值引用(上述String类增加移动构造)
- 示例:
String(String&& s)
: _str(s._str)
{
s._str = nullptr;
cout << "移动拷贝" << endl;
} - 效果:
注:对于连续的两次移动构造,编译器会自动优化成一次,即直接省去了中间的临时变量构造
- 注意:
- 移动构造函数的参数千万不能设置成const类型的右值引用,因为资源无法转移而导致移动语义失效
- 在C++11中,编译器会为类默认生成一个移动构造,该移动构造为浅拷贝,因此当类中涉及到资源管理时,用户必须显式定义自己的移动构造
5、右值引用引用左值
- 概念:
- 按照语法,右值引用只能引用右值,但右值引用是可以引用move后左值:有些场景下,可能真的需要用右值去引用左值实现移动语义
- 当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于头文件< utility >中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义
- move的定义:
template<class _Ty>
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{
// forward _Arg as movable
return ((typename remove_reference<_Ty>::type&&)_Arg);
} - 注意:
- 被转化的左值,其生命周期并没有随着左值的转化而改变,即std::move转化的左值变量lvalue不会被销毁
- STL中也有另一个move函数,就是将一个范围中的元素搬移到另一个位置
- 示例:
int main()
{
String s1("hello world");
String s2(move(s1));
String s3(s2);
return 0;
} - 效果:
注:以上代码是move函数的经典的误用,因为move将s1转化为右值后,在实现s2的拷贝时就会使用移动构造,此时s1的资源就被转移到s2中,s1就成为了无效的字符串
- 示例:
class Person
{
public:
Person(const char* name, const char* sex, int age)
: _name(name)
, _sex(sex)
, _age(age)
{}
Person(const Person& p)
: _name(p._name)
, _sex(p._sex)
, _age(p._age)
{}
Person(Person&& p)
: _name(move(p._name))
, _sex(move(p._sex))
, _age(p._age)
{}
private:
String _name;
String _sex;
int _age;
};
Person GetTempPerson()
{
Person p("prety", "male", 18);
return p;
}
int main()
{
Person p(GetTempPerson());
return 0;
} - 效果:
- 右值引用和移动语义的作用:
移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不
用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己,也就是说资源的生命周期被延长了(对象的生命周期不会改变)
- 移动赋值:
除了移动构造之外,还有移动赋值
Person& operator=(Person&& p)
{
_name=move(p._name);
_sex=move(p._sex);
_age=p._age;
} 注意:
STL中的容器都是增加了移动构造和移动赋值
示例:
void push_back (value_type&& val);
int main()
{
list<String> lt;
String s1("1111");
// 这里调用的是拷贝构造
lt.push_back(s1);
// 下面调用都是移动构造
lt.push_back("2222");
lt.push_back(std::move(s1));
return 0;
} - 示图:
6、完美转发
- 概念及引入:
完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数
- 示例:
void Func(int x)
{
// ......
}
template<typename T>
void PerfectForward(T t)
{
Fun(t);
} - 解释:
- PerfectForward为转发的模板函数,Func为实际目标函数,但是上述转发还不算完美,完美转发是目标函数总希望将参数按照传递给转发函数的实际类型转给目标函数,而不产生额外的开销,就好像转发者不存在一样
- 所谓完美就是函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值
- 这样做是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理,比如参数为左值时执行拷贝语义;参数为右值时执行移动语义
- 具体场景:
- 对于模板参数中的&&,不仅仅是引用右值,语法规定该中情况为万能引用,既能引用右值也能引用左值
- 而这种情况下引用进来的类型变量,都会变成左值,对于引用左值,本身就是左值;对于右值引用,引用后的引用变量会将引用内容储存到空间中,也就是会退化成左值,这里就存在属性的混淆
- 对于这种情况,C++11通过forward函数来实现完美转发
- std::forward 完美转发在传参的过程中保留对象原生类型属性
void Fun(int& x)
{
cout << "lvalue ref" << endl;
}
void Fun(int&& x)
{
cout << "rvalue ref" << endl;
}
void Fun(const int& x)
{
cout << "const lvalue ref" << endl;
}
void Fun(const int&& x)
{
cout << "const rvalue ref" << endl;
}
template<typename T>
void PerfectForward(T&& t)
{
// std::forward<T>(t)在传参的过程中保持了t的原生类型属性
Fun(std::forward<T>(t));
}
int main()
{
PerfectForward(10); // rvalue ref
int a;
PerfectForward(a); // lvalue ref
PerfectForward(std::move(a)); // rvalue ref
const int b = 8;
PerfectForward(b); // const lvalue ref
PerfectForward(std::move(b)); // const rvalue ref
return 0;
} - 效果:
7、右值引用作用
- C++98中引用作用:
因为引用是一个别名,需要用指针操作的地方,可以使用指针来代替,可以提高代码的可读性以及安全性
- C++11中右值引用主要有以下作用:
实现移动语义(移动构造与移动赋值)
给中间临时变量取别名
- 示例:
int main()
{
string s1("hello");
string s2(" world");
string s3 = s1 + s2; // s3是用s1和s2拼接完成之后的结果拷贝构造的新对象
stirng&& s4 = s1 + s2; // s4就是s1和s2拼接完成之后结果的别名
return 0;
} 实现完美转发
查看11道真题和解析
