C++98的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,C++11之后我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名(语法层面上)。
左值和右值
- 左值是一个表示数据的表达式(如变量名或解引用的指针),一般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。
- 右值也是一个表示数据的表达式,要么是字面值常量、要么是表达式求值过程中创建的临时对象,匿名对象等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
- 左值的英文简写为lvalue,右值的英文简写为rvalue。传统认为它们分别是leftvalue、right value 的缩写。
- 现代C++中,lvalue 被解释为loactor value的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象,而 rvalue 被解释为 read value,指的是那些可以提供数据值,但是不可以寻址,例如:临时变量,字⾯量常量,存储于寄存器中的变量等,也就是说左值和右值的核心区别就是能否取地址。
int main()
{
// 左值:可以取地址
// 以下的p、b、c、*p、s、s[0]就是常见的左值
int* p = new int(0);
int b = 1;
const int c = 1;
*p = 10;
string s("11111");
s[0];
//查看地址
cout << &b << endl;
cout << &(*p) << endl;
cout << p << endl;
cout << &p << endl;
//特殊一点的,默认s[0]地址是整个值
cout << (void*)&s[0] << endl;
cout << &s << endl;
// 右值:不能取地址
double x = 1.1;
double y = 2.2;
// 以下几个10、x + y、fmin(x, y)、string("11111")都是常见的右值
10; // 常量
x + y; //临时变量
fmin(x, y); //是传值返回, 临时变量
string("111111"); //匿名变量
//cout << &10 << endl;
//cout << &(x+y) << endl;
//cout << &(fmin(x, y)) << endl;
//cout << &string("11111") << endl;
return 0;
}
左值引用和右值引用
- Type& r1 = x; Type&& rr1 = y; 第一个语句就是左值引用,左值引用就是给左值取别名,第二个就是右值引用,同样的道理,右值引用就是给右值取别名。
- 左值引用不能直接引用右值,但是const左值引用可以引用右值
- 右值引用不能直接引用左值,但是右值引用可以引用move(左值)
int main()
{
// 左值:可以取地址
// 以下的p、b、c、*p、s、s[0]就是常见的左值
int* p = new int(0);
int b = 1;
const int c = 1;
*p = 10;
string s("11111");
s[0];
// 右值:不能取地址
double x = 1.1;
double y = 2.2;
// 以下几个10、x + y、fmin(x, y)、string("11111")都是常见的右值
10; // 常量
x + y; //临时变量
fmin(x, y); //是传值返回, 临时变量
string("111111"); //匿名变量
// 左值引⽤给左值取别名
int*& r1 = p;
int& r2 = b;
const int& r3 = c;
string& r4 = s;
char& r5 = s[0];
// 右值引⽤给右值取别名
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
string&& rr4 = string("11111");
// 左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值
const int& rx1 = 10;
const double& rx2 = x + y;
const double& rx3 = fmin(x, y);
const string& rx4 = string("11111");
// 右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)
int&& rrx1 = move(b);
int*&& rrx2 = move(p);
int&& rrx3 = move(*p);
string&& rrx4 = move(s);
string&& rrx5 = (string&&)s;
return 0;
}
- template <class T> typename remove_reference<T>::type&& move (T&&arg);
- move是库里面的一个函数模板,本质内部是进行强制类型转换,当然他还涉及一些引用折叠的知识。
就是类似于强转
- 需要注意的是变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量变量表达式的属性是左值。
接上面代码
// b、r1、rr1都是变量表达式,都是左值
cout << &b << endl;
cout << &r1 << endl;
cout << &rr1 << endl;
// 这⾥要注意的是,rr1的属性是左值,所以不能再被右值引⽤绑定,除⾮move⼀下
int& r6 = r1;
//int&& rrx6 = rr1;
int&& rrx6 = move(rr1);
为什么能转换呢?
一切皆指针,无论是什么引用在汇编层都是指针,所以可以相互转换;
注意:
- 语法层面看,左值引用和右值引用都是取别名,不开空间。
- 从汇编底层的角度看下面代码中r1和rr1汇编层实现,底层都是用指针实现的,没什么区别。
- 底层汇编等实现和上层语法表达的意义有时是背离的,所以不要然到一起去理解,互相佐证,这样反而是陷入迷途。
- 知道使用move,首先强转后,本身类型不变;
- 但要注意使用,不能随便使用move,move调用的是移动构造,会改变原来的值;
左值和右值的参数匹配
- C++98中,我们实现⼀个const左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。
- C++11以后,分别重载左值引用、const左值引用、右值引用作为形参的f函数,那么实参是左值会匹配f(左值引用),实参是const左值会匹配f(const 左值引用),实参是右值会匹配f(右值引用)。
说白了和模板一样,如果有对胃的菜,就选最对胃的;没有才选需要转化的。
- 右值引用变量在用于表达式时属性是左值,这个设计这里会感觉跟怪,下面讲右值引用的使用场景时,就能体会这样设计的价值了。(左值是可以改变,这很重要)
相关代码:
void f(int& x)
{
std::cout << "左值引用重载 f(" << x << ")\n";
}
void f(const int& x)
{
std::cout << "到 const 的左值引用重载 f(" << x << ")\n";
}
void f(int&& x)
{
std::cout << "右值引用重载 f(" << x << ")\n";
}
int main()
{
/*int&& rr1 = 10;
int a = 20;
int& r2 = a;*/
int i = 1;
const int ci = 2;
f(i); // 调用 f(int&)
f(ci); // 调用 f(const int&)
f(3); // 调用 f(int&&),如果没有 f(int&&) 重载则会调用 f(const int&)
f(std::move(i)); // 调用 f(int&&)
// 右值引用变量在用于表达式时是左值
// 右值引用本身的属性是左值
int&& x = 1;
f(x); // 调用 f(int& x)
f(std::move(x)); // 调用 f(int&& x)
return 0;
}
对应输出
引用延长生命周期
右值引用可用于为临时对象延长生命周期,const 的左值引用也能延长临时对象生存期,但这些对象无法被修改。
相关代码
//延长生命周期;
int main()
{
string s1 = "test1";
//string&& rr1 = s1; // 错误:右值引用不能绑定左值
const string& r1 = s1 + s1; // OK:到 const 的左值引⽤延⻓⽣存期
//r1 += 'test'; // 错误:不能通过到 const 的引⽤修改
string&& rr1 = s1 + s1; // OK:右值引⽤延⻓⽣存期
rr1 += "test"; // OK:能通过到⾮ const 的引⽤修改
cout << rr1 << endl;
return 0;
}
注意:
延长生命周期的场景是什么?
对应同一栈里的,可以利用那两种引用延长临时对象的生命周期;把临时对象的生命周期从那一行延长。
但并不是什么情况下右值引用都能延长生命周期;
比如:在一个函数中,这个栈中;返回一个深拷贝的对象,为什么不能返回引用呢?因为这个函数结束时,栈析构时;这个引用的内容也会被析构,形成一个类似野指针的东西;
具体例子见 (左值引用主要使用场景回顾);
右值引用和移动语义的使用场景
左值引用主要使用场景回顾
左值引用主要使用场景是在函数中左值引用传参和左值引用传返回值时减少拷贝,同时还可以修改实参和修改返回对象的值。
值引用已经解决大多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回,如addStrings和generate函数,C++98中的解决方案只能是被迫使用输出型参数解决。
总的来说:
优点
- 左值引用和右值引用最终目的就是减少拷贝构造,提高效率;
- 左值引用还能修改参数,返回值的值,方便我们的使用
缺点
- 部分返回场景,只能用传值返回,不能用引用返回;
- 当前函数局部对象,出了当前函数作用域生命周期的到头了,销毁了,不能引用左值返回,之能传值返回。
class Solution {
public:
// 传值返回需要拷⻉
string addStrings(string num1, string num2) {
string str;
int end1 = num1.size() - 1, end2 = num2.size() - 1;
// 进位
int next = 0;
while (end1 >= 0 || end2 >= 0)
{
int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
int ret = val1 + val2 + next;
next = ret / 10;
ret = ret % 10;
str += ('0' + ret);
}
if (next == 1)
str += '1';
reverse(str.begin(), str.end());
return str;
}
};
那有什么解决方案呢?能提高效率呢?
方案一:不返回值,用输出型参数解决。
有人会想到返回指针,但你想一下,返回指针,但原本部分的数据还是随着函数域到头析构了。
但这种牺牲了可读性;
// 方案一:不返回值,用输出型参数解决。
void addStrings(string num1, string num2, string& s) {
int end1 = num1.size() - 1, end2 = num2.size() - 1;
// 进位
int next = 0;
while (end1 >= 0 || end2 >= 0)
{
int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
int ret = val1 + val2 + next;
next = ret / 10;
ret = ret % 10;
s += ('0' + ret);
}
if (next == 1)
s += '1';
reverse(s.begin(), s.end());
}
// 方案一:不返回值,用输出型参数解决。
void generate(int numRows, vector<vector<int>>& vv) {
vv.reserve(numRows);
for (int i = 0; i < numRows; ++i)
{
vv[i].resize(i + 1, 1);
}
for (int i = 2; i < numRows; ++i)
{
for (int j = 1; j < i; ++j)
{
vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
}
}
}
方案二:编译器优化
右值对象构造,只有拷贝构造,没有移动构造的场景
- 下图展示了vs2019 debug环境下编译器对拷贝的优化,左边为不优化的情况下,两次拷贝构造,中间为编译器优化的场景下连续步骤中的拷贝合二为一变为一次拷贝构造。
- 需要注意的是在vs2019的release和vs2022的debug和release,下面代码优化为非常恐怖,会直接将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象,合三为⼀,变为直接构造。(图最右边)
如何彻底没有优化?
linux下可以将下面代码拷贝到test.cpp⽂件,编译时用 g++ test.cpp -fno-elideconstructors 的方式关闭构造优化,运行结果可以看到图最左边没有优化的两次拷贝。
右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景
彻底关闭优化:
展示了vs2019 debug和 g++ test.cpp -fno-elide-constructors 关闭优化环境下编译器的处理,一次拷贝构造,一次拷贝赋值。
最厉害的优化 :
在vs2019的release和vs2022的debug和release,下面代码会进一步优化,直接构造要返回的临时对象,str本质是临时对象的引用,底层角度用指针实现。运行结果的角度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名。
方案三:新标准语法处理(左值引用,右值引用)
那移动构造的意义是什么呢,所有自定义类型都有移动构造?
深拷贝定义类型如vector,string,map等等实现移动构造的意义很大,因为移动构造的效率比拷贝构造快的多;直接交换对应内存的内容,比开辟一个再拷贝过去,效率高的多。
浅拷贝定义类型如data,pair<int,int>,实现移动构造的意义其实一般
原因:
类型分类
引用折叠
- C++中不能直接定义引用的引用如 int& && r = i; ,这样写会直接报错,通过模板或 typedef中的类型操作可以构成引用的引用。
- 通过模板或 typedef 中的类型操作可以构成引用的引用时,这时C++11给出了⼀个引用折叠的规则:右值引用的右值引用折叠成右值引用(仅右+右),所有其他组合均折叠成左值引用;
上面两个知识的代码演示1在下面的程序中很好的展示了模板和typedef时构成引用的引用时的引用折叠规则,大家可以一个一个仔细理解一下。
- 像f2这样的函数模板中,T&& x参数看起来是右值引用参数,但是由于引用折叠的规则,他传递左值时就是左值引用,传递右值时就是右值引用,有些地方也把这种函数模板的参数叫做万能引用。
- 万能引用,无论传什么都能接收,且数值类型和其本身一样
Function(T&& t)函数模板程序中,假设实参是int右值,模板参数T的推导int,实参是int左值,模板参数T的推导int&,再结合引用折叠规则;就实现了实参是左值,实例化出左值引用版本形参的Function,实参是右值,实例化出右值引用版本形参的Function。
在代码演示2中存在万能引用
代码演示1:
// 由于引用折叠限定,f1实例化以后总是⼀个左值引用
template<class T>
void f1(T & x)
{}
// 由于引用折叠限定,f2实例化后可以是左值引用,也可以是右值引用
template<class T>
void f2(T&& x)
{}
int main()
{
typedef int& lref;
typedef int&& rref;
int n = 0;
lref& r1 = n; // r1 的类型是 int&
lref&& r2 = n; // r2 的类型是 int&
rref& r3 = n; // r3 的类型是 int&
rref&& r4 = move(n); // r4 的类型是 int&&
//这里学习,加了转换函数的调用表达类型(模板参数)
//一般实际使用引用折叠是不加的;
// 没有折叠->实例化为void f1(int& x) 引用左值
f1<int>(n);
//f1<int>(0); // 报错
// 折叠->实例化为void f1(int& x) 引用左值
f1<int&&>(n);
//f1<int&&>(0); // 报错
// 折叠->实例化为void f1(const int& x) 引用左值
f1<const int&>(n);
//f1<const int&>(0);
f1<const int&>(0);
// 折叠->实例化为void f1(const int& x) 引用右值
f1<const int&&>(n);
f1<const int&&>(0);
// 没有折叠->实例化为void f2(int&& x) 引用右值
//f2<int>(n); // 报错
f2<int>(0);
// 折叠->实例化为void f2(int& x) 引用左值
f2<int&>(n);
//f2<int&>(0); // 报错
// 折叠->实例化为void f2(int&& x) 引用右值
//f2<int&&>(n); // 报错
f2<int&&>(0);
return 0;
}
代码演示2:
万能引用
//万能引用
template<class T>
void Function(T&& t)
{
int a = 0;
T x = a;
//x++;
cout << &a << endl;
cout << &x << endl << endl;
}
int main()
{
// 10是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(10); // 右值
int a;
// a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t)
Function(a); // 左值
// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(std::move(a)); // 右值
const int b = 8;
// a是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int&t)
// 所以Function内部会编译报错,x不能++
Function(b); // const 左值
// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&t)
// 所以Function内部会编译报错,x不能++
Function(std::move(b)); // const 右值
return 0;
}
总结:
那有人会说了,这么麻烦有什么用呢?
实践意义
list的模拟实现
完美转发
引入:
- Function(T&& t)函数模板程序(万能引用)中,传左值实例化以后是左值引用的Function函数,传右值实例化以后是右值引用的Function函数。
- 但是变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传递给下一层函数Fun,那么匹配的都是左值引用版本的Fun函数。这里我们想要保持t对象的属性,就需要使用完美转发实现。
完美转发forward本质是⼀个函数模板,他主要还是通过引用折叠的方式实现。
- 下面示例中传递给Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引用返回。
- 传递给Function的实参是左值,T被推导为int&,引用折叠为左值引用,forward内部t被强转为左值引用返回。
forward
底层其实就是强转类型转换
使用的使用要注意的是:
标签:11,string,右值,int,左值,引用,&& From: https://blog.csdn.net/2302_80253411/article/details/143370783不要在模板参数上加& 传要参数的本身的类型即可;